a054592fd78c2c2dd38841db668e0f84212f794e
[idea/community.git] / platform / xdebugger-impl / src / com / intellij / xdebugger / impl / frame / XWatchesViewImpl.java
1 /*
2  * Copyright 2000-2016 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.frame;
17
18 import com.intellij.icons.AllIcons;
19 import com.intellij.ide.DataManager;
20 import com.intellij.ide.dnd.DnDEvent;
21 import com.intellij.ide.dnd.DnDManager;
22 import com.intellij.ide.dnd.DnDNativeTarget;
23 import com.intellij.openapi.CompositeDisposable;
24 import com.intellij.openapi.Disposable;
25 import com.intellij.openapi.actionSystem.*;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.ide.CopyPasteManager;
28 import com.intellij.openapi.util.Disposer;
29 import com.intellij.openapi.util.EmptyRunnable;
30 import com.intellij.openapi.util.SystemInfo;
31 import com.intellij.ui.*;
32 import com.intellij.ui.border.CustomLineBorder;
33 import com.intellij.util.Alarm;
34 import com.intellij.util.Consumer;
35 import com.intellij.util.containers.ContainerUtil;
36 import com.intellij.util.ui.UIUtil;
37 import com.intellij.util.ui.tree.TreeUtil;
38 import com.intellij.xdebugger.XDebugSession;
39 import com.intellij.xdebugger.XDebuggerBundle;
40 import com.intellij.xdebugger.XExpression;
41 import com.intellij.xdebugger.frame.XStackFrame;
42 import com.intellij.xdebugger.impl.XDebugSessionImpl;
43 import com.intellij.xdebugger.impl.actions.XDebuggerActions;
44 import com.intellij.xdebugger.impl.breakpoints.XExpressionImpl;
45 import com.intellij.xdebugger.impl.frame.actions.XWatchesTreeActionBase;
46 import com.intellij.xdebugger.impl.ui.XDebugSessionData;
47 import com.intellij.xdebugger.impl.ui.XDebugSessionTab;
48 import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree;
49 import com.intellij.xdebugger.impl.ui.tree.actions.XWatchTransferable;
50 import com.intellij.xdebugger.impl.ui.tree.nodes.*;
51 import org.jetbrains.annotations.NonNls;
52 import org.jetbrains.annotations.NotNull;
53 import org.jetbrains.annotations.Nullable;
54
55 import javax.swing.*;
56 import javax.swing.event.TreeSelectionEvent;
57 import javax.swing.event.TreeSelectionListener;
58 import java.awt.*;
59 import java.awt.datatransfer.DataFlavor;
60 import java.awt.event.*;
61 import java.util.ArrayList;
62 import java.util.Collections;
63 import java.util.List;
64
65 /**
66  * @author nik
67  */
68 public class XWatchesViewImpl extends XVariablesView implements DnDNativeTarget, XWatchesView {
69   private WatchesRootNode myRootNode;
70
71   private final CompositeDisposable myDisposables = new CompositeDisposable();
72   private boolean myRebuildNeeded;
73   private final boolean myWatchesInVariables;
74
75   public XWatchesViewImpl(@NotNull XDebugSessionImpl session, boolean watchesInVariables) {
76     super(session);
77     myWatchesInVariables = watchesInVariables;
78
79     ActionManager actionManager = ActionManager.getInstance();
80
81     XDebuggerTree tree = getTree();
82     createNewRootNode(null);
83     AnAction newWatchAction = actionManager.getAction(XDebuggerActions.XNEW_WATCH);
84     AnAction removeWatchAction = actionManager.getAction(XDebuggerActions.XREMOVE_WATCH);
85     AnAction copyAction = actionManager.getAction(XDebuggerActions.XCOPY_WATCH);
86     AnAction editWatchAction = actionManager.getAction(XDebuggerActions.XEDIT_WATCH);
87
88     newWatchAction.registerCustomShortcutSet(CommonShortcuts.INSERT, tree, myDisposables);
89     removeWatchAction.registerCustomShortcutSet(CommonShortcuts.getDelete(), tree, myDisposables);
90
91     CustomShortcutSet f2Shortcut = new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0));
92     editWatchAction.registerCustomShortcutSet(f2Shortcut, tree, myDisposables);
93
94     copyAction.registerCustomShortcutSet(
95       ActionManager.getInstance().getAction(IdeActions.ACTION_EDITOR_DUPLICATE).getShortcutSet(), tree, myDisposables);
96
97     DnDManager.getInstance().registerTarget(this, tree);
98
99     new AnAction() {
100       @Override
101       public void actionPerformed(@NotNull AnActionEvent e) {
102         Object contents = CopyPasteManager.getInstance().getContents(XWatchTransferable.EXPRESSIONS_FLAVOR);
103         if (contents instanceof List) {
104           for (Object item : ((List)contents)){
105             if (item instanceof XExpression) {
106               addWatchExpression(((XExpression)item), -1, true);
107             }
108           }
109         }
110       }
111     }.registerCustomShortcutSet(CommonShortcuts.getPaste(), tree, myDisposables);
112
113     final ToolbarDecorator decorator = ToolbarDecorator.createDecorator(getTree()).disableUpDownActions();
114
115     decorator.setAddAction(button -> executeAction(XDebuggerActions.XNEW_WATCH));
116     decorator.setAddActionName(newWatchAction.getTemplatePresentation().getText());
117
118     decorator.setRemoveAction(button -> executeAction(XDebuggerActions.XREMOVE_WATCH));
119     decorator.setRemoveActionName(removeWatchAction.getTemplatePresentation().getText());
120
121     decorator.setRemoveActionUpdater(e -> {
122       removeWatchAction.update(e);
123       return e.getPresentation().isEnabled();
124     });
125     decorator.addExtraAction(AnActionButton.fromAction(copyAction));
126     decorator.addExtraAction(
127       new ToggleActionButton(XDebuggerBundle.message("debugger.session.tab.show.watches.in.variables"), AllIcons.Debugger.Watches) {
128         @Override
129         public boolean isSelected(AnActionEvent e) {
130           XDebugSessionTab tab = session.getSessionTab();
131           return tab == null || tab.isWatchesInVariables();
132         }
133
134         @Override
135         public void setSelected(AnActionEvent e, boolean state) {
136           XDebugSessionTab tab = session.getSessionTab();
137           if (tab != null) {
138             tab.setWatchesInVariables(!tab.isWatchesInVariables());
139           }
140         }
141       });
142     decorator.setMoveUpAction(button -> {
143       List<? extends WatchNode> nodes = XWatchesTreeActionBase.getSelectedNodes(getTree(), WatchNode.class);
144       assert nodes.size() == 1;
145       myRootNode.moveUp(nodes.get(0));
146       updateSessionData();
147     });
148     decorator.setMoveUpActionUpdater(e -> {
149       List<? extends WatchNode> nodes = XWatchesTreeActionBase.getSelectedNodes(getTree(), WatchNode.class);
150       if (nodes.size() != 1) return false;
151       return myRootNode.getIndex(nodes.get(0)) > 0;
152     });
153     decorator.setMoveDownAction(button -> {
154       List<? extends WatchNode> nodes = XWatchesTreeActionBase.getSelectedNodes(getTree(), WatchNode.class);
155       assert nodes.size() == 1;
156       myRootNode.moveDown(nodes.get(0));
157       updateSessionData();
158     });
159     decorator.setMoveDownActionUpdater(e -> {
160       List<? extends WatchNode> nodes = XWatchesTreeActionBase.getSelectedNodes(getTree(), WatchNode.class);
161       if (nodes.size() != 1) return false;
162       return myRootNode.getIndex(nodes.get(0)) < myRootNode.getWatchChildren().size() - 1;
163     });
164     CustomLineBorder border = new CustomLineBorder(CaptionPanel.CNT_ACTIVE_BORDER_COLOR,
165                                                    SystemInfo.isMac ? 1 : 0, 0,
166                                                    SystemInfo.isMac ? 0 : 1, 0);
167     decorator.setToolbarBorder(border);
168     decorator.setPanelBorder(BorderFactory.createEmptyBorder());
169     getPanel().removeAll();
170     if (myWatchesInVariables) {
171       decorator.setToolbarPosition(ActionToolbarPosition.LEFT);
172     }
173     else {
174       getTree().getEmptyText().setText(XDebuggerBundle.message("debugger.no.watches"));
175     }
176     getPanel().add(decorator.createPanel());
177
178     installEditListeners();
179   }
180
181   private void installEditListeners() {
182     final XDebuggerTree watchTree = getTree();
183     final Alarm quitePeriod = new Alarm();
184     final Alarm editAlarm = new Alarm();
185     final ClickListener mouseListener = new ClickListener() {
186       @Override
187       public boolean onClick(@NotNull MouseEvent event, int clickCount) {
188         if (!SwingUtilities.isLeftMouseButton(event) ||
189             ((event.getModifiers() & (InputEvent.SHIFT_MASK | InputEvent.ALT_MASK | InputEvent.CTRL_MASK | InputEvent.META_MASK)) !=0) ) {
190           return false;
191         }
192         boolean sameRow = isAboveSelectedItem(event, watchTree);
193         if (!sameRow || clickCount > 1) {
194           editAlarm.cancelAllRequests();
195           return false;
196         }
197         final AnAction editWatchAction = ActionManager.getInstance().getAction(XDebuggerActions.XEDIT_WATCH);
198         Presentation presentation = editWatchAction.getTemplatePresentation().clone();
199         DataContext context = DataManager.getInstance().getDataContext(watchTree);
200         final AnActionEvent actionEvent = new AnActionEvent(null, context, "WATCH_TREE", presentation, ActionManager.getInstance(), 0);
201         Runnable runnable = new Runnable() {
202           @Override
203           public void run() {
204             editWatchAction.actionPerformed(actionEvent);
205           }
206         };
207         if (editAlarm.isEmpty() && quitePeriod.isEmpty()) {
208           editAlarm.addRequest(runnable, UIUtil.getMultiClickInterval());
209         } else {
210           editAlarm.cancelAllRequests();
211         }
212         return false;
213       }
214     };
215     final ClickListener mouseEmptySpaceListener = new DoubleClickListener() {
216       @Override
217       protected boolean onDoubleClick(MouseEvent event) {
218         if (!isAboveSelectedItem(event, watchTree)) {
219           myRootNode.addNewWatch();
220           return true;
221         }
222         return false;
223       }
224     };
225     ListenerUtil.addClickListener(watchTree, mouseListener);
226     ListenerUtil.addClickListener(watchTree, mouseEmptySpaceListener);
227
228     final FocusListener focusListener = new FocusListener() {
229       @Override
230       public void focusGained(@NotNull FocusEvent e) {
231         quitePeriod.addRequest(EmptyRunnable.getInstance(), UIUtil.getMultiClickInterval());
232       }
233
234       @Override
235       public void focusLost(@NotNull FocusEvent e) {
236         editAlarm.cancelAllRequests();
237       }
238     };
239     ListenerUtil.addFocusListener(watchTree, focusListener);
240
241     final TreeSelectionListener selectionListener = new TreeSelectionListener() {
242       @Override
243       public void valueChanged(@NotNull TreeSelectionEvent e) {
244         quitePeriod.addRequest(EmptyRunnable.getInstance(), UIUtil.getMultiClickInterval());
245       }
246     };
247     watchTree.addTreeSelectionListener(selectionListener);
248     myDisposables.add(new Disposable() {
249       @Override
250       public void dispose() {
251         ListenerUtil.removeClickListener(watchTree, mouseListener);
252         ListenerUtil.removeClickListener(watchTree, mouseEmptySpaceListener);
253         ListenerUtil.removeFocusListener(watchTree, focusListener);
254         watchTree.removeTreeSelectionListener(selectionListener);
255       }
256     });
257   }
258
259   @Override
260   public void dispose() {
261     Disposer.dispose(myDisposables);
262     DnDManager.getInstance().unregisterTarget(this, getTree());
263     super.dispose();
264   }
265
266   private static boolean isAboveSelectedItem(MouseEvent event, XDebuggerTree watchTree) {
267     Rectangle bounds = watchTree.getRowBounds(watchTree.getLeadSelectionRow());
268     if (bounds != null) {
269       bounds.width = watchTree.getWidth();
270       if (bounds.contains(event.getPoint())) {
271         return true;
272       }
273     }
274     return false;
275   }
276
277   private void executeAction(@NotNull String watch) {
278     AnAction action = ActionManager.getInstance().getAction(watch);
279     Presentation presentation = action.getTemplatePresentation().clone();
280     DataContext context = DataManager.getInstance().getDataContext(getTree());
281
282     AnActionEvent actionEvent =
283       new AnActionEvent(null, context, ActionPlaces.DEBUGGER_TOOLBAR, presentation, ActionManager.getInstance(), 0);
284     action.actionPerformed(actionEvent);
285   }
286
287   @Override
288   public void addWatchExpression(@NotNull XExpression expression, int index, final boolean navigateToWatchNode) {
289     XDebugSession session = getSession(getTree());
290     myRootNode.addWatchExpression(session != null ? session.getCurrentStackFrame() : null, expression, index, navigateToWatchNode);
291     updateSessionData();
292     if (navigateToWatchNode && session != null) {
293       XDebugSessionTab.showWatchesView((XDebugSessionImpl)session);
294     }
295   }
296
297   public boolean rebuildNeeded() {
298     return myRebuildNeeded;
299   }
300
301   @Override
302   public void processSessionEvent(@NotNull final SessionEvent event) {
303     if (myWatchesInVariables ||
304         getPanel().isShowing() ||
305         ApplicationManager.getApplication().isUnitTestMode()) {
306       myRebuildNeeded = false;
307     }
308     else {
309       myRebuildNeeded = true;
310       return;
311     }
312     super.processSessionEvent(event);
313   }
314
315   @Override
316   protected XValueContainerNode createNewRootNode(@Nullable XStackFrame stackFrame) {
317     WatchesRootNode node = new WatchesRootNode(getTree(), this, getExpressions(), stackFrame, myWatchesInVariables);
318     myRootNode = node;
319     getTree().setRoot(node, false);
320     return node;
321   }
322
323   @Override
324   protected void addEmptyMessage(XValueContainerNode root) {
325     if (myWatchesInVariables) {
326       super.addEmptyMessage(root);
327     }
328   }
329
330   @NotNull
331   private XExpression[] getExpressions() {
332     XDebuggerTree tree = getTree();
333     XDebugSession session = getSession(tree);
334     XExpression[] expressions;
335     if (session != null) {
336       expressions = ((XDebugSessionImpl)session).getSessionData().getWatchExpressions();
337     }
338     else {
339       XDebuggerTreeNode root = tree.getRoot();
340       List<? extends WatchNode> current = root instanceof WatchesRootNode
341                                           ? ((WatchesRootNode)tree.getRoot()).getWatchChildren() : Collections.emptyList();
342       List<XExpression> list = ContainerUtil.newArrayList();
343       for (WatchNode child : current) {
344         list.add(child.getExpression());
345       }
346       expressions = list.toArray(new XExpression[list.size()]);
347     }
348     return expressions;
349   }
350
351   @Nullable
352   @Override
353   public Object getData(@NonNls String dataId) {
354     if (XWatchesView.DATA_KEY.is(dataId)) {
355       return this;
356     }
357     return super.getData(dataId);
358   }
359
360   @Override
361   public void removeWatches(List<? extends XDebuggerTreeNode> nodes) {
362     List<? extends WatchNode> children = myRootNode.getWatchChildren();
363     int minIndex = Integer.MAX_VALUE;
364     List<XDebuggerTreeNode> toRemove = new ArrayList<>();
365     for (XDebuggerTreeNode node : nodes) {
366       @SuppressWarnings("SuspiciousMethodCalls")
367       int index = children.indexOf(node);
368       if (index != -1) {
369         toRemove.add(node);
370         minIndex = Math.min(minIndex, index);
371       }
372     }
373     myRootNode.removeChildren(toRemove);
374
375     List<? extends WatchNode> newChildren = myRootNode.getWatchChildren();
376     if (!newChildren.isEmpty()) {
377       WatchNode node = newChildren.get(Math.min(minIndex, newChildren.size() - 1));
378       TreeUtil.selectNode(getTree(), node);
379     }
380     updateSessionData();
381   }
382
383   @Override
384   public void removeAllWatches() {
385     myRootNode.removeAllChildren();
386     updateSessionData();
387   }
388
389   public void updateSessionData() {
390     List<XExpression> watchExpressions = ContainerUtil.newArrayList();
391     List<? extends WatchNode> children = myRootNode.getWatchChildren();
392     for (WatchNode child : children) {
393       watchExpressions.add(child.getExpression());
394     }
395     XDebugSession session = getSession(getTree());
396     XExpression[] expressions = watchExpressions.toArray(new XExpression[watchExpressions.size()]);
397     if (session != null) {
398       ((XDebugSessionImpl)session).setWatchExpressions(expressions);
399     }
400     else {
401       XDebugSessionData data = getData(XDebugSessionData.DATA_KEY, getTree());
402       if (data != null) {
403         data.setWatchExpressions(expressions);
404       }
405     }
406   }
407
408   @Override
409   public boolean update(final DnDEvent aEvent) {
410     Object object = aEvent.getAttachedObject();
411     boolean possible = false;
412     if (object instanceof XValueNodeImpl[]) {
413       possible = true;
414     }
415     else if (object instanceof EventInfo) {
416       possible = ((EventInfo)object).getTextForFlavor(DataFlavor.stringFlavor) != null;
417     }
418
419     aEvent.setDropPossible(possible, XDebuggerBundle.message("xdebugger.drop.text.add.to.watches"));
420
421     return true;
422   }
423
424   @Override
425   public void drop(DnDEvent aEvent) {
426     Object object = aEvent.getAttachedObject();
427     if (object instanceof XValueNodeImpl[]) {
428       final XValueNodeImpl[] nodes = (XValueNodeImpl[])object;
429       for (XValueNodeImpl node : nodes) {
430         node.getValueContainer().calculateEvaluationExpression().done(new Consumer<XExpression>() {
431           @Override
432           public void consume(XExpression expression) {
433             if (expression != null) {
434               //noinspection ConstantConditions
435               addWatchExpression(expression, -1, false);
436             }
437           }
438         });
439       }
440     }
441     else if (object instanceof EventInfo) {
442       String text = ((EventInfo)object).getTextForFlavor(DataFlavor.stringFlavor);
443       if (text != null) {
444         //noinspection ConstantConditions
445         addWatchExpression(XExpressionImpl.fromText(text), -1, false);
446       }
447     }
448   }
449
450   @Override
451   public void cleanUpOnLeave() {
452   }
453
454   @Override
455   public void updateDraggedImage(final Image image, final Point dropPoint, final Point imageOffset) {
456   }
457 }