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.

478 lines
16KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  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 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. class KeyMappingEditorComponent::ChangeKeyButton : public Button
  20. {
  21. public:
  22. ChangeKeyButton (KeyMappingEditorComponent& kec, const CommandID command,
  23. const String& keyName, const int keyIndex)
  24. : Button (keyName),
  25. owner (kec),
  26. commandID (command),
  27. keyNum (keyIndex)
  28. {
  29. setWantsKeyboardFocus (false);
  30. setTriggeredOnMouseDown (keyNum >= 0);
  31. setTooltip (keyIndex < 0 ? TRANS("Adds a new key-mapping")
  32. : TRANS("Click to change this key-mapping"));
  33. }
  34. void paintButton (Graphics& g, bool /*isOver*/, bool /*isDown*/) override
  35. {
  36. getLookAndFeel().drawKeymapChangeButton (g, getWidth(), getHeight(), *this,
  37. keyNum >= 0 ? getName() : String());
  38. }
  39. static void menuCallback (int result, ChangeKeyButton* button)
  40. {
  41. if (button != nullptr)
  42. {
  43. switch (result)
  44. {
  45. case 1: button->assignNewKey(); break;
  46. case 2: button->owner.getMappings().removeKeyPress (button->commandID, button->keyNum); break;
  47. default: break;
  48. }
  49. }
  50. }
  51. void clicked() override
  52. {
  53. if (keyNum >= 0)
  54. {
  55. // existing key clicked..
  56. PopupMenu m;
  57. m.addItem (1, TRANS("Change this key-mapping"));
  58. m.addSeparator();
  59. m.addItem (2, TRANS("Remove this key-mapping"));
  60. m.showMenuAsync (PopupMenu::Options(),
  61. ModalCallbackFunction::forComponent (menuCallback, this));
  62. }
  63. else
  64. {
  65. assignNewKey(); // + button pressed..
  66. }
  67. }
  68. void fitToContent (const int h) noexcept
  69. {
  70. if (keyNum < 0)
  71. setSize (h, h);
  72. else
  73. setSize (jlimit (h * 4, h * 8, 6 + Font (h * 0.6f).getStringWidth (getName())), h);
  74. }
  75. //==============================================================================
  76. class KeyEntryWindow : public AlertWindow
  77. {
  78. public:
  79. KeyEntryWindow (KeyMappingEditorComponent& kec)
  80. : AlertWindow (TRANS("New key-mapping"),
  81. TRANS("Please press a key combination now..."),
  82. AlertWindow::NoIcon),
  83. owner (kec)
  84. {
  85. addButton (TRANS("OK"), 1);
  86. addButton (TRANS("Cancel"), 0);
  87. // (avoid return + escape keys getting processed by the buttons..)
  88. for (int i = getNumChildComponents(); --i >= 0;)
  89. getChildComponent (i)->setWantsKeyboardFocus (false);
  90. setWantsKeyboardFocus (true);
  91. grabKeyboardFocus();
  92. }
  93. bool keyPressed (const KeyPress& key) override
  94. {
  95. lastPress = key;
  96. String message (TRANS("Key") + ": " + owner.getDescriptionForKeyPress (key));
  97. const CommandID previousCommand = owner.getMappings().findCommandForKeyPress (key);
  98. if (previousCommand != 0)
  99. message << "\n\n("
  100. << TRANS("Currently assigned to \"CMDN\"")
  101. .replace ("CMDN", TRANS (owner.getCommandManager().getNameOfCommand (previousCommand)))
  102. << ')';
  103. setMessage (message);
  104. return true;
  105. }
  106. bool keyStateChanged (bool) override
  107. {
  108. return true;
  109. }
  110. KeyPress lastPress;
  111. private:
  112. KeyMappingEditorComponent& owner;
  113. JUCE_DECLARE_NON_COPYABLE (KeyEntryWindow)
  114. };
  115. static void assignNewKeyCallback (int result, ChangeKeyButton* button, KeyPress newKey)
  116. {
  117. if (result != 0 && button != nullptr)
  118. button->setNewKey (newKey, true);
  119. }
  120. void setNewKey (const KeyPress& newKey, bool dontAskUser)
  121. {
  122. if (newKey.isValid())
  123. {
  124. const CommandID previousCommand = owner.getMappings().findCommandForKeyPress (newKey);
  125. if (previousCommand == 0 || dontAskUser)
  126. {
  127. owner.getMappings().removeKeyPress (newKey);
  128. if (keyNum >= 0)
  129. owner.getMappings().removeKeyPress (commandID, keyNum);
  130. owner.getMappings().addKeyPress (commandID, newKey, keyNum);
  131. }
  132. else
  133. {
  134. AlertWindow::showOkCancelBox (AlertWindow::WarningIcon,
  135. TRANS("Change key-mapping"),
  136. TRANS("This key is already assigned to the command \"CMDN\"")
  137. .replace ("CMDN", owner.getCommandManager().getNameOfCommand (previousCommand))
  138. + "\n\n"
  139. + TRANS("Do you want to re-assign it to this new command instead?"),
  140. TRANS("Re-assign"),
  141. TRANS("Cancel"),
  142. this,
  143. ModalCallbackFunction::forComponent (assignNewKeyCallback,
  144. this, KeyPress (newKey)));
  145. }
  146. }
  147. }
  148. static void keyChosen (int result, ChangeKeyButton* button)
  149. {
  150. if (button != nullptr && button->currentKeyEntryWindow != nullptr)
  151. {
  152. if (result != 0)
  153. {
  154. button->currentKeyEntryWindow->setVisible (false);
  155. button->setNewKey (button->currentKeyEntryWindow->lastPress, false);
  156. }
  157. button->currentKeyEntryWindow = nullptr;
  158. }
  159. }
  160. void assignNewKey()
  161. {
  162. currentKeyEntryWindow = new KeyEntryWindow (owner);
  163. currentKeyEntryWindow->enterModalState (true, ModalCallbackFunction::forComponent (keyChosen, this));
  164. }
  165. private:
  166. KeyMappingEditorComponent& owner;
  167. const CommandID commandID;
  168. const int keyNum;
  169. ScopedPointer<KeyEntryWindow> currentKeyEntryWindow;
  170. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChangeKeyButton)
  171. };
  172. //==============================================================================
  173. class KeyMappingEditorComponent::ItemComponent : public Component
  174. {
  175. public:
  176. ItemComponent (KeyMappingEditorComponent& kec, const CommandID command)
  177. : owner (kec), commandID (command)
  178. {
  179. setInterceptsMouseClicks (false, true);
  180. const bool isReadOnly = owner.isCommandReadOnly (commandID);
  181. const Array<KeyPress> keyPresses (owner.getMappings().getKeyPressesAssignedToCommand (commandID));
  182. for (int i = 0; i < jmin ((int) maxNumAssignments, keyPresses.size()); ++i)
  183. addKeyPressButton (owner.getDescriptionForKeyPress (keyPresses.getReference (i)), i, isReadOnly);
  184. addKeyPressButton (String(), -1, isReadOnly);
  185. }
  186. void addKeyPressButton (const String& desc, const int index, const bool isReadOnly)
  187. {
  188. ChangeKeyButton* const b = new ChangeKeyButton (owner, commandID, desc, index);
  189. keyChangeButtons.add (b);
  190. b->setEnabled (! isReadOnly);
  191. b->setVisible (keyChangeButtons.size() <= (int) maxNumAssignments);
  192. addChildComponent (b);
  193. }
  194. void paint (Graphics& g) override
  195. {
  196. g.setFont (getHeight() * 0.7f);
  197. g.setColour (owner.findColour (KeyMappingEditorComponent::textColourId));
  198. g.drawFittedText (TRANS (owner.getCommandManager().getNameOfCommand (commandID)),
  199. 4, 0, jmax (40, getChildComponent (0)->getX() - 5), getHeight(),
  200. Justification::centredLeft, true);
  201. }
  202. void resized() override
  203. {
  204. int x = getWidth() - 4;
  205. for (int i = keyChangeButtons.size(); --i >= 0;)
  206. {
  207. ChangeKeyButton* const b = keyChangeButtons.getUnchecked(i);
  208. b->fitToContent (getHeight() - 2);
  209. b->setTopRightPosition (x, 1);
  210. x = b->getX() - 5;
  211. }
  212. }
  213. private:
  214. KeyMappingEditorComponent& owner;
  215. OwnedArray<ChangeKeyButton> keyChangeButtons;
  216. const CommandID commandID;
  217. enum { maxNumAssignments = 3 };
  218. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent)
  219. };
  220. //==============================================================================
  221. class KeyMappingEditorComponent::MappingItem : public TreeViewItem
  222. {
  223. public:
  224. MappingItem (KeyMappingEditorComponent& kec, const CommandID command)
  225. : owner (kec), commandID (command)
  226. {}
  227. String getUniqueName() const override { return String ((int) commandID) + "_id"; }
  228. bool mightContainSubItems() override { return false; }
  229. int getItemHeight() const override { return 20; }
  230. Component* createItemComponent() override { return new ItemComponent (owner, commandID); }
  231. private:
  232. KeyMappingEditorComponent& owner;
  233. const CommandID commandID;
  234. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MappingItem)
  235. };
  236. //==============================================================================
  237. class KeyMappingEditorComponent::CategoryItem : public TreeViewItem
  238. {
  239. public:
  240. CategoryItem (KeyMappingEditorComponent& kec, const String& name)
  241. : owner (kec), categoryName (name)
  242. {}
  243. String getUniqueName() const override { return categoryName + "_cat"; }
  244. bool mightContainSubItems() override { return true; }
  245. int getItemHeight() const override { return 22; }
  246. void paintItem (Graphics& g, int width, int height) override
  247. {
  248. g.setFont (Font (height * 0.7f, Font::bold));
  249. g.setColour (owner.findColour (KeyMappingEditorComponent::textColourId));
  250. g.drawText (TRANS (categoryName), 2, 0, width - 2, height, Justification::centredLeft, true);
  251. }
  252. void itemOpennessChanged (bool isNowOpen) override
  253. {
  254. if (isNowOpen)
  255. {
  256. if (getNumSubItems() == 0)
  257. {
  258. const Array<CommandID> commands (owner.getCommandManager().getCommandsInCategory (categoryName));
  259. for (int i = 0; i < commands.size(); ++i)
  260. if (owner.shouldCommandBeIncluded (commands.getUnchecked(i)))
  261. addSubItem (new MappingItem (owner, commands.getUnchecked(i)));
  262. }
  263. }
  264. else
  265. {
  266. clearSubItems();
  267. }
  268. }
  269. private:
  270. KeyMappingEditorComponent& owner;
  271. String categoryName;
  272. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CategoryItem)
  273. };
  274. //==============================================================================
  275. class KeyMappingEditorComponent::TopLevelItem : public TreeViewItem,
  276. public ButtonListener,
  277. private ChangeListener
  278. {
  279. public:
  280. TopLevelItem (KeyMappingEditorComponent& kec) : owner (kec)
  281. {
  282. setLinesDrawnForSubItems (false);
  283. owner.getMappings().addChangeListener (this);
  284. }
  285. ~TopLevelItem()
  286. {
  287. owner.getMappings().removeChangeListener (this);
  288. }
  289. bool mightContainSubItems() override { return true; }
  290. String getUniqueName() const override { return "keys"; }
  291. void changeListenerCallback (ChangeBroadcaster*) override
  292. {
  293. const OpennessRestorer opennessRestorer (*this);
  294. clearSubItems();
  295. const StringArray categories (owner.getCommandManager().getCommandCategories());
  296. for (int i = 0; i < categories.size(); ++i)
  297. {
  298. const Array<CommandID> commands (owner.getCommandManager().getCommandsInCategory (categories[i]));
  299. int count = 0;
  300. for (int j = 0; j < commands.size(); ++j)
  301. if (owner.shouldCommandBeIncluded (commands.getUnchecked(j)))
  302. ++count;
  303. if (count > 0)
  304. addSubItem (new CategoryItem (owner, categories[i]));
  305. }
  306. }
  307. static void resetToDefaultsCallback (int result, KeyMappingEditorComponent* owner)
  308. {
  309. if (result != 0 && owner != nullptr)
  310. owner->getMappings().resetToDefaultMappings();
  311. }
  312. void buttonClicked (Button*) override
  313. {
  314. AlertWindow::showOkCancelBox (AlertWindow::QuestionIcon,
  315. TRANS("Reset to defaults"),
  316. TRANS("Are you sure you want to reset all the key-mappings to their default state?"),
  317. TRANS("Reset"),
  318. String(),
  319. &owner,
  320. ModalCallbackFunction::forComponent (resetToDefaultsCallback, &owner));
  321. }
  322. private:
  323. KeyMappingEditorComponent& owner;
  324. };
  325. //==============================================================================
  326. KeyMappingEditorComponent::KeyMappingEditorComponent (KeyPressMappingSet& mappingManager,
  327. const bool showResetToDefaultButton)
  328. : mappings (mappingManager),
  329. resetButton (TRANS ("reset to defaults"))
  330. {
  331. treeItem = new TopLevelItem (*this);
  332. if (showResetToDefaultButton)
  333. {
  334. addAndMakeVisible (resetButton);
  335. resetButton.addListener (treeItem);
  336. }
  337. addAndMakeVisible (tree);
  338. tree.setColour (TreeView::backgroundColourId, findColour (backgroundColourId));
  339. tree.setRootItemVisible (false);
  340. tree.setDefaultOpenness (true);
  341. tree.setRootItem (treeItem);
  342. tree.setIndentSize (12);
  343. }
  344. KeyMappingEditorComponent::~KeyMappingEditorComponent()
  345. {
  346. tree.setRootItem (nullptr);
  347. }
  348. //==============================================================================
  349. void KeyMappingEditorComponent::setColours (Colour mainBackground,
  350. Colour textColour)
  351. {
  352. setColour (backgroundColourId, mainBackground);
  353. setColour (textColourId, textColour);
  354. tree.setColour (TreeView::backgroundColourId, mainBackground);
  355. }
  356. void KeyMappingEditorComponent::parentHierarchyChanged()
  357. {
  358. treeItem->changeListenerCallback (nullptr);
  359. }
  360. void KeyMappingEditorComponent::resized()
  361. {
  362. int h = getHeight();
  363. if (resetButton.isVisible())
  364. {
  365. const int buttonHeight = 20;
  366. h -= buttonHeight + 8;
  367. int x = getWidth() - 8;
  368. resetButton.changeWidthToFitText (buttonHeight);
  369. resetButton.setTopRightPosition (x, h + 6);
  370. }
  371. tree.setBounds (0, 0, getWidth(), h);
  372. }
  373. //==============================================================================
  374. bool KeyMappingEditorComponent::shouldCommandBeIncluded (const CommandID commandID)
  375. {
  376. const ApplicationCommandInfo* const ci = mappings.getCommandManager().getCommandForID (commandID);
  377. return ci != nullptr && (ci->flags & ApplicationCommandInfo::hiddenFromKeyEditor) == 0;
  378. }
  379. bool KeyMappingEditorComponent::isCommandReadOnly (const CommandID commandID)
  380. {
  381. const ApplicationCommandInfo* const ci = mappings.getCommandManager().getCommandForID (commandID);
  382. return ci != nullptr && (ci->flags & ApplicationCommandInfo::readOnlyInKeyEditor) != 0;
  383. }
  384. String KeyMappingEditorComponent::getDescriptionForKeyPress (const KeyPress& key)
  385. {
  386. return key.getTextDescription();
  387. }