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