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.

476 lines
16KB

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