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