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
17KB

  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. class KeyMappingEditorComponent::ChangeKeyButton final : public Button
  21. {
  22. public:
  23. ChangeKeyButton (KeyMappingEditorComponent& kec, CommandID command,
  24. const String& keyName, int keyIndex)
  25. : Button (keyName),
  26. owner (kec),
  27. commandID (command),
  28. keyNum (keyIndex)
  29. {
  30. setWantsKeyboardFocus (false);
  31. setTriggeredOnMouseDown (keyNum >= 0);
  32. setTooltip (keyIndex < 0 ? TRANS ("Adds a new key-mapping")
  33. : TRANS ("Click to change this key-mapping"));
  34. }
  35. void paintButton (Graphics& g, bool /*isOver*/, bool /*isDown*/) override
  36. {
  37. getLookAndFeel().drawKeymapChangeButton (g, getWidth(), getHeight(), *this,
  38. keyNum >= 0 ? getName() : String());
  39. }
  40. void clicked() override
  41. {
  42. if (keyNum >= 0)
  43. {
  44. Component::SafePointer<ChangeKeyButton> button (this);
  45. PopupMenu m;
  46. m.addItem (TRANS ("Change this key-mapping"),
  47. [button]
  48. {
  49. if (button != nullptr)
  50. button.getComponent()->assignNewKey();
  51. });
  52. m.addSeparator();
  53. m.addItem (TRANS ("Remove this key-mapping"),
  54. [button]
  55. {
  56. if (button != nullptr)
  57. button->owner.getMappings().removeKeyPress (button->commandID,
  58. button->keyNum);
  59. });
  60. m.showMenuAsync (PopupMenu::Options().withTargetComponent (this));
  61. }
  62. else
  63. {
  64. assignNewKey(); // + button pressed..
  65. }
  66. }
  67. using Button::clicked;
  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 ((float) h * 0.6f).getStringWidth (getName())), h);
  74. }
  75. //==============================================================================
  76. class KeyEntryWindow final : public AlertWindow
  77. {
  78. public:
  79. KeyEntryWindow (KeyMappingEditorComponent& kec)
  80. : AlertWindow (TRANS ("New key-mapping"),
  81. TRANS ("Please press a key combination now..."),
  82. MessageBoxIconType::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 (auto* child : getChildren())
  89. child->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. auto 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. void setNewKey (const KeyPress& newKey, bool dontAskUser)
  116. {
  117. if (newKey.isValid())
  118. {
  119. auto previousCommand = owner.getMappings().findCommandForKeyPress (newKey);
  120. if (previousCommand == 0 || dontAskUser)
  121. {
  122. owner.getMappings().removeKeyPress (newKey);
  123. if (keyNum >= 0)
  124. owner.getMappings().removeKeyPress (commandID, keyNum);
  125. owner.getMappings().addKeyPress (commandID, newKey, keyNum);
  126. }
  127. else
  128. {
  129. auto options = MessageBoxOptions::makeOptionsOkCancel (MessageBoxIconType::WarningIcon,
  130. TRANS ("Change key-mapping"),
  131. TRANS ("This key is already assigned to the command \"CMDN\"")
  132. .replace ("CMDN", owner.getCommandManager().getNameOfCommand (previousCommand))
  133. + "\n\n"
  134. + TRANS ("Do you want to re-assign it to this new command instead?"),
  135. TRANS ("Re-assign"),
  136. TRANS ("Cancel"),
  137. this);
  138. messageBox = AlertWindow::showScopedAsync (options, [this, newKey] (int result)
  139. {
  140. if (result != 0)
  141. setNewKey (newKey, true);
  142. });
  143. }
  144. }
  145. }
  146. static void keyChosen (int result, ChangeKeyButton* button)
  147. {
  148. if (button != nullptr && button->currentKeyEntryWindow != nullptr)
  149. {
  150. if (result != 0)
  151. {
  152. button->currentKeyEntryWindow->setVisible (false);
  153. button->setNewKey (button->currentKeyEntryWindow->lastPress, false);
  154. }
  155. button->currentKeyEntryWindow.reset();
  156. }
  157. }
  158. void assignNewKey()
  159. {
  160. currentKeyEntryWindow.reset (new KeyEntryWindow (owner));
  161. currentKeyEntryWindow->enterModalState (true, ModalCallbackFunction::forComponent (keyChosen, this));
  162. }
  163. private:
  164. KeyMappingEditorComponent& owner;
  165. const CommandID commandID;
  166. const int keyNum;
  167. std::unique_ptr<KeyEntryWindow> currentKeyEntryWindow;
  168. ScopedMessageBox messageBox;
  169. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChangeKeyButton)
  170. };
  171. //==============================================================================
  172. class KeyMappingEditorComponent::ItemComponent final : public Component
  173. {
  174. public:
  175. ItemComponent (KeyMappingEditorComponent& kec, CommandID command)
  176. : owner (kec), commandID (command)
  177. {
  178. setInterceptsMouseClicks (false, true);
  179. const bool isReadOnly = owner.isCommandReadOnly (commandID);
  180. auto keyPresses = owner.getMappings().getKeyPressesAssignedToCommand (commandID);
  181. for (int i = 0; i < jmin ((int) maxNumAssignments, keyPresses.size()); ++i)
  182. addKeyPressButton (owner.getDescriptionForKeyPress (keyPresses.getReference (i)), i, isReadOnly);
  183. addKeyPressButton ("Change Key Mapping", -1, isReadOnly);
  184. }
  185. void addKeyPressButton (const String& desc, const int index, const bool isReadOnly)
  186. {
  187. auto* b = new ChangeKeyButton (owner, commandID, desc, index);
  188. keyChangeButtons.add (b);
  189. b->setEnabled (! isReadOnly);
  190. b->setVisible (keyChangeButtons.size() <= (int) maxNumAssignments);
  191. addChildComponent (b);
  192. }
  193. void paint (Graphics& g) override
  194. {
  195. g.setFont ((float) getHeight() * 0.7f);
  196. g.setColour (owner.findColour (KeyMappingEditorComponent::textColourId));
  197. g.drawFittedText (TRANS (owner.getCommandManager().getNameOfCommand (commandID)),
  198. 4, 0, jmax (40, getChildComponent (0)->getX() - 5), getHeight(),
  199. Justification::centredLeft, true);
  200. }
  201. void resized() override
  202. {
  203. int x = getWidth() - 4;
  204. for (int i = keyChangeButtons.size(); --i >= 0;)
  205. {
  206. auto* b = keyChangeButtons.getUnchecked (i);
  207. b->fitToContent (getHeight() - 2);
  208. b->setTopRightPosition (x, 1);
  209. x = b->getX() - 5;
  210. }
  211. }
  212. private:
  213. std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
  214. {
  215. return createIgnoredAccessibilityHandler (*this);
  216. }
  217. KeyMappingEditorComponent& owner;
  218. OwnedArray<ChangeKeyButton> keyChangeButtons;
  219. const CommandID commandID;
  220. enum { maxNumAssignments = 3 };
  221. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ItemComponent)
  222. };
  223. //==============================================================================
  224. class KeyMappingEditorComponent::MappingItem final : public TreeViewItem
  225. {
  226. public:
  227. MappingItem (KeyMappingEditorComponent& kec, CommandID command)
  228. : owner (kec), commandID (command)
  229. {}
  230. String getUniqueName() const override { return String ((int) commandID) + "_id"; }
  231. bool mightContainSubItems() override { return false; }
  232. int getItemHeight() const override { return 20; }
  233. std::unique_ptr<Component> createItemComponent() override { return std::make_unique<ItemComponent> (owner, commandID); }
  234. String getAccessibilityName() override { return TRANS (owner.getCommandManager().getNameOfCommand (commandID)); }
  235. private:
  236. KeyMappingEditorComponent& owner;
  237. const CommandID commandID;
  238. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MappingItem)
  239. };
  240. //==============================================================================
  241. class KeyMappingEditorComponent::CategoryItem final : public TreeViewItem
  242. {
  243. public:
  244. CategoryItem (KeyMappingEditorComponent& kec, const String& name)
  245. : owner (kec), categoryName (name)
  246. {}
  247. String getUniqueName() const override { return categoryName + "_cat"; }
  248. bool mightContainSubItems() override { return true; }
  249. int getItemHeight() const override { return 22; }
  250. String getAccessibilityName() override { return categoryName; }
  251. void paintItem (Graphics& g, int width, int height) override
  252. {
  253. g.setFont (Font ((float) height * 0.7f, Font::bold));
  254. g.setColour (owner.findColour (KeyMappingEditorComponent::textColourId));
  255. g.drawText (TRANS (categoryName), 2, 0, width - 2, height, Justification::centredLeft, true);
  256. }
  257. void itemOpennessChanged (bool isNowOpen) override
  258. {
  259. if (isNowOpen)
  260. {
  261. if (getNumSubItems() == 0)
  262. for (auto command : owner.getCommandManager().getCommandsInCategory (categoryName))
  263. if (owner.shouldCommandBeIncluded (command))
  264. addSubItem (new MappingItem (owner, command));
  265. }
  266. else
  267. {
  268. clearSubItems();
  269. }
  270. }
  271. private:
  272. KeyMappingEditorComponent& owner;
  273. String categoryName;
  274. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CategoryItem)
  275. };
  276. //==============================================================================
  277. class KeyMappingEditorComponent::TopLevelItem final : public TreeViewItem,
  278. private ChangeListener
  279. {
  280. public:
  281. TopLevelItem (KeyMappingEditorComponent& kec) : owner (kec)
  282. {
  283. setLinesDrawnForSubItems (false);
  284. owner.getMappings().addChangeListener (this);
  285. }
  286. ~TopLevelItem() override
  287. {
  288. owner.getMappings().removeChangeListener (this);
  289. }
  290. bool mightContainSubItems() override { return true; }
  291. String getUniqueName() const override { return "keys"; }
  292. void changeListenerCallback (ChangeBroadcaster*) override
  293. {
  294. const OpennessRestorer opennessRestorer (*this);
  295. clearSubItems();
  296. for (auto category : owner.getCommandManager().getCommandCategories())
  297. {
  298. int count = 0;
  299. for (auto command : owner.getCommandManager().getCommandsInCategory (category))
  300. if (owner.shouldCommandBeIncluded (command))
  301. ++count;
  302. if (count > 0)
  303. addSubItem (new CategoryItem (owner, category));
  304. }
  305. }
  306. private:
  307. KeyMappingEditorComponent& owner;
  308. };
  309. //==============================================================================
  310. KeyMappingEditorComponent::KeyMappingEditorComponent (KeyPressMappingSet& mappingManager,
  311. const bool showResetToDefaultButton)
  312. : mappings (mappingManager),
  313. resetButton (TRANS ("reset to defaults"))
  314. {
  315. treeItem.reset (new TopLevelItem (*this));
  316. if (showResetToDefaultButton)
  317. {
  318. addAndMakeVisible (resetButton);
  319. resetButton.onClick = [this]
  320. {
  321. auto options = MessageBoxOptions::makeOptionsOkCancel (MessageBoxIconType::QuestionIcon,
  322. TRANS ("Reset to defaults"),
  323. TRANS ("Are you sure you want to reset all the key-mappings to their default state?"),
  324. TRANS ("Reset"),
  325. {},
  326. this);
  327. messageBox = AlertWindow::showScopedAsync (options, [this] (int result)
  328. {
  329. if (result != 0)
  330. getMappings().resetToDefaultMappings();
  331. });
  332. };
  333. }
  334. addAndMakeVisible (tree);
  335. tree.setTitle ("Key Mappings");
  336. tree.setColour (TreeView::backgroundColourId, findColour (backgroundColourId));
  337. tree.setRootItemVisible (false);
  338. tree.setDefaultOpenness (true);
  339. tree.setRootItem (treeItem.get());
  340. tree.setIndentSize (12);
  341. }
  342. KeyMappingEditorComponent::~KeyMappingEditorComponent()
  343. {
  344. tree.setRootItem (nullptr);
  345. }
  346. //==============================================================================
  347. void KeyMappingEditorComponent::setColours (Colour mainBackground,
  348. Colour textColour)
  349. {
  350. setColour (backgroundColourId, mainBackground);
  351. setColour (textColourId, textColour);
  352. tree.setColour (TreeView::backgroundColourId, mainBackground);
  353. }
  354. void KeyMappingEditorComponent::parentHierarchyChanged()
  355. {
  356. treeItem->changeListenerCallback (nullptr);
  357. }
  358. void KeyMappingEditorComponent::resized()
  359. {
  360. int h = getHeight();
  361. if (resetButton.isVisible())
  362. {
  363. const int buttonHeight = 20;
  364. h -= buttonHeight + 8;
  365. int x = getWidth() - 8;
  366. resetButton.changeWidthToFitText (buttonHeight);
  367. resetButton.setTopRightPosition (x, h + 6);
  368. }
  369. tree.setBounds (0, 0, getWidth(), h);
  370. }
  371. //==============================================================================
  372. bool KeyMappingEditorComponent::shouldCommandBeIncluded (const CommandID commandID)
  373. {
  374. auto* ci = mappings.getCommandManager().getCommandForID (commandID);
  375. return ci != nullptr && (ci->flags & ApplicationCommandInfo::hiddenFromKeyEditor) == 0;
  376. }
  377. bool KeyMappingEditorComponent::isCommandReadOnly (const CommandID commandID)
  378. {
  379. auto* ci = mappings.getCommandManager().getCommandForID (commandID);
  380. return ci != nullptr && (ci->flags & ApplicationCommandInfo::readOnlyInKeyEditor) != 0;
  381. }
  382. String KeyMappingEditorComponent::getDescriptionForKeyPress (const KeyPress& key)
  383. {
  384. return key.getTextDescription();
  385. }
  386. } // namespace juce