IDEA-80690 Live Template with matched prefix should be at top of popup window
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / template / impl / TemplateListPanel.java
1 /*
2  * Copyright 2000-2009 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
17 package com.intellij.codeInsight.template.impl;
18
19 import com.google.common.collect.Sets;
20 import com.intellij.application.options.ExportSchemeAction;
21 import com.intellij.application.options.SchemesToImportPopup;
22 import com.intellij.codeInsight.CodeInsightBundle;
23 import com.intellij.codeInsight.template.TemplateContextType;
24 import com.intellij.ide.dnd.*;
25 import com.intellij.ide.dnd.aware.DnDAwareTree;
26 import com.intellij.openapi.Disposable;
27 import com.intellij.openapi.actionSystem.*;
28 import com.intellij.openapi.diagnostic.Logger;
29 import com.intellij.openapi.options.ConfigurationException;
30 import com.intellij.openapi.options.SchemesManager;
31 import com.intellij.openapi.project.DumbAwareAction;
32 import com.intellij.openapi.ui.InputValidator;
33 import com.intellij.openapi.ui.Messages;
34 import com.intellij.openapi.ui.Splitter;
35 import com.intellij.openapi.util.Comparing;
36 import com.intellij.openapi.util.text.StringUtil;
37 import com.intellij.ui.*;
38 import com.intellij.util.Alarm;
39 import com.intellij.util.NullableFunction;
40 import com.intellij.util.ObjectUtils;
41 import com.intellij.util.PlatformIcons;
42 import com.intellij.util.containers.Convertor;
43 import com.intellij.util.ui.tree.TreeUtil;
44 import com.intellij.util.ui.update.UiNotifyConnector;
45 import org.jetbrains.annotations.Nullable;
46
47 import javax.swing.*;
48 import javax.swing.border.EmptyBorder;
49 import javax.swing.event.TreeSelectionEvent;
50 import javax.swing.event.TreeSelectionListener;
51 import javax.swing.tree.DefaultMutableTreeNode;
52 import javax.swing.tree.DefaultTreeModel;
53 import javax.swing.tree.TreeNode;
54 import javax.swing.tree.TreePath;
55 import java.awt.*;
56 import java.awt.event.*;
57 import java.util.*;
58 import java.util.List;
59
60 public class TemplateListPanel extends JPanel implements Disposable {
61
62   private static final String NO_SELECTION = "NoSelection";
63   private static final String TEMPLATE_SETTINGS = "TemplateSettings";
64   private static final TemplateImpl MOCK_TEMPLATE = new TemplateImpl("mockTemplate-xxx", "mockTemplateGroup-yyy");
65   public static final String ABBREVIATION = "<abbreviation>";
66   public static final Comparator<TemplateImpl> TEMPLATE_COMPARATOR = new Comparator<TemplateImpl>() {
67     public int compare(final TemplateImpl o1, final TemplateImpl o2) {
68       return o1.getKey().compareToIgnoreCase(o2.getKey());
69     }
70   };
71
72   static {
73     MOCK_TEMPLATE.setString("");
74   }
75
76   private CheckboxTree myTree;
77   private final List<TemplateGroup> myTemplateGroups = new ArrayList<TemplateGroup>();
78   private JComboBox myExpandByCombo;
79   private static final String SPACE = CodeInsightBundle.message("template.shortcut.space");
80   private static final String TAB = CodeInsightBundle.message("template.shortcut.tab");
81   private static final String ENTER = CodeInsightBundle.message("template.shortcut.enter");
82
83   private CheckedTreeNode myTreeRoot = new CheckedTreeNode(null);
84
85   private final Alarm myAlarm = new Alarm();
86   private boolean myUpdateNeeded = false;
87
88   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.template.impl.TemplateListPanel");
89
90   private final Map<Integer, Map<TemplateOptionalProcessor, Boolean>> myTemplateOptions = new LinkedHashMap<Integer, Map<TemplateOptionalProcessor, Boolean>>();
91   private final Map<Integer, Map<TemplateContextType, Boolean>> myTemplateContext = new LinkedHashMap<Integer, Map<TemplateContextType, Boolean>>();
92   private JPanel myDetailsPanel = new JPanel(new CardLayout());
93   private LiveTemplateSettingsEditor myCurrentTemplateEditor;
94
95   public TemplateListPanel() {
96     super(new BorderLayout());
97
98     myDetailsPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0));
99     JLabel label = new JLabel("No live template is selected");
100     label.setHorizontalAlignment(SwingConstants.CENTER);
101     myDetailsPanel.add(label, NO_SELECTION);
102     createTemplateEditor(MOCK_TEMPLATE, "Tab", MOCK_TEMPLATE.createOptions(), MOCK_TEMPLATE.createContext());
103
104     add(createExpandByPanel(), BorderLayout.NORTH);
105
106     Splitter splitter = new Splitter(true, 0.9f);
107     splitter.setFirstComponent(createTable());
108     splitter.setSecondComponent(myDetailsPanel);
109     add(splitter, BorderLayout.CENTER);
110   }
111
112   public void dispose() {
113     myCurrentTemplateEditor.dispose();
114     myAlarm.cancelAllRequests();
115   }
116
117   public void reset() {
118     myTemplateOptions.clear();
119     myTemplateContext.clear();
120
121     TemplateSettings templateSettings = TemplateSettings.getInstance();
122     List<TemplateGroup> groups = new ArrayList<TemplateGroup>(templateSettings.getTemplateGroups());
123
124     Collections.sort(groups, new Comparator<TemplateGroup>() {
125       public int compare(final TemplateGroup o1, final TemplateGroup o2) {
126         return o1.getName().compareToIgnoreCase(o2.getName());
127       }
128     });
129
130     initTemplates(groups, templateSettings.getLastSelectedTemplateGroup(), templateSettings.getLastSelectedTemplateKey());
131
132
133
134     if (templateSettings.getDefaultShortcutChar() == TemplateSettings.TAB_CHAR) {
135       myExpandByCombo.setSelectedItem(TAB);
136     }
137     else if (templateSettings.getDefaultShortcutChar() == TemplateSettings.ENTER_CHAR) {
138       myExpandByCombo.setSelectedItem(ENTER);
139     }
140     else {
141       myExpandByCombo.setSelectedItem(SPACE);
142     }
143
144     UiNotifyConnector.doWhenFirstShown(this, new Runnable() {
145       public void run() {
146         updateTemplateDetails(false);
147       }
148     });
149
150     myUpdateNeeded = true;
151   }
152
153   public void apply() throws ConfigurationException {
154     List<TemplateGroup> templateGroups = getTemplateGroups();
155     for (TemplateGroup templateGroup : templateGroups) {
156       Set<String> names = Sets.newHashSet();
157
158       for (TemplateImpl template : templateGroup.getElements()) {
159         if (StringUtil.isEmptyOrSpaces(template.getKey())) {
160           throw new ConfigurationException("A live template with an empty key has been found in " + templateGroup.getName() + " group, such live templates cannot be invoked");
161         }
162
163         if (StringUtil.isEmptyOrSpaces(template.getString())) {
164           throw new ConfigurationException("A live template with an empty text has been found in " + templateGroup.getName() + " group, such live templates cannot be invoked");
165         }
166
167         if (!names.add(template.getKey())) {
168           throw new ConfigurationException("Duplicate " + template.getKey() + " live templates in " + templateGroup.getName() + " group");
169         }
170       }
171     }
172
173
174     for (TemplateGroup templateGroup : templateGroups) {
175       for (TemplateImpl template : templateGroup.getElements()) {
176         template.applyOptions(getTemplateOptions(template));
177         template.applyContext(getTemplateContext(template));
178       }
179     }
180     TemplateSettings templateSettings = TemplateSettings.getInstance();
181     templateSettings.setTemplates(templateGroups);
182     templateSettings.setDefaultShortcutChar(getDefaultShortcutChar());
183
184     reset();
185   }
186
187   public boolean isModified() {
188     TemplateSettings templateSettings = TemplateSettings.getInstance();
189     if (templateSettings.getDefaultShortcutChar() != getDefaultShortcutChar()) {
190       return true;
191     }
192
193     List<TemplateGroup> originalGroups = templateSettings.getTemplateGroups();
194     List<TemplateGroup> newGroups = getTemplateGroups();
195
196     return !checkAreEqual(collectTemplates(originalGroups), collectTemplates(newGroups));
197   }
198
199   public void editTemplate(TemplateImpl template) {
200     selectTemplate(template.getGroupName(), template.getKey());
201     updateTemplateDetails(true);
202   }
203
204   @Nullable
205   public JComponent getPreferredFocusedComponent() {
206     if (getTemplate(getSingleSelectedIndex()) != null) {
207       return myCurrentTemplateEditor.getKeyField();
208     }
209     return null;
210   }
211
212   private static List<TemplateImpl> collectTemplates(final List<TemplateGroup> groups) {
213     ArrayList<TemplateImpl> result = new ArrayList<TemplateImpl>();
214     for (TemplateGroup group : groups) {
215       result.addAll(group.getElements());
216     }
217     Collections.sort(result, new Comparator<TemplateImpl>(){
218       public int compare(final TemplateImpl o1, final TemplateImpl o2) {
219         final int groupsEqual = o1.getGroupName().compareToIgnoreCase(o2.getGroupName());
220         if (groupsEqual != 0) {
221           return groupsEqual;
222         }
223         return o1.getKey().compareToIgnoreCase(o2.getKey());
224       }
225     });
226     return result;
227   }
228
229   private boolean checkAreEqual(final List<TemplateImpl> originalGroup, final List<TemplateImpl> newGroup) {
230     if (originalGroup.size() != newGroup.size()) return false;
231
232     for (int i = 0; i < newGroup.size(); i++) {
233       TemplateImpl newTemplate = newGroup.get(i);
234       newTemplate.parseSegments();
235       TemplateImpl originalTemplate = originalGroup.get(i);
236       originalTemplate.parseSegments();
237       if (!originalTemplate.equals(newTemplate)) {
238         return false;
239       }
240
241       if (originalTemplate.isDeactivated() != newTemplate.isDeactivated()) {
242         return false;
243       }
244
245       if (!areOptionsEqual(newTemplate, originalTemplate)) {
246         return false;
247       }
248
249       if (!areContextsEqual(newTemplate, originalTemplate)) {
250         return false;
251       }
252     }
253
254     return true;
255   }
256
257   private boolean areContextsEqual(final TemplateImpl newTemplate, final TemplateImpl originalTemplate) {
258     Map<TemplateContextType, Boolean> templateContext = getTemplateContext(newTemplate);
259     for (TemplateContextType processor : templateContext.keySet()) {
260       if (originalTemplate.getTemplateContext().isEnabled(processor) != templateContext.get(processor).booleanValue())
261         return false;
262     }
263     return true;
264   }
265
266   private boolean areOptionsEqual(final TemplateImpl newTemplate, final TemplateImpl originalTemplate) {
267     Map<TemplateOptionalProcessor, Boolean> templateOptions = getTemplateOptions(newTemplate);
268     for (TemplateOptionalProcessor processor : templateOptions.keySet()) {
269       if (processor.isEnabled(originalTemplate) != templateOptions.get(processor).booleanValue()) return false;
270     }
271     return true;
272   }
273
274   private Map<TemplateContextType, Boolean> getTemplateContext(final TemplateImpl newTemplate) {
275     return myTemplateContext.get(getKey(newTemplate));
276   }
277
278   private Map<TemplateOptionalProcessor, Boolean> getTemplateOptions(final TemplateImpl newTemplate) {
279     return myTemplateOptions.get(getKey(newTemplate));
280   }
281
282   private char getDefaultShortcutChar() {
283     Object selectedItem = myExpandByCombo.getSelectedItem();
284     if (TAB.equals(selectedItem)) {
285       return TemplateSettings.TAB_CHAR;
286     }
287     else if (ENTER.equals(selectedItem)) {
288       return TemplateSettings.ENTER_CHAR;
289     }
290     else {
291       return TemplateSettings.SPACE_CHAR;
292     }
293   }
294
295   private List<TemplateGroup> getTemplateGroups() {
296     return myTemplateGroups;
297   }
298
299   private void createTemplateEditor(final TemplateImpl template,
300                                     String shortcut,
301                                     Map<TemplateOptionalProcessor, Boolean> options,
302                                     Map<TemplateContextType, Boolean> context) {
303     myCurrentTemplateEditor = new LiveTemplateSettingsEditor(template, shortcut, options, context, new Runnable() {
304       @Override
305       public void run() {
306         DefaultMutableTreeNode node = getNode(getSingleSelectedIndex());
307         if (node != null) {
308           ((DefaultTreeModel)myTree.getModel()).nodeChanged(node);
309           TemplateSettings.getInstance().setLastSelectedTemplate(template.getGroupName(), template.getKey());
310         }
311       }
312     }, TemplateSettings.getInstance().getTemplate(template.getKey(), template.getGroupName()) != null);
313     for (Component component : myDetailsPanel.getComponents()) {
314       if (component instanceof LiveTemplateSettingsEditor) {
315         myDetailsPanel.remove(component);
316       }
317     }
318
319     myDetailsPanel.add(myCurrentTemplateEditor, TEMPLATE_SETTINGS);
320   }
321
322   private Iterable<? extends TemplateImpl> collectAllTemplates() {
323     ArrayList<TemplateImpl> result = new ArrayList<TemplateImpl>();
324     for (TemplateGroup templateGroup : myTemplateGroups) {
325       result.addAll(templateGroup.getElements());
326     }
327     return result;
328   }
329
330   private void exportCurrentGroup() {
331     int selected = getSingleSelectedIndex();
332     if (selected < 0) return;
333
334     ExportSchemeAction.doExport(getGroup(selected), getSchemesManager());
335
336   }
337
338   private static SchemesManager<TemplateGroup, TemplateGroup> getSchemesManager() {
339     return (TemplateSettings.getInstance()).getSchemesManager();
340   }
341
342   private JPanel createExpandByPanel() {
343     JPanel panel = new JPanel(new GridBagLayout());
344     GridBagConstraints gbConstraints = new GridBagConstraints();
345     gbConstraints.weighty = 0;
346     gbConstraints.weightx = 0;
347     gbConstraints.gridy = 0;
348     panel.add(new JLabel(CodeInsightBundle.message("templates.dialog.shortcut.chooser.label")), gbConstraints);
349
350     gbConstraints.gridx = 1;
351     gbConstraints.insets = new Insets(0, 4, 0, 0);
352     myExpandByCombo = new JComboBox();
353     myExpandByCombo.addItem(SPACE);
354     myExpandByCombo.addItem(TAB);
355     myExpandByCombo.addItem(ENTER);
356     panel.add(myExpandByCombo, gbConstraints);
357
358     gbConstraints.gridx = 2;
359     gbConstraints.weightx = 1;
360     panel.add(new JPanel(), gbConstraints);
361     panel.setBorder(new EmptyBorder(0, 0, 10, 0));
362     return panel;
363   }
364
365   @Nullable
366   private TemplateImpl getTemplate(int row) {
367     JTree tree = myTree;
368     TreePath path = tree.getPathForRow(row);
369     if (path != null) {
370       DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
371       if (node.getUserObject() instanceof TemplateImpl) {
372         return (TemplateImpl)node.getUserObject();
373       }
374     }
375
376     return null;
377   }
378
379   @Nullable
380   private TemplateGroup getGroup(int row) {
381     JTree tree = myTree;
382     TreePath path = tree.getPathForRow(row);
383     if (path != null) {
384       DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
385       if (node.getUserObject() instanceof TemplateGroup) {
386         return (TemplateGroup)node.getUserObject();
387       }
388     }
389
390     return null;
391   }
392
393   private void moveTemplates(Map<TemplateImpl, DefaultMutableTreeNode> map, String newGroupName) {
394     List<TreePath> toSelect = new ArrayList<TreePath>();
395     for (TemplateImpl template : map.keySet()) {
396       DefaultMutableTreeNode oldTemplateNode = map.get(template);
397
398       TemplateGroup oldGroup = getTemplateGroup(template.getGroupName());
399       if (oldGroup != null) {
400         oldGroup.removeElement(template);
401       }
402
403       template.setGroupName(newGroupName);
404
405       DefaultMutableTreeNode parent = (DefaultMutableTreeNode)oldTemplateNode.getParent();
406       removeNodeFromParent(oldTemplateNode);
407       if (parent.getChildCount() == 0) removeNodeFromParent(parent);
408
409       toSelect.add(new TreePath(registerTemplate(template).getPath()));
410     }
411
412     myTree.getSelectionModel().clearSelection();
413     for (TreePath path : toSelect) {
414       myTree.expandPath(path.getParentPath());
415       myTree.addSelectionPath(path);
416       myTree.scrollRowToVisible(myTree.getRowForPath(path));
417     }
418   }
419
420   @Nullable
421   private DefaultMutableTreeNode getNode(final int row) {
422     JTree tree = myTree;
423     TreePath path = tree.getPathForRow(row);
424     if (path != null) {
425       return (DefaultMutableTreeNode)path.getLastPathComponent();
426     }
427
428     return null;
429
430   }
431
432   @Nullable
433   private TemplateGroup getTemplateGroup(final String groupName) {
434     for (TemplateGroup group : myTemplateGroups) {
435       if (group.getName().equals(groupName)) return group;
436     }
437
438     return null;
439   }
440
441   private void addRow() {
442     String defaultGroup = TemplateSettings.USER_GROUP_NAME;
443     final DefaultMutableTreeNode node = getNode(getSingleSelectedIndex());
444     if (node != null) {
445       if (node.getUserObject() instanceof TemplateImpl) {
446         defaultGroup = ((TemplateImpl) node.getUserObject()).getGroupName();
447       }
448       else if (node.getUserObject() instanceof TemplateGroup) {
449         defaultGroup = ((TemplateGroup) node.getUserObject()).getName();
450       }
451     }
452
453     addTemplate(new TemplateImpl(ABBREVIATION, "", defaultGroup));
454   }
455
456   public void addTemplate(TemplateImpl template) {
457     myTemplateOptions.put(getKey(template), template.createOptions());
458     myTemplateContext.put(getKey(template), template.createContext());
459
460     registerTemplate(template);
461     updateTemplateDetails(true);
462   }
463
464   private static int getKey(final TemplateImpl template) {
465     return System.identityHashCode(template);
466   }
467
468   private void copyRow() {
469     int selected = getSingleSelectedIndex();
470     if (selected < 0) return;
471
472     TemplateImpl orTemplate = getTemplate(selected);
473     LOG.assertTrue(orTemplate != null);
474     TemplateImpl template = orTemplate.copy();
475     template.setKey(ABBREVIATION);
476     myTemplateOptions.put(getKey(template), new HashMap<TemplateOptionalProcessor, Boolean>(getTemplateOptions(orTemplate)));
477     myTemplateContext.put(getKey(template), new HashMap<TemplateContextType, Boolean>(getTemplateContext(orTemplate)));
478     registerTemplate(template);
479
480     updateTemplateDetails(true);
481   }
482
483   private int getSingleSelectedIndex() {
484     int[] rows = myTree.getSelectionRows();
485     return rows != null && rows.length == 1 ? rows[0] : -1;
486   }
487
488   private void removeRows() {
489     TreeNode toSelect = null;
490
491     TreePath[] paths = myTree.getSelectionPaths();
492     if (paths == null) return;
493
494     for (TreePath path : paths) {
495       DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
496       Object o = node.getUserObject();
497       if (o instanceof TemplateGroup) {
498         //noinspection SuspiciousMethodCalls
499         myTemplateGroups.remove(o);
500         removeNodeFromParent(node);
501       } else if (o instanceof TemplateImpl) {
502         TemplateImpl template = (TemplateImpl)o;
503         TemplateGroup templateGroup = getTemplateGroup(template.getGroupName());
504         if (templateGroup != null) {
505           templateGroup.removeElement(template);
506           DefaultMutableTreeNode parent = (DefaultMutableTreeNode)node.getParent();
507
508           if (templateGroup.getElements().isEmpty()) {
509             myTemplateGroups.remove(templateGroup);
510             removeNodeFromParent(parent);
511           } else {
512             toSelect = parent.getChildAfter(node);
513             removeNodeFromParent(node);
514           }
515         }
516       }
517     }
518
519     if (toSelect instanceof DefaultMutableTreeNode) {
520       setSelectedNode((DefaultMutableTreeNode)toSelect);
521     }
522   }
523
524   private JPanel createTable() {
525     myTreeRoot = new CheckedTreeNode(null);
526
527     myTree = new CheckboxTree(new CheckboxTree.CheckboxTreeCellRenderer(){
528       public void customizeRenderer(final JTree tree,
529                                         Object value,
530                                         final boolean selected,
531                                         final boolean expanded,
532                                         final boolean leaf,
533                                         final int row,
534                                         final boolean hasFocus) {
535         if (!(value instanceof DefaultMutableTreeNode)) return;
536         value = ((DefaultMutableTreeNode)value).getUserObject();
537
538         if (value instanceof TemplateImpl) {
539           getTextRenderer().append (((TemplateImpl)value).getKey(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
540           String description = ((TemplateImpl)value).getDescription();
541           if (description != null && description.length() > 0) {
542             getTextRenderer().append (" (" + description + ")", SimpleTextAttributes.GRAY_ATTRIBUTES);
543           }
544         }
545         else if (value instanceof TemplateGroup) {
546           getTextRenderer().append (((TemplateGroup)value).getName(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES);
547         }
548
549       }
550     }, myTreeRoot) {
551       @Override
552       protected void onNodeStateChanged(final CheckedTreeNode node) {
553         Object obj = node.getUserObject();
554         if (obj instanceof TemplateImpl) {
555           ((TemplateImpl)obj).setDeactivated(!node.isChecked());
556         }
557       }
558
559       @Override
560       protected void installSpeedSearch() {
561         new TreeSpeedSearch(this, new Convertor<TreePath, String>() {
562           @Override
563           public String convert(TreePath o) {
564             Object object = ((DefaultMutableTreeNode)o.getLastPathComponent()).getUserObject();
565             if (object instanceof TemplateGroup) {
566               return ((TemplateGroup)object).getName();
567             }
568             if (object instanceof TemplateImpl) {
569               return ((TemplateImpl)object).getKey();
570             }
571             return "";
572           }
573         }, true);
574
575       }
576     };
577     myTree.setRootVisible(false);
578     myTree.setShowsRootHandles(true);
579
580     myTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener(){
581       public void valueChanged(final TreeSelectionEvent e) {
582         TemplateSettings templateSettings = TemplateSettings.getInstance();
583         TemplateImpl template = getTemplate(getSingleSelectedIndex());
584         if (template != null) {
585           templateSettings.setLastSelectedTemplate(template.getGroupName(), template.getKey());
586         } else {
587           templateSettings.setLastSelectedTemplate(null, null);
588           ((CardLayout) myDetailsPanel.getLayout()).show(myDetailsPanel, NO_SELECTION);
589         }
590         if (myUpdateNeeded) {
591           myAlarm.cancelAllRequests();
592           myAlarm.addRequest(new Runnable() {
593             public void run() {
594               updateTemplateDetails(false);
595             }
596           }, 100);
597         }
598       }
599     });
600
601     myTree.registerKeyboardAction(new ActionListener() {
602       public void actionPerformed(ActionEvent event) {
603         myCurrentTemplateEditor.focusKey();
604       }
605     }, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), JComponent.WHEN_FOCUSED);
606
607     myTree.addMouseListener(new MouseAdapter() {
608       @Override
609       public void mouseClicked(MouseEvent e) {
610         if (e.getClickCount() == 2) {
611           renameGroup();
612         }
613       }
614     });
615
616     installPopup();
617
618
619     DnDSupport.createBuilder(myTree)
620       .setBeanProvider(new NullableFunction<DnDActionInfo, DnDDragStartBean>() {
621         @Override
622         public DnDDragStartBean fun(DnDActionInfo dnDActionInfo) {
623           Point point = dnDActionInfo.getPoint();
624           if (myTree.getPathForLocation(point.x, point.y) == null) return null;
625
626           Map<TemplateImpl, DefaultMutableTreeNode> templates = getSelectedTemplates();
627
628           return !templates.isEmpty() ? new DnDDragStartBean(templates) : null;
629         }
630       }).
631       setDisposableParent(this)
632       .setTargetChecker(new DnDTargetChecker() {
633         @Override
634         public boolean update(DnDEvent event) {
635           @SuppressWarnings("unchecked") Set<String> oldGroupNames = getAllGroups((Map<TemplateImpl, DefaultMutableTreeNode>)event.getAttachedObject());
636           TemplateGroup group = getDropGroup(event);
637           boolean differentGroup = group != null && !oldGroupNames.contains(group.getName());
638           boolean possible = differentGroup && !getSchemesManager().isShared(group);
639           event.setDropPossible(possible, differentGroup && !possible ? "Cannot modify a shared group" : "");
640           return true;
641         }
642       })
643       .setDropHandler(new DnDDropHandler() {
644         @Override
645         public void drop(DnDEvent event) {
646           //noinspection unchecked
647           moveTemplates((Map<TemplateImpl, DefaultMutableTreeNode>)event.getAttachedObject(),
648                         ObjectUtils.assertNotNull(getDropGroup(event)).getName());
649         }
650       })
651       .setImageProvider(new NullableFunction<DnDActionInfo, DnDImage>() {
652         @Override
653         public DnDImage fun(DnDActionInfo dnDActionInfo) {
654           Point point = dnDActionInfo.getPoint();
655           TreePath path = myTree.getPathForLocation(point.x, point.y);
656           return path == null ? null : new DnDImage(DnDAwareTree.getDragImage(myTree, path, point).first);
657         }
658       })
659       .install();
660
661     if (myTemplateGroups.size() > 0) {
662       myTree.setSelectionInterval(0, 0);
663     }
664
665     return initToolbar().createPanel();
666
667   }
668
669   private ToolbarDecorator initToolbar() {
670     ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myTree)
671       .setAddAction(new AnActionButtonRunnable() {
672         @Override
673         public void run(AnActionButton button) {
674           addRow();
675         }
676       })
677       .setRemoveAction(new AnActionButtonRunnable() {
678         @Override
679         public void run(AnActionButton anActionButton) {
680           removeRows();
681         }
682       })
683       .disableDownAction()
684       .disableUpAction()
685       .addExtraAction(new AnActionButton("Copy", PlatformIcons.DUPLICATE_ICON) {
686         @Override
687         public void actionPerformed(AnActionEvent e) {
688           copyRow();
689         }
690
691         @Override
692         public void updateButton(AnActionEvent e) {
693           e.getPresentation().setEnabled(getTemplate(getSingleSelectedIndex()) != null);
694         }
695       });
696     if (getSchemesManager().isExportAvailable()) {
697       decorator.addExtraAction(new AnActionButton("Share...", PlatformIcons.EXPORT_ICON) {
698         @Override
699         public void actionPerformed(AnActionEvent e) {
700           exportCurrentGroup();
701         }
702
703         @Override
704         public void updateButton(AnActionEvent e) {
705           TemplateGroup group = getGroup(getSingleSelectedIndex());
706           e.getPresentation().setEnabled(group != null && !getSchemesManager().isShared(group));
707         }
708       });
709     }
710     if (getSchemesManager().isImportAvailable()) {
711       decorator.addExtraAction(new AnActionButton("Import Shared...", PlatformIcons.IMPORT_ICON) {
712         @Override
713         public void actionPerformed(AnActionEvent e) {
714           new SchemesToImportPopup<TemplateGroup, TemplateGroup>(TemplateListPanel.this){
715             protected void onSchemeSelected(final TemplateGroup scheme) {
716               for (TemplateImpl newTemplate : scheme.getElements()) {
717                 for (TemplateImpl existingTemplate : collectAllTemplates()) {
718                   if (existingTemplate.getKey().equals(newTemplate.getKey())) {
719                     Messages.showMessageDialog(
720                       TemplateListPanel.this,
721                       CodeInsightBundle
722                         .message("dialog.edit.template.error.already.exists", existingTemplate.getKey(), existingTemplate.getGroupName()),
723                       CodeInsightBundle.message("dialog.edit.template.error.title"),
724                       Messages.getErrorIcon()
725                     );
726                     return;
727                   }
728                 }
729               }
730               insertNewGroup(scheme);
731               for (TemplateImpl template : scheme.getElements()) {
732                 registerTemplate(template);
733               }
734             }
735           }.show(getSchemesManager(), myTemplateGroups);
736         }
737       });
738     }
739     return decorator.setToolbarPosition(ActionToolbarPosition.RIGHT);
740   }
741
742   @Nullable
743   private TemplateGroup getDropGroup(DnDEvent event) {
744     Point point = event.getPointOn(myTree);
745     return getGroup(myTree.getRowForLocation(point.x, point.y));
746   }
747
748   private void installPopup() {
749     final DumbAwareAction rename = new DumbAwareAction("Rename") {
750
751       @Override
752       public void update(AnActionEvent e) {
753         final int selected = getSingleSelectedIndex();
754         final TemplateGroup templateGroup = getGroup(selected);
755         boolean enabled = templateGroup != null;
756         e.getPresentation().setEnabled(enabled);
757         e.getPresentation().setVisible(enabled);
758         super.update(e);
759       }
760
761       @Override
762       public void actionPerformed(AnActionEvent e) {
763         renameGroup();
764       }
765     };
766     rename.registerCustomShortcutSet(ActionManager.getInstance().getAction(IdeActions.ACTION_RENAME).getShortcutSet(), myTree);
767
768     final DefaultActionGroup move = new DefaultActionGroup("Move", true) {
769       @Override
770       public void update(AnActionEvent e) {
771         final Map<TemplateImpl, DefaultMutableTreeNode> templates = getSelectedTemplates();
772         boolean enabled = !templates.isEmpty();
773         e.getPresentation().setEnabled(enabled);
774         e.getPresentation().setVisible(enabled);
775
776         if (enabled) {
777           Set<String> oldGroups = getAllGroups(templates);
778
779           removeAll();
780           SchemesManager<TemplateGroup, TemplateGroup> schemesManager = TemplateSettings.getInstance().getSchemesManager();
781
782           for (TemplateGroup group : getTemplateGroups()) {
783             final String newGroupName = group.getName();
784             if (!oldGroups.contains(newGroupName) && !schemesManager.isShared(group)) {
785               add(new DumbAwareAction(newGroupName) {
786                 @Override
787                 public void actionPerformed(AnActionEvent e) {
788                   moveTemplates(templates, newGroupName);
789                 }
790               });
791             }
792           }
793           addSeparator();
794           add(new DumbAwareAction("New group...") {
795             @Override
796             public void actionPerformed(AnActionEvent e) {
797               String newName = Messages.showInputDialog(myTree, "Enter the new group name:", "Move to a New Group", null, "", new TemplateGroupInputValidator(null));
798               if (newName != null) {
799                 moveTemplates(templates, newName);
800               }
801             }
802           });
803         }
804       }
805     };
806
807     myTree.addMouseListener(new PopupHandler() {
808       @Override
809       public void invokePopup(Component comp, int x, int y) {
810         final DefaultActionGroup group = new DefaultActionGroup();
811         group.add(rename);
812         group.add(move);
813         ActionManager.getInstance().createActionPopupMenu(ActionPlaces.UNKNOWN, group).getComponent().show(comp, x, y);
814       }
815     });
816   }
817
818   private static Set<String> getAllGroups(Map<TemplateImpl, DefaultMutableTreeNode> templates) {
819     Set<String> oldGroups = new HashSet<String>();
820     for (TemplateImpl template : templates.keySet()) {
821       oldGroups.add(template.getGroupName());
822     }
823     return oldGroups;
824   }
825
826   private Map<TemplateImpl, DefaultMutableTreeNode> getSelectedTemplates() {
827     TreePath[] paths = myTree.getSelectionPaths();
828     if (paths == null) {
829       return Collections.emptyMap();
830     }
831     Map<TemplateImpl, DefaultMutableTreeNode> templates = new LinkedHashMap<TemplateImpl, DefaultMutableTreeNode>();
832     for (TreePath path : paths) {
833       DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
834       Object o = node.getUserObject();
835       if (!(o instanceof TemplateImpl)) {
836         return Collections.emptyMap();
837       }
838       templates.put((TemplateImpl)o, node);
839     }
840     return templates;
841   }
842
843   private void renameGroup() {
844     final int selected = getSingleSelectedIndex();
845     final TemplateGroup templateGroup = getGroup(selected);
846     if (templateGroup == null) return;
847
848     final String oldName = templateGroup.getName();
849     String newName = Messages.showInputDialog(myTree, "Enter the new group name:", "Rename", null, oldName,
850                                               new TemplateGroupInputValidator(oldName));
851
852     if (newName != null && !newName.equals(oldName)) {
853       templateGroup.setName(newName);
854       ((DefaultTreeModel)myTree.getModel()).nodeChanged(getNode(selected));
855     }
856   }
857
858   private void updateTemplateDetails(boolean focusKey) {
859     int selected = getSingleSelectedIndex();
860     CardLayout layout = (CardLayout)myDetailsPanel.getLayout();
861     if (selected < 0 || getTemplate(selected) == null) {
862       layout.show(myDetailsPanel, NO_SELECTION);
863     }
864     else {
865       TemplateImpl newTemplate = getTemplate(selected);
866       if (myCurrentTemplateEditor == null || myCurrentTemplateEditor.getTemplate() != newTemplate) {
867         if (myCurrentTemplateEditor != null) {
868           myCurrentTemplateEditor.dispose();
869         }
870         createTemplateEditor(newTemplate, (String)myExpandByCombo.getSelectedItem(), getTemplateOptions(newTemplate),
871                              getTemplateContext(newTemplate));
872         if (focusKey) {
873           myCurrentTemplateEditor.focusKey();
874         }
875       }
876       layout.show(myDetailsPanel, TEMPLATE_SETTINGS);
877     }
878   }
879
880   private CheckedTreeNode registerTemplate(TemplateImpl template) {
881     TemplateGroup newGroup = getTemplateGroup(template.getGroupName());
882     if (newGroup == null) {
883       newGroup = new TemplateGroup(template.getGroupName());
884       insertNewGroup(newGroup);
885     }
886     if (!newGroup.contains(template)) {
887       newGroup.addElement(template);
888     }
889
890     CheckedTreeNode node = new CheckedTreeNode(template);
891     node.setChecked(!template.isDeactivated());
892     for (DefaultMutableTreeNode child = (DefaultMutableTreeNode)myTreeRoot.getFirstChild();
893          child != null;
894          child = (DefaultMutableTreeNode)myTreeRoot.getChildAfter(child)) {
895       if (((TemplateGroup)child.getUserObject()).getName().equals(template.getGroupName())) {
896         int index = getIndexToInsert (child, template.getKey());
897         child.insert(node, index);
898         ((DefaultTreeModel)myTree.getModel()).nodesWereInserted(child, new int[]{index});
899         setSelectedNode(node);
900       }
901     }
902     return node;
903   }
904
905   private void insertNewGroup(final TemplateGroup newGroup) {
906     myTemplateGroups.add(newGroup);
907
908     int index = getIndexToInsert(myTreeRoot, newGroup.getName());
909     DefaultMutableTreeNode groupNode = new CheckedTreeNode(newGroup);
910     myTreeRoot.insert(groupNode, index);
911     ((DefaultTreeModel)myTree.getModel()).nodesWereInserted(myTreeRoot, new int[]{index});
912   }
913
914   private static int getIndexToInsert(DefaultMutableTreeNode parent, String key) {
915     if (parent.getChildCount() == 0) return 0;
916
917     int res = 0;
918     for (DefaultMutableTreeNode child = (DefaultMutableTreeNode)parent.getFirstChild();
919          child != null;
920          child = (DefaultMutableTreeNode)parent.getChildAfter(child)) {
921       Object o = child.getUserObject();
922       String key1 = o instanceof TemplateImpl ? ((TemplateImpl)o).getKey() : ((TemplateGroup)o).getName();
923       if (key1.compareToIgnoreCase(key) > 0) return res;
924       res++;
925     }
926     return res;
927   }
928
929   private void setSelectedNode(DefaultMutableTreeNode node) {
930     TreePath path = new TreePath(node.getPath());
931     myTree.expandPath(path.getParentPath());
932     int row = myTree.getRowForPath(path);
933     myTree.setSelectionRow(row);
934     myTree.scrollRowToVisible(row);
935   }
936
937   private void removeNodeFromParent(DefaultMutableTreeNode node) {
938     TreeNode parent = node.getParent();
939     int idx = parent.getIndex(node);
940     node.removeFromParent();
941
942     ((DefaultTreeModel)myTree.getModel()).nodesWereRemoved(parent, new int[]{idx}, new TreeNode[]{node});
943   }
944
945   private void initTemplates(List<TemplateGroup> groups, String lastSelectedGroup, String lastSelectedKey) {
946     myTreeRoot.removeAllChildren();
947     myTemplateGroups.clear();
948     for (TemplateGroup group : groups) {
949       myTemplateGroups.add((TemplateGroup)group.copy());
950     }
951
952     for (TemplateGroup group : myTemplateGroups) {
953       CheckedTreeNode groupNode = new CheckedTreeNode(group);
954       addTemplateNodes(group, groupNode);
955       myTreeRoot.add(groupNode);
956     }
957     fireStructureChange();
958
959     selectTemplate(lastSelectedGroup, lastSelectedKey);
960   }
961
962   private void selectTemplate(final String lastSelectedGroup, final String lastSelectedKey) {
963     TreeUtil.traverseDepth(myTreeRoot, new TreeUtil.Traverse() {
964       @Override
965       public boolean accept(Object node) {
966         Object o = ((DefaultMutableTreeNode)node).getUserObject();
967         if (lastSelectedKey == null && o instanceof TemplateGroup && Comparing.equal(lastSelectedGroup, ((TemplateGroup)o).getName()) ||
968             o instanceof TemplateImpl && Comparing.equal(lastSelectedKey, ((TemplateImpl)o).getKey()) && Comparing.equal(lastSelectedGroup, ((TemplateImpl)o).getGroupName())) {
969           setSelectedNode((DefaultMutableTreeNode)node);
970           return false;
971         }
972
973         return true;
974       }
975     });
976   }
977
978   private void fireStructureChange() {
979     ((DefaultTreeModel)myTree.getModel()).nodeStructureChanged(myTreeRoot);
980   }
981
982   private void addTemplateNodes(TemplateGroup group, CheckedTreeNode groupNode) {
983     List<TemplateImpl> templates = new ArrayList<TemplateImpl>(group.getElements());
984     Collections.sort(templates, TEMPLATE_COMPARATOR);
985     for (final TemplateImpl template : templates) {
986       myTemplateOptions.put(getKey(template), template.createOptions());
987       myTemplateContext.put(getKey(template), template.createContext());
988       CheckedTreeNode node = new CheckedTreeNode(template);
989       node.setChecked(!template.isDeactivated());
990       groupNode.add(node);
991     }
992   }
993
994   private class TemplateGroupInputValidator implements InputValidator {
995     private final String myOldName;
996
997     public TemplateGroupInputValidator(String oldName) {
998       myOldName = oldName;
999     }
1000
1001     @Override
1002     public boolean checkInput(String inputString) {
1003       return StringUtil.isNotEmpty(inputString) &&
1004              (getTemplateGroup(inputString) == null || inputString.equals(myOldName));
1005     }
1006
1007     @Override
1008     public boolean canClose(String inputString) {
1009       return checkInput(inputString);
1010     }
1011   }
1012 }