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