3083d4f6508b1b3214ac4b1d2847f726b8937674
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / keymap / impl / ui / KeymapPanel.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.keymap.impl.ui;
17
18 import com.intellij.CommonBundle;
19 import com.intellij.icons.AllIcons;
20 import com.intellij.ide.CommonActionsManager;
21 import com.intellij.ide.DataManager;
22 import com.intellij.ide.TreeExpander;
23 import com.intellij.openapi.Disposable;
24 import com.intellij.openapi.actionSystem.*;
25 import com.intellij.openapi.actionSystem.ex.QuickList;
26 import com.intellij.openapi.actionSystem.ex.QuickListsManager;
27 import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl;
28 import com.intellij.openapi.application.ApplicationManager;
29 import com.intellij.openapi.keymap.*;
30 import com.intellij.openapi.keymap.ex.KeymapManagerEx;
31 import com.intellij.openapi.keymap.impl.ActionShortcutRestrictions;
32 import com.intellij.openapi.keymap.impl.KeymapImpl;
33 import com.intellij.openapi.keymap.impl.KeymapManagerImpl;
34 import com.intellij.openapi.keymap.impl.ShortcutRestrictions;
35 import com.intellij.openapi.options.Configurable;
36 import com.intellij.openapi.options.ConfigurationException;
37 import com.intellij.openapi.options.SearchableConfigurable;
38 import com.intellij.openapi.project.DumbAwareAction;
39 import com.intellij.openapi.ui.Messages;
40 import com.intellij.openapi.ui.popup.JBPopupFactory;
41 import com.intellij.openapi.ui.popup.ListPopup;
42 import com.intellij.openapi.util.Comparing;
43 import com.intellij.openapi.util.Condition;
44 import com.intellij.openapi.util.Disposer;
45 import com.intellij.openapi.util.SystemInfo;
46 import com.intellij.openapi.util.registry.Registry;
47 import com.intellij.openapi.util.text.StringUtil;
48 import com.intellij.openapi.wm.IdeFocusManager;
49 import com.intellij.openapi.wm.IdeFrame;
50 import com.intellij.packageDependencies.ui.TreeExpansionMonitor;
51 import com.intellij.ui.DoubleClickListener;
52 import com.intellij.ui.FilterComponent;
53 import com.intellij.ui.awt.RelativePoint;
54 import com.intellij.util.Alarm;
55 import com.intellij.util.ui.ComboBoxModelEditor;
56 import com.intellij.util.ui.ListItemEditor;
57 import com.intellij.util.ui.tree.TreeUtil;
58 import gnu.trove.THashSet;
59 import org.jetbrains.annotations.Nls;
60 import org.jetbrains.annotations.NotNull;
61 import org.jetbrains.annotations.Nullable;
62
63 import javax.swing.*;
64 import java.awt.*;
65 import java.awt.event.*;
66 import java.beans.PropertyChangeEvent;
67 import java.beans.PropertyChangeListener;
68 import java.util.ArrayList;
69 import java.util.List;
70 import java.util.Map;
71 import java.util.Set;
72
73 public class KeymapPanel extends JPanel implements SearchableConfigurable, Configurable.NoScroll, KeymapListener, Disposable {
74   private static final Condition<Keymap> KEYMAP_FILTER = new Condition<Keymap>() {
75     @Override
76     public boolean value(Keymap keymap) {
77       return !SystemInfo.isMac || !KeymapManager.DEFAULT_IDEA_KEYMAP.equals(keymap.getName());
78     }
79   };
80
81   // Name editor calls "setName" to apply new name. It is scheme name, not presentable name —
82   // but only bundled scheme name could be different from presentable and bundled scheme is not editable (could not be renamed). So, it is ok.
83   private final ComboBoxModelEditor<Keymap> myEditor = new ComboBoxModelEditor<Keymap>(new ListItemEditor<Keymap>() {
84     @NotNull
85     @Override
86     public String getName(@NotNull Keymap item) {
87       String name = item.getPresentableName();
88       return name == null ? KeyMapBundle.message("keymap.noName.presentable.name") : name;
89     }
90
91     @NotNull
92     @Override
93     public Class<? extends Keymap> getItemClass() {
94       return KeymapImpl.class;
95     }
96
97     @Override
98     public Keymap clone(@NotNull Keymap item, boolean forInPlaceEditing) {
99       return ((KeymapImpl)item).copy();
100     }
101
102     @Override
103     public void applyModifiedProperties(@NotNull Keymap newItem, @NotNull Keymap oldItem) {
104       ((KeymapImpl)newItem).copyTo((KeymapImpl)oldItem);
105     }
106
107     @Override
108     public boolean isRemovable(@NotNull Keymap item) {
109       return item.canModify();
110     }
111
112     @Override
113     public boolean isEditable(@NotNull Keymap item) {
114       return item.canModify();
115     }
116   });
117
118   private JButton myCopyButton;
119   private JButton myDeleteButton;
120   private JButton myResetToDefault;
121   private JCheckBox myNonEnglishKeyboardSupportOption;
122
123   private JLabel myBaseKeymapLabel;
124
125   private ActionsTree myActionsTree;
126   private FilterComponent myFilterComponent;
127   private TreeExpansionMonitor myTreeExpansionMonitor;
128   private final ShortcutFilteringPanel myFilteringPanel = new ShortcutFilteringPanel();
129
130   private boolean myQuickListsModified = false;
131   private QuickList[] myQuickLists = QuickListsManager.getInstance().getAllQuickLists();
132
133   public KeymapPanel() {
134     setLayout(new BorderLayout());
135     JPanel keymapPanel = new JPanel(new BorderLayout());
136     keymapPanel.add(createKeymapListPanel(), BorderLayout.NORTH);
137     keymapPanel.add(createKeymapSettingsPanel(), BorderLayout.CENTER);
138     add(keymapPanel, BorderLayout.CENTER);
139     addPropertyChangeListener(new PropertyChangeListener() {
140       @Override
141       public void propertyChange(@NotNull final PropertyChangeEvent evt) {
142         if (evt.getPropertyName().equals("ancestor") && evt.getNewValue() != null && evt.getOldValue() == null && myQuickListsModified) {
143           currentKeymapChanged();
144           myQuickListsModified = false;
145         }
146       }
147     });
148     myFilteringPanel.addPropertyChangeListener("shortcut", new PropertyChangeListener() {
149       @Override
150       public void propertyChange(PropertyChangeEvent event) {
151         filterTreeByShortcut(myFilteringPanel.getShortcut());
152       }
153     });
154     //ApplicationManager.getApplication().getMessageBus().connect(this).subscribe(CHANGE_TOPIC, this);
155   }
156
157   @Override
158   public void updateUI() {
159     super.updateUI();
160     if (myFilteringPanel != null) {
161       SwingUtilities.updateComponentTreeUI(myFilteringPanel);
162     }
163   }
164
165   @Override
166   public void quickListRenamed(final QuickList oldQuickList, final QuickList newQuickList) {
167     for (Keymap keymap : myEditor.getModel().getItems()) {
168       String actionId = oldQuickList.getActionId();
169       Shortcut[] shortcuts = keymap.getShortcuts(actionId);
170       if (shortcuts.length != 0) {
171         String newActionId = newQuickList.getActionId();
172         for (Shortcut shortcut : shortcuts) {
173           keymap.removeShortcut(actionId, shortcut);
174           keymap.addShortcut(newActionId, shortcut);
175         }
176       }
177     }
178
179     myQuickListsModified = true;
180   }
181
182   private JPanel createKeymapListPanel() {
183     JPanel panel = new JPanel();
184     panel.setLayout(new GridBagLayout());
185
186     JLabel keymapLabel = new JLabel(KeyMapBundle.message("keymaps.border.factory.title"));
187     keymapLabel.setLabelFor(myEditor.getComboBox());
188     panel.add(keymapLabel, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
189     panel.add(myEditor.getComboBox(), new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 0), 0, 0));
190
191     panel.add(createKeymapButtonsPanel(), new GridBagConstraints(2, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
192     myEditor.getComboBox().addActionListener(new ActionListener() {
193       @Override
194       public void actionPerformed(@NotNull ActionEvent e) {
195         currentKeymapChanged();
196       }
197     });
198     panel.add(createKeymapNamePanel(), new GridBagConstraints(3, 0, 1, 1, 1, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 10, 0, 0), 0, 0));
199     return panel;
200   }
201
202   @Override
203   public Runnable enableSearch(final String option) {
204     return new Runnable() {
205       @Override
206       public void run() {
207         showOption(option);
208       }
209     };
210   }
211
212   @Override
213   public void processCurrentKeymapChanged() {
214     currentKeymapChanged();
215   }
216
217   @Override
218   public void processCurrentKeymapChanged(@NotNull QuickList[] ids) {
219     myQuickLists = ids;
220     currentKeymapChanged();
221   }
222
223   private void currentKeymapChanged() {
224     myResetToDefault.setEnabled(false);
225
226     Keymap selectedKeymap = myEditor.getModel().getSelected();
227
228     boolean editable = selectedKeymap != null && selectedKeymap.canModify();
229     myDeleteButton.setEnabled(editable);
230     myCopyButton.setEnabled(selectedKeymap != null);
231     myEditor.getComboBox().setEditable(editable);
232
233     if (selectedKeymap == null) {
234       myActionsTree.reset(new KeymapImpl(), myQuickLists);
235       return;
236     }
237
238     Keymap parent = selectedKeymap.getParent();
239     if (parent == null || !selectedKeymap.canModify()) {
240       myBaseKeymapLabel.setText("");
241     }
242     else {
243       myBaseKeymapLabel.setText(KeyMapBundle.message("based.on.keymap.label", parent.getPresentableName()));
244       if (selectedKeymap.canModify() && ((KeymapImpl)selectedKeymap).getOwnActionIds().length > 0) {
245         myResetToDefault.setEnabled(true);
246       }
247     }
248
249     myActionsTree.reset(selectedKeymap, myQuickLists);
250   }
251
252   private JPanel createKeymapButtonsPanel() {
253     final JPanel panel = new JPanel();
254     panel.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0));
255     panel.setLayout(new GridBagLayout());
256     myCopyButton = new JButton(new AbstractAction(KeyMapBundle.message("copy.keymap.button")) {
257       @Override
258       public void actionPerformed(ActionEvent e) {
259             copyKeymap();
260       }
261     });
262     Insets insets = new Insets(2, 2, 2, 2);
263     myCopyButton.setMargin(insets);
264     final GridBagConstraints gc =
265       new GridBagConstraints(GridBagConstraints.RELATIVE, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 5, 0, 0), 0, 0);
266     panel.add(myCopyButton, gc);
267     myResetToDefault = new JButton(CommonBundle.message("button.reset"));
268     myResetToDefault.setMargin(insets);
269     panel.add(myResetToDefault, gc);
270     myDeleteButton = new JButton(new AbstractAction(KeyMapBundle.message("delete.keymap.button")) {
271       @Override
272       public void actionPerformed(ActionEvent e) {
273         deleteKeymap();
274       }
275     });
276     myDeleteButton.setMargin(insets);
277     gc.weightx = 1;
278     panel.add(myDeleteButton, gc);
279     IdeFrame ideFrame = IdeFocusManager.getGlobalInstance().getLastFocusedFrame();
280     if (ideFrame != null && KeyboardSettingsExternalizable.isSupportedKeyboardLayout(ideFrame.getComponent()))
281     {
282       String displayLanguage = ideFrame.getComponent().getInputContext().getLocale().getDisplayLanguage();
283       myNonEnglishKeyboardSupportOption = new JCheckBox(new AbstractAction(displayLanguage + " " + KeyMapBundle.message("use.non.english.keyboard.layout.support")) {
284         @Override
285         public void actionPerformed(ActionEvent e) {
286           KeyboardSettingsExternalizable.getInstance().setNonEnglishKeyboardSupportEnabled(myNonEnglishKeyboardSupportOption.isSelected());
287         }
288       });
289       myNonEnglishKeyboardSupportOption.setSelected(KeyboardSettingsExternalizable.getInstance().isNonEnglishKeyboardSupportEnabled());
290       panel.add(myNonEnglishKeyboardSupportOption, gc);
291     }
292
293     myResetToDefault.addActionListener(new ActionListener() {
294       @Override
295       public void actionPerformed(@NotNull ActionEvent e) {
296         resetKeymap();
297       }
298     });
299     return panel;
300   }
301
302   private JPanel createKeymapSettingsPanel() {
303     JPanel panel = new JPanel();
304     panel.setLayout(new BorderLayout());
305
306     myActionsTree = new ActionsTree();
307
308     panel.add(createToolbarPanel(), BorderLayout.NORTH);
309     panel.add(myActionsTree.getComponent(), BorderLayout.CENTER);
310
311     myTreeExpansionMonitor = TreeExpansionMonitor.install(myActionsTree.getTree());
312
313     new DoubleClickListener() {
314       @Override
315       protected boolean onDoubleClick(MouseEvent e) {
316         editSelection(e);
317         return true;
318       }
319     }.installOn(myActionsTree.getTree());
320
321
322     myActionsTree.getTree().addMouseListener(new MouseAdapter() {
323       @Override
324       public void mousePressed(@NotNull MouseEvent e) {
325         if (e.isPopupTrigger()) {
326           editSelection(e);
327           e.consume();
328         }
329       }
330
331       @Override
332       public void mouseReleased(@NotNull MouseEvent e) {
333         if (e.isPopupTrigger()) {
334           editSelection(e);
335           e.consume();
336         }
337       }
338     });
339     return panel;
340   }
341
342   private JPanel createToolbarPanel() {
343     final JPanel panel = new JPanel(new GridBagLayout());
344     DefaultActionGroup group = new DefaultActionGroup();
345     final JComponent toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, true).getComponent();
346     final CommonActionsManager commonActionsManager = CommonActionsManager.getInstance();
347     final TreeExpander treeExpander = new TreeExpander() {
348       @Override
349       public void expandAll() {
350         TreeUtil.expandAll(myActionsTree.getTree());
351       }
352
353       @Override
354       public boolean canExpand() {
355         return true;
356       }
357
358       @Override
359       public void collapseAll() {
360         TreeUtil.collapseAll(myActionsTree.getTree(), 0);
361       }
362
363       @Override
364       public boolean canCollapse() {
365         return true;
366       }
367     };
368     group.add(commonActionsManager.createExpandAllAction(treeExpander, myActionsTree.getTree()));
369     group.add(commonActionsManager.createCollapseAllAction(treeExpander, myActionsTree.getTree()));
370
371     group.add(new AnAction("Edit Shortcut", "Edit Shortcut", AllIcons.ToolbarDecorator.Edit) {
372       {
373         registerCustomShortcutSet(CommonShortcuts.ENTER, myActionsTree.getTree());
374       }
375
376       @Override
377       public void update(@NotNull AnActionEvent e) {
378         final String actionId = myActionsTree.getSelectedActionId();
379         e.getPresentation().setEnabled(actionId != null);
380       }
381
382       @Override
383       public void actionPerformed(@NotNull AnActionEvent e) {
384         editSelection(e.getInputEvent());
385       }
386     });
387
388     panel.add(toolbar, new GridBagConstraints(0, 0, 1, 1, 1, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(8, 0, 0, 0), 0, 0));
389     group = new DefaultActionGroup();
390     final JComponent searchToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, true).getComponent();
391     final Alarm alarm = new Alarm();
392     myFilterComponent = new FilterComponent("KEYMAP", 5) {
393       @Override
394       public void filter() {
395         alarm.cancelAllRequests();
396         alarm.addRequest(new Runnable() {
397           @Override
398           public void run() {
399             if (!myFilterComponent.isShowing()) return;
400             myTreeExpansionMonitor.freeze();
401             final String filter = getFilter();
402             myActionsTree.filter(filter, myQuickLists);
403             final JTree tree = myActionsTree.getTree();
404             TreeUtil.expandAll(tree);
405             if (filter == null || filter.length() == 0) {
406               TreeUtil.collapseAll(tree, 0);
407               myTreeExpansionMonitor.restore();
408             }
409             else {
410               myTreeExpansionMonitor.unfreeze();
411             }
412           }
413         }, 300);
414       }
415     };
416     myFilterComponent.reset();
417
418     panel.add(myFilterComponent, new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(8, 0, 0, 0), 0, 0));
419
420     group.add(new DumbAwareAction(KeyMapBundle.message("filter.shortcut.action.text"),
421                                   KeyMapBundle.message("filter.shortcut.action.text"),
422                                   AllIcons.Actions.ShortcutFilter) {
423       @Override
424       public void actionPerformed(@NotNull AnActionEvent e) {
425         myFilterComponent.reset();
426         //noinspection ConstantConditions
427         myActionsTree.reset(myEditor.getModel().getSelected(), myQuickLists);
428         myFilteringPanel.showPopup(searchToolbar);
429       }
430     });
431     group.add(new DumbAwareAction(KeyMapBundle.message("filter.clear.action.text"),
432                                   KeyMapBundle.message("filter.clear.action.text"), AllIcons.Actions.GC) {
433       @Override
434       public void actionPerformed(@NotNull AnActionEvent e) {
435         myTreeExpansionMonitor.freeze();
436         myActionsTree.filter(null, myQuickLists); //clear filtering
437         TreeUtil.collapseAll(myActionsTree.getTree(), 0);
438         myTreeExpansionMonitor.restore();
439       }
440     });
441
442     panel.add(searchToolbar, new GridBagConstraints(2, 0, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(8, 0, 0, 0), 0, 0));
443     return panel;
444   }
445
446   private JPanel createKeymapNamePanel() {
447     JPanel panel = new JPanel(new GridBagLayout());
448     myBaseKeymapLabel = new JLabel(KeyMapBundle.message("parent.keymap.label"));
449     panel.add(myBaseKeymapLabel,
450               new GridBagConstraints(0, 0, 1, 1, 1, 0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(0, 16, 0, 0), 0, 0));
451     return panel;
452   }
453
454   private void filterTreeByShortcut(Shortcut shortcut) {
455     myTreeExpansionMonitor.freeze();
456     myActionsTree.filterTree(shortcut, myQuickLists);
457     final JTree tree = myActionsTree.getTree();
458     TreeUtil.expandAll(tree);
459     myTreeExpansionMonitor.restore();
460   }
461
462   public void showOption(String option) {
463     //noinspection ConstantConditions
464     myActionsTree.reset(myEditor.getModel().getSelected(), myQuickLists);
465     myFilterComponent.setFilter(option);
466     myActionsTree.filter(option, myQuickLists);
467   }
468
469   private void addKeyboardShortcut(@NotNull String actionId, @Nullable Shortcut shortcut) {
470     Keymap keymap = createKeymapCopyIfNeeded();
471     addKeyboardShortcut(actionId, shortcut, keymap, this, myQuickLists);
472     repaintLists();
473     currentKeymapChanged();
474   }
475
476   public static void addKeyboardShortcut(@NotNull String actionId,
477                                          @Nullable Shortcut shortcut,
478                                          @NotNull Keymap keymap,
479                                          @NotNull Component parent,
480                                          @NotNull QuickList[] quickLists) {
481     KeyboardShortcutDialog dialog = new KeyboardShortcutDialog(parent);
482     KeyboardShortcut keyboardShortcut = dialog.showAndGet(shortcut, actionId, keymap, quickLists);
483     if (keyboardShortcut == null) {
484       return;
485     }
486
487     Map<String, ArrayList<KeyboardShortcut>> conflicts = keymap.getConflicts(actionId, keyboardShortcut);
488     if (!conflicts.isEmpty()) {
489       int result = Messages.showYesNoCancelDialog(
490         parent, 
491         KeyMapBundle.message("conflict.shortcut.dialog.message"),
492         KeyMapBundle.message("conflict.shortcut.dialog.title"),
493         KeyMapBundle.message("conflict.shortcut.dialog.remove.button"),
494         KeyMapBundle.message("conflict.shortcut.dialog.leave.button"),
495         KeyMapBundle.message("conflict.shortcut.dialog.cancel.button"),
496         Messages.getWarningIcon());
497
498       if (result == Messages.YES) {
499         for (String id : conflicts.keySet()) {
500           for (KeyboardShortcut s : conflicts.get(id)) {
501             keymap.removeShortcut(id, s);
502           }
503         }
504       }
505       else if (result != Messages.NO) {
506         return;
507       }
508     }
509
510     // if shortcut is already registered to this action, just select it in the list
511     Shortcut[] shortcuts = keymap.getShortcuts(actionId);
512     for (Shortcut s : shortcuts) {
513       if (s.equals(keyboardShortcut)) {
514         return;
515       }
516     }
517
518     keymap.addShortcut(actionId, keyboardShortcut);
519     if (StringUtil.startsWithChar(actionId, '$')) {
520       keymap.addShortcut(KeyMapBundle.message("editor.shortcut", actionId.substring(1)), keyboardShortcut);
521     }
522   }
523
524   private void addMouseShortcut(Shortcut shortcut, ShortcutRestrictions restrictions) {
525     String actionId = myActionsTree.getSelectedActionId();
526     if (actionId == null) {
527       return;
528     }
529
530     Keymap keymap = createKeymapCopyIfNeeded();
531
532     MouseShortcutDialog dialog = new MouseShortcutDialog(this, restrictions.allowMouseDoubleClick);
533     MouseShortcut mouseShortcut = dialog.showAndGet(shortcut, actionId, keymap, myQuickLists);
534     if (mouseShortcut == null) {
535       return;
536     }
537
538     String[] actionIds = keymap.getActionIds(mouseShortcut);
539     if (actionIds.length > 1 || (actionIds.length == 1 && !actionId.equals(actionIds[0]))) {
540       int result = Messages.showYesNoCancelDialog(
541         this,
542         KeyMapBundle.message("conflict.shortcut.dialog.message"),
543         KeyMapBundle.message("conflict.shortcut.dialog.title"),
544         KeyMapBundle.message("conflict.shortcut.dialog.remove.button"),
545         KeyMapBundle.message("conflict.shortcut.dialog.leave.button"),
546         KeyMapBundle.message("conflict.shortcut.dialog.cancel.button"),
547         Messages.getWarningIcon());
548
549       if (result == Messages.YES) {
550         for (String id : actionIds) {
551           keymap.removeShortcut(id, mouseShortcut);
552         }
553       }
554       else if (result != Messages.NO) {
555         return;
556       }
557     }
558
559     // if shortcut is already registered to this action, just select it in the list
560
561     Shortcut[] shortcuts = keymap.getShortcuts(actionId);
562     for (Shortcut shortcut1 : shortcuts) {
563       if (shortcut1.equals(mouseShortcut)) {
564         return;
565       }
566     }
567
568     keymap.addShortcut(actionId, mouseShortcut);
569     if (StringUtil.startsWithChar(actionId, '$')) {
570       keymap.addShortcut(KeyMapBundle.message("editor.shortcut", actionId.substring(1)), mouseShortcut);
571     }
572
573     repaintLists();
574     currentKeymapChanged();
575   }
576
577   private void repaintLists() {
578     myActionsTree.getComponent().repaint();
579   }
580
581   @NotNull
582   private Keymap createKeymapCopyIfNeeded() {
583     Keymap keymap = myEditor.getModel().getSelected();
584     assert keymap != null;
585     if (keymap.canModify()) {
586       Keymap mutable = myEditor.getMutable(keymap);
587       myActionsTree.setKeymap(mutable);
588       return mutable;
589     }
590
591     String newKeymapName = KeyMapBundle.message("new.keymap.name", keymap.getPresentableName());
592     if (!tryNewKeymapName(newKeymapName)) {
593       for (int i = 0; ; i++) {
594         newKeymapName = KeyMapBundle.message("new.indexed.keymap.name", keymap.getPresentableName(), i);
595         if (tryNewKeymapName(newKeymapName)) {
596           break;
597         }
598       }
599     }
600
601     KeymapImpl newKeymap = ((KeymapImpl)keymap).deriveKeymap();
602     newKeymap.setName(newKeymapName);
603     newKeymap.setCanModify(true);
604
605     int indexOf = myEditor.getModel().getElementIndex(keymap);
606     if (indexOf >= 0) {
607       myEditor.getModel().add(indexOf + 1, newKeymap);
608     }
609     else {
610       myEditor.getModel().add(newKeymap);
611     }
612
613     myEditor.getModel().setSelectedItem(newKeymap);
614     currentKeymapChanged();
615     return newKeymap;
616   }
617
618   private void copyKeymap() {
619     Keymap keymap = myEditor.getModel().getSelected();
620     if (keymap == null) {
621       return;
622     }
623
624     KeymapImpl newKeymap = ((KeymapImpl)keymap).deriveKeymap();
625
626     String newKeymapName = KeyMapBundle.message("new.keymap.name", keymap.getPresentableName());
627     if (!tryNewKeymapName(newKeymapName)) {
628       for (int i = 0; ; i++) {
629         newKeymapName = KeyMapBundle.message("new.indexed.keymap.name", keymap.getPresentableName(), i);
630         if (tryNewKeymapName(newKeymapName)) {
631           break;
632         }
633       }
634     }
635     newKeymap.setName(newKeymapName);
636     newKeymap.setCanModify(true);
637     myEditor.getModel().add(newKeymap);
638     myEditor.getModel().setSelectedItem(newKeymap);
639     myEditor.getComboBox().getEditor().selectAll();
640     currentKeymapChanged();
641   }
642
643   private boolean tryNewKeymapName(String name) {
644     for (int i = 0; i < myEditor.getModel().getSize(); i++) {
645       if (name.equals(myEditor.getModel().getElementAt(i).getPresentableName())) {
646         return false;
647       }
648     }
649
650     return true;
651   }
652
653   private void deleteKeymap() {
654     Keymap keymap = myEditor.getModel().getSelected();
655     if (keymap == null || Messages.showYesNoDialog(this, KeyMapBundle.message("delete.keymap.dialog.message"),
656                                                    KeyMapBundle.message("delete.keymap.dialog.title"), Messages.getWarningIcon()) != Messages.YES) {
657       return;
658     }
659
660     myEditor.getModel().remove(keymap);
661     currentKeymapChanged();
662   }
663
664   private void resetKeymap() {
665     Keymap keymap = myEditor.getModel().getSelected();
666     if (keymap == null) {
667       return;
668     }
669     ((KeymapImpl)keymap).clearOwnActionsIds();
670     currentKeymapChanged();
671   }
672
673   @Override
674   @NotNull
675   public String getId() {
676     return "preferences.keymap";
677   }
678
679   @Override
680   public void reset() {
681     if (myNonEnglishKeyboardSupportOption != null) {
682       KeyboardSettingsExternalizable.getInstance().setNonEnglishKeyboardSupportEnabled(false);
683       myNonEnglishKeyboardSupportOption.setSelected(KeyboardSettingsExternalizable.getInstance().isNonEnglishKeyboardSupportEnabled());
684     }
685
686     Keymap selectedKeymap = null;
687     List<Keymap> list = getManagerKeymaps();
688     for (Keymap keymap : list) {
689       if (selectedKeymap == null && keymap == KeymapManagerEx.getInstanceEx().getActiveKeymap()) {
690         selectedKeymap = keymap;
691       }
692     }
693     myEditor.reset(list);
694
695     if (myEditor.getModel().isEmpty()) {
696       KeymapImpl keymap = new KeymapImpl();
697       keymap.setName(KeyMapBundle.message("keymap.no.name"));
698       myEditor.getModel().add(keymap);
699       selectedKeymap = keymap;
700     }
701
702     myEditor.getModel().setSelectedItem(selectedKeymap);
703
704     currentKeymapChanged();
705   }
706
707   @Override
708   public void apply() throws ConfigurationException {
709     myEditor.ensureNonEmptyNames(KeyMapBundle.message("configuration.all.keymaps.should.have.non.empty.names.error.message"));
710
711     ensureUniqueKeymapNames();
712     KeymapManagerImpl keymapManager = (KeymapManagerImpl)KeymapManager.getInstance();
713     // we must specify the same filter, which was used to get original items
714     keymapManager.setKeymaps(myEditor.apply(), myEditor.getModel().getSelected(), KEYMAP_FILTER);
715     ActionToolbarImpl.updateAllToolbarsImmediately();
716   }
717
718   private void ensureUniqueKeymapNames() throws ConfigurationException {
719     Set<String> keymapNames = new THashSet<String>();
720     for (Keymap keymap : myEditor.getModel().getItems()) {
721       if (!keymapNames.add(keymap.getName())) {
722         throw new ConfigurationException(KeyMapBundle.message("configuration.all.keymaps.should.have.unique.names.error.message"));
723       }
724     }
725   }
726
727   @Override
728   public boolean isModified() {
729     return !Comparing.equal(myEditor.getModel().getSelected(), KeymapManager.getInstance().getActiveKeymap()) || myEditor.isModified();
730   }
731
732   @NotNull
733   private static List<Keymap> getManagerKeymaps() {
734     return ((KeymapManagerImpl)KeymapManagerEx.getInstanceEx()).getKeymaps(KEYMAP_FILTER);
735   }
736
737   public void selectAction(String actionId) {
738     myActionsTree.selectAction(actionId);
739   }
740
741   @Override
742   @Nls
743   public String getDisplayName() {
744     return KeyMapBundle.message("keymap.display.name");
745   }
746
747   @Override
748   public String getHelpTopic() {
749     return "preferences.keymap";
750   }
751
752   @Override
753   public JComponent createComponent() {
754     ApplicationManager.getApplication().getMessageBus().connect(this).subscribe(CHANGE_TOPIC, this);
755     return this;
756   }
757
758   @Override
759   public void disposeUIResources() {
760     myFilteringPanel.hidePopup();
761     if (myFilterComponent != null) {
762       myFilterComponent.dispose();
763     }
764     Disposer.dispose(this);
765   }
766
767   @Override
768   public void dispose() {
769   }
770
771   @Nullable
772   public Shortcut[] getCurrentShortcuts(@NotNull String actionId) {
773     Keymap keymap = myEditor.getModel().getSelected();
774     return keymap == null ? null : keymap.getShortcuts(actionId);
775   }
776
777   private void editSelection(InputEvent e) {
778     String actionId = myActionsTree.getSelectedActionId();
779     if (actionId == null) {
780       return;
781     }
782
783     DefaultActionGroup group = createEditActionGroup(actionId);
784     if (e instanceof MouseEvent && ((MouseEvent)e).isPopupTrigger()) {
785       ActionManager.getInstance()
786         .createActionPopupMenu(ActionPlaces.UNKNOWN, group)
787         .getComponent()
788         .show(e.getComponent(), ((MouseEvent)e).getX(), ((MouseEvent)e).getY());
789     }
790     else {
791       DataContext dataContext = DataManager.getInstance().getDataContext(this);
792       ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup("Edit Shortcuts",
793                                                                             group,
794                                                                             dataContext,
795                                                                             JBPopupFactory.ActionSelectionAid.SPEEDSEARCH,
796                                                                             true);
797
798       if (e instanceof MouseEvent) {
799         popup.show(new RelativePoint((MouseEvent)e));
800       }
801       else {
802         popup.showInBestPositionFor(dataContext);
803       }
804     }
805   }
806
807   @NotNull
808   private DefaultActionGroup createEditActionGroup(@NotNull final String actionId) {
809     DefaultActionGroup group = new DefaultActionGroup();
810     final ShortcutRestrictions restrictions = ActionShortcutRestrictions.getInstance().getForActionId(actionId);
811     if (restrictions.allowKeyboardShortcut) {
812       group.add(new DumbAwareAction("Add Keyboard Shortcut") {
813         @Override
814         public void actionPerformed(@NotNull AnActionEvent e) {
815           Shortcut firstShortcut = null;
816           Keymap keymap = myEditor.getModel().getSelected();
817           assert keymap != null;
818           for (Shortcut shortcut : keymap.getShortcuts(actionId)) {
819             if (shortcut instanceof KeyboardShortcut) {
820               firstShortcut = shortcut;
821               break;
822             }
823           }
824
825           addKeyboardShortcut(actionId, firstShortcut);
826         }
827       });
828     }
829
830     if (restrictions.allowMouseShortcut) {
831       group.add(new DumbAwareAction("Add Mouse Shortcut") {
832         @Override
833         public void actionPerformed(@NotNull AnActionEvent e) {
834           Shortcut firstMouse = null;
835           Keymap keymap = myEditor.getModel().getSelected();
836           assert keymap != null;
837           for (Shortcut shortcut : keymap.getShortcuts(actionId)) {
838             if (shortcut instanceof MouseShortcut) {
839               firstMouse = shortcut;
840               break;
841             }
842           }
843           addMouseShortcut(firstMouse, restrictions);
844         }
845       });
846     }
847
848     if (Registry.is("actionSystem.enableAbbreviations") && restrictions.allowAbbreviation) {
849       group.add(new DumbAwareAction("Add Abbreviation") {
850         @Override
851         public void actionPerformed(@NotNull AnActionEvent e) {
852           String abbr = Messages.showInputDialog("Enter new abbreviation:", "Abbreviation", null);
853           if (abbr != null) {
854             AbbreviationManager.getInstance().register(abbr, myActionsTree.getSelectedActionId());
855             repaintLists();
856           }
857         }
858
859         @Override
860         public void update(@NotNull AnActionEvent e) {
861           e.getPresentation().setEnabledAndVisible(myActionsTree.getSelectedActionId() != null);
862         }
863       });
864     }
865
866     group.addSeparator();
867
868     Keymap keymap = myEditor.getModel().getSelected();
869     assert keymap != null;
870     for (final Shortcut shortcut : keymap.getShortcuts(actionId)) {
871       group.add(new DumbAwareAction("Remove " + KeymapUtil.getShortcutText(shortcut)) {
872         @Override
873         public void actionPerformed(@NotNull AnActionEvent e) {
874           Keymap keymap = createKeymapCopyIfNeeded();
875           keymap.removeShortcut(actionId, shortcut);
876           if (StringUtil.startsWithChar(actionId, '$')) {
877             keymap.removeShortcut(KeyMapBundle.message("editor.shortcut", actionId.substring(1)), shortcut);
878           }
879
880           repaintLists();
881           currentKeymapChanged();
882         }
883       });
884     }
885
886     if (Registry.is("actionSystem.enableAbbreviations")) {
887       for (final String abbreviation : AbbreviationManager.getInstance().getAbbreviations(actionId)) {
888         group.addAction(new DumbAwareAction("Remove Abbreviation '" + abbreviation + "'") {
889           @Override
890           public void actionPerformed(@NotNull AnActionEvent e) {
891             AbbreviationManager.getInstance().remove(abbreviation, actionId);
892             repaintLists();
893           }
894         });
895       }
896     }
897     group.add(new Separator());
898     group.add(new DumbAwareAction("Reset Shortcuts") {
899       @Override
900       public void actionPerformed(@NotNull AnActionEvent e) {
901         ((KeymapImpl)createKeymapCopyIfNeeded()).clearOwnActionsId(actionId);
902         currentKeymapChanged();
903         repaintLists();
904       }
905
906       @Override
907       public void update(@NotNull AnActionEvent e) {
908         e.getPresentation().setVisible(((KeymapImpl)myEditor.getModel().getSelected()).hasOwnActionId(actionId));
909       }
910     });
911
912     return group;
913   }
914 }