expand breakpoint groups on double click
[idea/community.git] / platform / xdebugger-impl / src / com / intellij / xdebugger / impl / breakpoints / ui / BreakpointsDialog.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.xdebugger.impl.breakpoints.ui;
17
18 import com.intellij.icons.AllIcons;
19 import com.intellij.ide.DataManager;
20 import com.intellij.ide.util.treeView.TreeState;
21 import com.intellij.openapi.Disposable;
22 import com.intellij.openapi.actionSystem.*;
23 import com.intellij.openapi.project.DumbAware;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.ui.DialogWrapper;
26 import com.intellij.openapi.ui.Messages;
27 import com.intellij.openapi.ui.popup.JBPopupFactory;
28 import com.intellij.openapi.util.Condition;
29 import com.intellij.openapi.util.Disposer;
30 import com.intellij.ui.*;
31 import com.intellij.ui.popup.util.DetailController;
32 import com.intellij.ui.popup.util.DetailViewImpl;
33 import com.intellij.ui.popup.util.ItemWrapper;
34 import com.intellij.ui.popup.util.MasterController;
35 import com.intellij.util.Function;
36 import com.intellij.util.SingleAlarm;
37 import com.intellij.util.containers.ContainerUtil;
38 import com.intellij.util.containers.HashSet;
39 import com.intellij.util.ui.tree.TreeUtil;
40 import com.intellij.xdebugger.XDebuggerManager;
41 import com.intellij.xdebugger.breakpoints.XBreakpoint;
42 import com.intellij.xdebugger.breakpoints.XBreakpointType;
43 import com.intellij.xdebugger.breakpoints.ui.XBreakpointGroupingRule;
44 import com.intellij.xdebugger.impl.breakpoints.XBreakpointBase;
45 import com.intellij.xdebugger.impl.breakpoints.XBreakpointManagerImpl;
46 import com.intellij.xdebugger.impl.breakpoints.XBreakpointUtil;
47 import com.intellij.xdebugger.impl.breakpoints.XBreakpointsDialogState;
48 import com.intellij.xdebugger.impl.breakpoints.ui.grouping.XBreakpointCustomGroup;
49 import com.intellij.xdebugger.impl.breakpoints.ui.tree.BreakpointItemNode;
50 import com.intellij.xdebugger.impl.breakpoints.ui.tree.BreakpointItemsTreeController;
51 import com.intellij.xdebugger.impl.breakpoints.ui.tree.BreakpointsCheckboxTree;
52 import com.intellij.xdebugger.impl.breakpoints.ui.tree.BreakpointsGroupNode;
53 import org.jetbrains.annotations.NotNull;
54 import org.jetbrains.annotations.Nullable;
55
56 import javax.swing.*;
57 import javax.swing.tree.DefaultMutableTreeNode;
58 import javax.swing.tree.TreePath;
59 import java.awt.*;
60 import java.util.*;
61 import java.util.List;
62
63 public class BreakpointsDialog extends DialogWrapper {
64   @NotNull private Project myProject;
65
66   private Object myInitialBreakpoint;
67   private List<BreakpointPanelProvider> myBreakpointsPanelProviders;
68
69   private BreakpointItemsTreeController myTreeController;
70
71   JLabel temp = new JLabel();
72
73   private MasterController myMasterController = new MasterController() {
74     @Override
75     public ItemWrapper[] getSelectedItems() {
76       final List<BreakpointItem> res = myTreeController.getSelectedBreakpoints();
77       return res.toArray(new ItemWrapper[res.size()]);
78     }
79
80     @Override
81     public JLabel getPathLabel() {
82       return temp;
83     }
84   };
85
86   private final DetailController myDetailController = new DetailController(myMasterController);
87
88   private final Collection<BreakpointItem> myBreakpointItems = new ArrayList<BreakpointItem>();
89
90   private final SingleAlarm myRebuildAlarm = new SingleAlarm(new Runnable() {
91     @Override
92     public void run() {
93       collectItems();
94       myTreeController.rebuildTree(myBreakpointItems);
95       myDetailController.doUpdateDetailView(true);
96     }
97   }, 100, myDisposable);
98
99   private final List<XBreakpointGroupingRule> myRulesAvailable = new ArrayList<XBreakpointGroupingRule>();
100
101   private final Set<XBreakpointGroupingRule> myRulesEnabled = new TreeSet<XBreakpointGroupingRule>(XBreakpointGroupingRule.PRIORITY_COMPARATOR);
102   private final Disposable myListenerDisposable = Disposer.newDisposable();
103   private final List<ToggleActionButton> myToggleRuleActions = new ArrayList<ToggleActionButton>();
104
105   private XBreakpointManagerImpl getBreakpointManager() {
106     return (XBreakpointManagerImpl)XDebuggerManager.getInstance(myProject).getBreakpointManager();
107   }
108
109   protected BreakpointsDialog(@NotNull Project project, Object breakpoint, @NotNull List<BreakpointPanelProvider> providers) {
110     super(project);
111     myProject = project;
112     myBreakpointsPanelProviders = providers;
113     myInitialBreakpoint = breakpoint;
114
115     collectGroupingRules();
116
117     collectItems();
118
119     setTitle("Breakpoints");
120     setModal(false);
121     init();
122     setOKButtonText("Done");
123   }
124
125   private String getSplitterProportionKey() {
126     return getDimensionServiceKey() + ".splitter";
127   }
128
129   @Nullable
130   @Override
131   protected JComponent createCenterPanel() {
132     JPanel mainPanel = new JPanel(new BorderLayout());
133
134     JBSplitter splitPane = new JBSplitter(0.3f);
135     splitPane.setSplitterProportionKey(getSplitterProportionKey());
136
137     splitPane.setFirstComponent(createMasterView());
138     splitPane.setSecondComponent(createDetailView());
139
140     mainPanel.add(splitPane, BorderLayout.CENTER);
141
142     return mainPanel;
143   }
144
145   private JComponent createDetailView() {
146     DetailViewImpl detailView = new DetailViewImpl(myProject);
147     myDetailController.setDetailView(detailView);
148
149     return detailView;
150   }
151
152   void collectItems() {
153     if (!myBreakpointsPanelProviders.isEmpty()) {
154       disposeItems();
155       myBreakpointItems.clear();
156       for (BreakpointPanelProvider panelProvider : myBreakpointsPanelProviders) {
157         panelProvider.provideBreakpointItems(myProject, myBreakpointItems);
158       }
159     }
160   }
161
162   void initSelection(Collection<BreakpointItem> breakpoints) {
163     XBreakpointsDialogState settings = (getBreakpointManager()).getBreakpointsDialogSettings();
164     if (settings != null && settings.getTreeState() != null) {
165       settings.getTreeState().applyTo(myTreeController.getTreeView());
166     }
167     else {
168       TreeUtil.expandAll(myTreeController.getTreeView());
169     }
170     selectBreakpoint(myInitialBreakpoint);
171   }
172
173   @Nullable
174   @Override
175   protected String getDimensionServiceKey() {
176     return getClass().getName();
177   }
178
179   @NotNull
180   @Override
181   protected Action[] createActions() {
182     return new Action[]{getOKAction(), getHelpAction()};
183   }
184
185   private class ToggleBreakpointGroupingRuleEnabledAction extends ToggleActionButton {
186     private XBreakpointGroupingRule myRule;
187
188     public ToggleBreakpointGroupingRuleEnabledAction(XBreakpointGroupingRule rule) {
189       super(rule.getPresentableName(), rule.getIcon());
190       myRule = rule;
191       getTemplatePresentation().setText(rule.getPresentableName());
192     }
193
194     @Override
195     public boolean isSelected(AnActionEvent e) {
196       return myRulesEnabled.contains(myRule);
197     }
198
199     @Override
200     public void setSelected(AnActionEvent e, boolean state) {
201       if (state) {
202         myRulesEnabled.add(myRule);
203       }
204       else {
205         myRulesEnabled.remove(myRule);
206       }
207       myTreeController.setGroupingRules(myRulesEnabled);
208     }
209   }
210
211   private JComponent createMasterView() {
212     myTreeController = new BreakpointItemsTreeController(myRulesEnabled) {
213       @Override
214       public void nodeStateWillChangeImpl(CheckedTreeNode node) {
215         if (node instanceof BreakpointItemNode) {
216           ((BreakpointItemNode)node).getBreakpointItem().saveState();
217         }
218         super.nodeStateWillChangeImpl(node);
219       }
220
221       @Override
222       public void nodeStateDidChangeImpl(CheckedTreeNode node) {
223         super.nodeStateDidChangeImpl(node);
224         if (node instanceof BreakpointItemNode) {
225           myDetailController.doUpdateDetailView(true);
226         }
227       }
228
229       @Override
230       protected void selectionChangedImpl() {
231         super.selectionChangedImpl();
232         saveCurrentItem();
233         myDetailController.updateDetailView();
234       }
235     };
236     final JTree tree = new BreakpointsCheckboxTree(myProject, myTreeController) {
237       @Override
238       protected void onDoubleClick(CheckedTreeNode node) {
239         if (node instanceof BreakpointsGroupNode) {
240           TreePath path = TreeUtil.getPathFromRoot(node);
241           if (isExpanded(path)) {
242             collapsePath(path);
243           }
244           else {
245             expandPath(path);
246           }
247         }
248         else {
249           navigate(false);
250         }
251       }
252     };
253
254     PopupHandler.installPopupHandler(tree, new ActionGroup() {
255       @NotNull
256       @Override
257       public AnAction[] getChildren(@Nullable AnActionEvent e) {
258         ActionGroup group = new ActionGroup("Move to group", true) {
259           @NotNull
260           @Override
261           public AnAction[] getChildren(@Nullable AnActionEvent e) {
262             Set<String> groups = getBreakpointManager().getAllGroups();
263             AnAction[] res = new AnAction[groups.size()+3];
264             int i = 0;
265             res[i++] = new MoveToGroupAction(null);
266             for (String group : groups) {
267               res[i++] = new MoveToGroupAction(group);
268             }
269             res[i++] = new Separator();
270             res[i] = new MoveToGroupAction();
271             return res;
272           }
273         };
274         List<AnAction> res = new ArrayList<AnAction>();
275         res.add(group);
276         Object component = tree.getLastSelectedPathComponent();
277         if (tree.getSelectionCount() == 1 && component instanceof BreakpointsGroupNode &&
278             ((BreakpointsGroupNode)component).getGroup() instanceof XBreakpointCustomGroup) {
279           res.add(new SetAsDefaultGroupAction((XBreakpointCustomGroup)((BreakpointsGroupNode)component).getGroup()));
280         }
281         if (tree.getSelectionCount() == 1 && component instanceof BreakpointItemNode) {
282           res.add(new EditDescriptionAction((XBreakpointBase)((BreakpointItemNode)component).getBreakpointItem().getBreakpoint()));
283         }
284         return res.toArray(new AnAction[res.size()]);
285       }
286     }, ActionPlaces.UNKNOWN, ActionManager.getInstance());
287
288     new AnAction("BreakpointDialog.GoToSource") {
289       @Override
290       public void actionPerformed(AnActionEvent e) {
291         navigate(true);
292         close(OK_EXIT_CODE);
293       }
294     }.registerCustomShortcutSet(CommonShortcuts.ENTER, tree);
295
296     new AnAction("BreakpointDialog.ShowSource") {
297       @Override
298       public void actionPerformed(AnActionEvent e) {
299         navigate(true);
300         close(OK_EXIT_CODE);
301       }
302     }.registerCustomShortcutSet(ActionManager.getInstance().getAction(IdeActions.ACTION_EDIT_SOURCE).getShortcutSet(), tree);
303
304     final DefaultActionGroup breakpointTypes = new DefaultActionGroup();
305     for (XBreakpointType<?, ?> type : XBreakpointUtil.getBreakpointTypes()) {
306       if (type.isAddBreakpointButtonVisible()) {
307         breakpointTypes.addAll(new AddXBreakpointAction(type));
308       }
309     }
310
311     ToolbarDecorator decorator = ToolbarDecorator.createDecorator(tree).
312       setAddAction(new AnActionButtonRunnable() {
313         @Override
314         public void run(AnActionButton button) {
315           JBPopupFactory.getInstance()
316             .createActionGroupPopup(null, breakpointTypes, DataManager.getInstance().getDataContext(button.getContextComponent()),
317                                     JBPopupFactory.ActionSelectionAid.NUMBERING, false)
318             .show(button.getPreferredPopupPoint());
319         }
320       }).
321       setRemoveAction(new AnActionButtonRunnable() {
322         @Override
323         public void run(AnActionButton button) {
324           myTreeController.removeSelectedBreakpoints(myProject);
325         }
326       }).
327       setRemoveActionUpdater(new AnActionButtonUpdater() {
328         @Override
329         public boolean isEnabled(AnActionEvent e) {
330           boolean enabled = false;
331           final ItemWrapper[] items = myMasterController.getSelectedItems();
332           for (ItemWrapper item : items) {
333             if (item.allowedToRemove()) {
334               enabled = true;
335             }
336           }
337           return enabled;
338         }
339       }).
340       setToolbarPosition(ActionToolbarPosition.TOP).
341       setToolbarBorder(IdeBorderFactory.createEmptyBorder());
342
343     tree.setBorder(IdeBorderFactory.createBorder());
344
345     for (ToggleActionButton action : myToggleRuleActions) {
346       decorator.addExtraAction(action);
347     }
348
349     JPanel decoratedTree = decorator.createPanel();
350     decoratedTree.setBorder(IdeBorderFactory.createEmptyBorder());
351
352     myTreeController.setTreeView(tree);
353
354     myTreeController.buildTree(myBreakpointItems);
355
356     initSelection(myBreakpointItems);
357
358     final BreakpointPanelProvider.BreakpointsListener listener = new BreakpointPanelProvider.BreakpointsListener() {
359       @Override
360       public void breakpointsChanged() {
361         myRebuildAlarm.cancelAndRequest();
362       }
363     };
364
365     for (BreakpointPanelProvider provider : myBreakpointsPanelProviders) {
366       provider.addListener(listener, myProject, myListenerDisposable);
367     }
368
369     return decoratedTree;
370   }
371
372   private void navigate(final boolean requestFocus) {
373     List<BreakpointItem> breakpoints = myTreeController.getSelectedBreakpoints();
374     if (!breakpoints.isEmpty()) {
375       breakpoints.get(0).navigate(requestFocus);
376     }
377   }
378
379   @Nullable
380   @Override
381   public JComponent getPreferredFocusedComponent() {
382     return myTreeController.getTreeView();
383   }
384
385   private void collectGroupingRules() {
386     for (BreakpointPanelProvider provider : myBreakpointsPanelProviders) {
387       provider.createBreakpointsGroupingRules(myRulesAvailable);
388     }
389     Collections.sort(myRulesAvailable, XBreakpointGroupingRule.PRIORITY_COMPARATOR);
390
391     myRulesEnabled.clear();
392     XBreakpointsDialogState settings = (getBreakpointManager()).getBreakpointsDialogSettings();
393
394     for (XBreakpointGroupingRule rule : myRulesAvailable) {
395       if (rule.isAlwaysEnabled() || (settings != null && settings.getSelectedGroupingRules().contains(rule.getId()) ) ) {
396         myRulesEnabled.add(rule);
397       }
398     }
399
400     for (XBreakpointGroupingRule rule : myRulesAvailable) {
401       if (!rule.isAlwaysEnabled()) {
402         myToggleRuleActions.add(new ToggleBreakpointGroupingRuleEnabledAction(rule));
403       }
404     }
405   }
406
407   private void saveBreakpointsDialogState() {
408     final XBreakpointsDialogState dialogState = new XBreakpointsDialogState();
409     saveTreeState(dialogState);
410     final List<XBreakpointGroupingRule> rulesEnabled = ContainerUtil.filter(myRulesEnabled, new Condition<XBreakpointGroupingRule>() {
411       @Override
412       public boolean value(XBreakpointGroupingRule rule) {
413         return !rule.isAlwaysEnabled();
414       }
415     });
416
417     dialogState.setSelectedGroupingRules(new HashSet<String>(ContainerUtil.map(rulesEnabled, new Function<XBreakpointGroupingRule, String>() {
418       @Override
419       public String fun(XBreakpointGroupingRule rule) {
420         return rule.getId();
421       }
422     })));
423     getBreakpointManager().setBreakpointsDialogSettings(dialogState);
424   }
425
426   private void saveTreeState(XBreakpointsDialogState state) {
427     JTree tree = myTreeController.getTreeView();
428     state.setTreeState(TreeState.createOn(tree, (DefaultMutableTreeNode)tree.getModel().getRoot()));
429   }
430
431   @Override
432   protected void dispose() {
433     saveCurrentItem();
434     Disposer.dispose(myListenerDisposable);
435     saveBreakpointsDialogState();
436     disposeItems();
437     super.dispose();
438   }
439
440   private void disposeItems() {
441     for (BreakpointItem item : myBreakpointItems) {
442       item.dispose();
443     }
444   }
445
446   @Nullable
447   @Override
448   protected String getHelpId() {
449     return "reference.dialogs.breakpoints";
450   }
451
452   private void saveCurrentItem() {
453     ItemWrapper item = myDetailController.getSelectedItem();
454     if (item instanceof BreakpointItem) {
455       ((BreakpointItem)item).saveState();
456     }
457   }
458
459   private class AddXBreakpointAction extends AnAction implements DumbAware {
460     private final XBreakpointType<?, ?> myType;
461
462     public AddXBreakpointAction(XBreakpointType<?, ?> type) {
463       myType = type;
464       getTemplatePresentation().setIcon(type.getEnabledIcon());
465       getTemplatePresentation().setText(type.getTitle());
466     }
467
468     @Override
469     public void actionPerformed(AnActionEvent e) {
470       saveCurrentItem();
471       XBreakpoint<?> breakpoint = myType.addBreakpoint(myProject, null);
472       if (breakpoint != null) {
473         selectBreakpoint(breakpoint);
474       }
475     }
476   }
477
478   private boolean selectBreakpoint(Object breakpoint) {
479     if (breakpoint != null) {
480       for (BreakpointItem item : myBreakpointItems) {
481         if (item.getBreakpoint() == breakpoint) {
482           myTreeController.selectBreakpointItem(item, null);
483           return true;
484         }
485       }
486     }
487     return false;
488   }
489
490   private class MoveToGroupAction extends AnAction {
491     private final String myGroup;
492     private final boolean myNewGroup;
493
494     private MoveToGroupAction(String group) {
495       super(group == null ? "<no group>" : group);
496       myGroup = group;
497       myNewGroup = false;
498     }
499
500     private MoveToGroupAction() {
501       super("Create new...");
502       myNewGroup = true;
503       myGroup = null;
504     }
505
506     @Override
507     public void actionPerformed(AnActionEvent e) {
508       String groupName = myGroup;
509       if (myNewGroup) {
510         groupName = Messages.showInputDialog("New group name", "New Group", AllIcons.Nodes.NewFolder);
511         if (groupName == null) {
512           return;
513         }
514       }
515       for (BreakpointItem item : myTreeController.getSelectedBreakpoints()) {
516         Object breakpoint = item.getBreakpoint();
517         if (breakpoint instanceof XBreakpointBase) {
518           ((XBreakpointBase)breakpoint).setGroup(groupName);
519         }
520       }
521       myTreeController.rebuildTree(myBreakpointItems);
522     }
523   }
524
525   private class SetAsDefaultGroupAction extends AnAction {
526     private final String myName;
527
528     private SetAsDefaultGroupAction(XBreakpointCustomGroup group) {
529       super(group.isDefault() ? "Unset as default" : "Set as default");
530       myName = group.isDefault() ? null : group.getName();
531     }
532
533     @Override
534     public void actionPerformed(AnActionEvent e) {
535       getBreakpointManager().setDefaultGroup(myName);
536       myTreeController.rebuildTree(myBreakpointItems);
537     }
538   }
539
540   private class EditDescriptionAction extends AnAction {
541     private final XBreakpointBase myBreakpoint;
542
543     private EditDescriptionAction(XBreakpointBase breakpoint) {
544       super("Edit description");
545       myBreakpoint = breakpoint;
546     }
547
548     @Override
549     public void actionPerformed(AnActionEvent e) {
550       String description = Messages.showInputDialog("", "Edit Description", null, myBreakpoint.getUserDescription(), null);
551       if (description == null) {
552         return;
553       }
554       myBreakpoint.setUserDescription(description);
555       myTreeController.rebuildTree(myBreakpointItems);
556     }
557   }
558 }