8ce2f6ad15a65e9f830b5d6838f0cb4466d10a5d
[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.ide.DataManager;
21 import com.intellij.ide.dnd.DnDEvent;
22 import com.intellij.ide.dnd.DnDManager;
23 import com.intellij.ide.dnd.DnDNativeTarget;
24 import com.intellij.openapi.CompositeDisposable;
25 import com.intellij.openapi.Disposable;
26 import com.intellij.openapi.actionSystem.*;
27 import com.intellij.openapi.application.ApplicationManager;
28 import com.intellij.openapi.ide.CopyPasteManager;
29 import com.intellij.openapi.util.Disposer;
30 import com.intellij.openapi.util.EmptyRunnable;
31 import com.intellij.openapi.util.SystemInfo;
32 import com.intellij.openapi.util.registry.Registry;
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
76   public XWatchesViewImpl(@NotNull XDebugSessionImpl session) {
77     super(session);
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.setMoveUpAction(button -> {
127       List<? extends WatchNode> nodes = XWatchesTreeActionBase.getSelectedNodes(getTree(), WatchNode.class);
128       assert nodes.size() == 1;
129       myRootNode.moveUp(nodes.get(0));
130       updateSessionData();
131     });
132     decorator.setMoveUpActionUpdater(e -> {
133       List<? extends WatchNode> nodes = XWatchesTreeActionBase.getSelectedNodes(getTree(), WatchNode.class);
134       if (nodes.size() != 1) return false;
135       return myRootNode.getIndex(nodes.get(0)) > 0;
136     });
137     decorator.setMoveDownAction(button -> {
138       List<? extends WatchNode> nodes = XWatchesTreeActionBase.getSelectedNodes(getTree(), WatchNode.class);
139       assert nodes.size() == 1;
140       myRootNode.moveDown(nodes.get(0));
141       updateSessionData();
142     });
143     decorator.setMoveDownActionUpdater(e -> {
144       List<? extends WatchNode> nodes = XWatchesTreeActionBase.getSelectedNodes(getTree(), WatchNode.class);
145       if (nodes.size() != 1) return false;
146       return myRootNode.getIndex(nodes.get(0)) < myRootNode.getWatchChildren().size() - 1;
147     });
148     CustomLineBorder border = new CustomLineBorder(CaptionPanel.CNT_ACTIVE_BORDER_COLOR,
149                                                    SystemInfo.isMac ? 1 : 0, 0,
150                                                    SystemInfo.isMac ? 0 : 1, 0);
151     decorator.setToolbarBorder(border);
152     decorator.setPanelBorder(BorderFactory.createEmptyBorder());
153     getPanel().removeAll();
154     if (Registry.is("debugger.watches.in.variables")) {
155       decorator.setToolbarPosition(ActionToolbarPosition.LEFT);
156     }
157     else {
158       getTree().getEmptyText().setText(XDebuggerBundle.message("debugger.no.watches"));
159     }
160     getPanel().add(decorator.createPanel());
161
162     installEditListeners();
163   }
164
165   private void installEditListeners() {
166     final XDebuggerTree watchTree = getTree();
167     final Alarm quitePeriod = new Alarm();
168     final Alarm editAlarm = new Alarm();
169     final ClickListener mouseListener = new ClickListener() {
170       @Override
171       public boolean onClick(@NotNull MouseEvent event, int clickCount) {
172         if (!SwingUtilities.isLeftMouseButton(event) ||
173             ((event.getModifiers() & (InputEvent.SHIFT_MASK | InputEvent.ALT_MASK | InputEvent.CTRL_MASK | InputEvent.META_MASK)) !=0) ) {
174           return false;
175         }
176         boolean sameRow = isAboveSelectedItem(event, watchTree);
177         if (!sameRow || clickCount > 1) {
178           editAlarm.cancelAllRequests();
179           return false;
180         }
181         final AnAction editWatchAction = ActionManager.getInstance().getAction(XDebuggerActions.XEDIT_WATCH);
182         Presentation presentation = editWatchAction.getTemplatePresentation().clone();
183         DataContext context = DataManager.getInstance().getDataContext(watchTree);
184         final AnActionEvent actionEvent = new AnActionEvent(null, context, "WATCH_TREE", presentation, ActionManager.getInstance(), 0);
185         Runnable runnable = new Runnable() {
186           @Override
187           public void run() {
188             editWatchAction.actionPerformed(actionEvent);
189           }
190         };
191         if (editAlarm.isEmpty() && quitePeriod.isEmpty()) {
192           editAlarm.addRequest(runnable, UIUtil.getMultiClickInterval());
193         } else {
194           editAlarm.cancelAllRequests();
195         }
196         return false;
197       }
198     };
199     final ClickListener mouseEmptySpaceListener = new DoubleClickListener() {
200       @Override
201       protected boolean onDoubleClick(MouseEvent event) {
202         if (!isAboveSelectedItem(event, watchTree)) {
203           myRootNode.addNewWatch();
204           return true;
205         }
206         return false;
207       }
208     };
209     ListenerUtil.addClickListener(watchTree, mouseListener);
210     ListenerUtil.addClickListener(watchTree, mouseEmptySpaceListener);
211
212     final FocusListener focusListener = new FocusListener() {
213       @Override
214       public void focusGained(@NotNull FocusEvent e) {
215         quitePeriod.addRequest(EmptyRunnable.getInstance(), UIUtil.getMultiClickInterval());
216       }
217
218       @Override
219       public void focusLost(@NotNull FocusEvent e) {
220         editAlarm.cancelAllRequests();
221       }
222     };
223     ListenerUtil.addFocusListener(watchTree, focusListener);
224
225     final TreeSelectionListener selectionListener = new TreeSelectionListener() {
226       @Override
227       public void valueChanged(@NotNull TreeSelectionEvent e) {
228         quitePeriod.addRequest(EmptyRunnable.getInstance(), UIUtil.getMultiClickInterval());
229       }
230     };
231     watchTree.addTreeSelectionListener(selectionListener);
232     myDisposables.add(new Disposable() {
233       @Override
234       public void dispose() {
235         ListenerUtil.removeClickListener(watchTree, mouseListener);
236         ListenerUtil.removeClickListener(watchTree, mouseEmptySpaceListener);
237         ListenerUtil.removeFocusListener(watchTree, focusListener);
238         watchTree.removeTreeSelectionListener(selectionListener);
239       }
240     });
241   }
242
243   @Override
244   public void dispose() {
245     Disposer.dispose(myDisposables);
246     DnDManager.getInstance().unregisterTarget(this, getTree());
247   }
248
249   private static boolean isAboveSelectedItem(MouseEvent event, XDebuggerTree watchTree) {
250     Rectangle bounds = watchTree.getRowBounds(watchTree.getLeadSelectionRow());
251     if (bounds != null) {
252       bounds.width = watchTree.getWidth();
253       if (bounds.contains(event.getPoint())) {
254         return true;
255       }
256     }
257     return false;
258   }
259
260   private void executeAction(@NotNull String watch) {
261     AnAction action = ActionManager.getInstance().getAction(watch);
262     Presentation presentation = action.getTemplatePresentation().clone();
263     DataContext context = DataManager.getInstance().getDataContext(getTree());
264
265     AnActionEvent actionEvent =
266       new AnActionEvent(null, context, ActionPlaces.DEBUGGER_TOOLBAR, presentation, ActionManager.getInstance(), 0);
267     action.actionPerformed(actionEvent);
268   }
269
270   @Override
271   public void addWatchExpression(@NotNull XExpression expression, int index, final boolean navigateToWatchNode) {
272     XDebugSession session = getSession(getTree());
273     myRootNode.addWatchExpression(session != null ? session.getCurrentStackFrame() : null, expression, index, navigateToWatchNode);
274     updateSessionData();
275     if (navigateToWatchNode && session != null) {
276       showWatchesTab((XDebugSessionImpl)session);
277     }
278   }
279
280   private static void showWatchesTab(@NotNull XDebugSessionImpl session) {
281     XDebugSessionTab tab = session.getSessionTab();
282     if (tab != null) {
283       tab.toFront(false, null);
284       // restore watches tab if minimized
285       JComponent component = tab.getUi().getComponent();
286       if (component instanceof DataProvider) {
287         RunnerContentUi ui = RunnerContentUi.KEY.getData(((DataProvider)component));
288         if (ui != null) {
289           ui.restoreContent(DebuggerContentInfo.WATCHES_CONTENT);
290         }
291       }
292     }
293   }
294
295   public boolean rebuildNeeded() {
296     return myRebuildNeeded;
297   }
298
299   @Override
300   public void processSessionEvent(@NotNull final SessionEvent event) {
301     if (Registry.is("debugger.watches.in.variables") ||
302         getPanel().isShowing() ||
303         ApplicationManager.getApplication().isUnitTestMode()) {
304       myRebuildNeeded = false;
305     }
306     else {
307       myRebuildNeeded = true;
308       return;
309     }
310     super.processSessionEvent(event);
311   }
312
313   @Override
314   protected XValueContainerNode createNewRootNode(@Nullable XStackFrame stackFrame) {
315     WatchesRootNode node = new WatchesRootNode(getTree(), this, getExpressions(), stackFrame);
316     myRootNode = node;
317     getTree().setRoot(node, false);
318     return node;
319   }
320
321   @Override
322   protected void addEmptyMessage(XValueContainerNode root) {
323     if (Registry.is("debugger.watches.in.variables")) {
324       super.addEmptyMessage(root);
325     }
326   }
327
328   @NotNull
329   private XExpression[] getExpressions() {
330     XDebuggerTree tree = getTree();
331     XDebugSession session = getSession(tree);
332     XExpression[] expressions;
333     if (session != null) {
334       expressions = ((XDebugSessionImpl)session).getSessionData().getWatchExpressions();
335     }
336     else {
337       XDebuggerTreeNode root = tree.getRoot();
338       List<? extends WatchNode> current = root instanceof WatchesRootNode
339                                           ? ((WatchesRootNode)tree.getRoot()).getWatchChildren() : Collections.emptyList();
340       List<XExpression> list = ContainerUtil.newArrayList();
341       for (WatchNode child : current) {
342         list.add(child.getExpression());
343       }
344       expressions = list.toArray(new XExpression[list.size()]);
345     }
346     return expressions;
347   }
348
349   @Nullable
350   @Override
351   public Object getData(@NonNls String dataId) {
352     if (XWatchesView.DATA_KEY.is(dataId)) {
353       return this;
354     }
355     return super.getData(dataId);
356   }
357
358   @Override
359   public void removeWatches(List<? extends XDebuggerTreeNode> nodes) {
360     List<? extends WatchNode> children = myRootNode.getWatchChildren();
361     int minIndex = Integer.MAX_VALUE;
362     List<XDebuggerTreeNode> toRemove = new ArrayList<>();
363     for (XDebuggerTreeNode node : nodes) {
364       @SuppressWarnings("SuspiciousMethodCalls")
365       int index = children.indexOf(node);
366       if (index != -1) {
367         toRemove.add(node);
368         minIndex = Math.min(minIndex, index);
369       }
370     }
371     myRootNode.removeChildren(toRemove);
372
373     List<? extends WatchNode> newChildren = myRootNode.getWatchChildren();
374     if (!newChildren.isEmpty()) {
375       WatchNode node = newChildren.get(Math.min(minIndex, newChildren.size() - 1));
376       TreeUtil.selectNode(getTree(), node);
377     }
378     updateSessionData();
379   }
380
381   @Override
382   public void removeAllWatches() {
383     myRootNode.removeAllChildren();
384     updateSessionData();
385   }
386
387   public void updateSessionData() {
388     List<XExpression> watchExpressions = ContainerUtil.newArrayList();
389     List<? extends WatchNode> children = myRootNode.getWatchChildren();
390     for (WatchNode child : children) {
391       watchExpressions.add(child.getExpression());
392     }
393     XDebugSession session = getSession(getTree());
394     XExpression[] expressions = watchExpressions.toArray(new XExpression[watchExpressions.size()]);
395     if (session != null) {
396       ((XDebugSessionImpl)session).setWatchExpressions(expressions);
397     }
398     else {
399       XDebugSessionData data = getData(XDebugSessionData.DATA_KEY, getTree());
400       if (data != null) {
401         data.setWatchExpressions(expressions);
402       }
403     }
404   }
405
406   @Override
407   public boolean update(final DnDEvent aEvent) {
408     Object object = aEvent.getAttachedObject();
409     boolean possible = false;
410     if (object instanceof XValueNodeImpl[]) {
411       possible = true;
412     }
413     else if (object instanceof EventInfo) {
414       possible = ((EventInfo)object).getTextForFlavor(DataFlavor.stringFlavor) != null;
415     }
416
417     aEvent.setDropPossible(possible, XDebuggerBundle.message("xdebugger.drop.text.add.to.watches"));
418
419     return true;
420   }
421
422   @Override
423   public void drop(DnDEvent aEvent) {
424     Object object = aEvent.getAttachedObject();
425     if (object instanceof XValueNodeImpl[]) {
426       final XValueNodeImpl[] nodes = (XValueNodeImpl[])object;
427       for (XValueNodeImpl node : nodes) {
428         node.getValueContainer().calculateEvaluationExpression().done(new Consumer<XExpression>() {
429           @Override
430           public void consume(XExpression expression) {
431             if (expression != null) {
432               //noinspection ConstantConditions
433               addWatchExpression(expression, -1, false);
434             }
435           }
436         });
437       }
438     }
439     else if (object instanceof EventInfo) {
440       String text = ((EventInfo)object).getTextForFlavor(DataFlavor.stringFlavor);
441       if (text != null) {
442         //noinspection ConstantConditions
443         addWatchExpression(XExpressionImpl.fromText(text), -1, false);
444       }
445     }
446   }
447
448   @Override
449   public void cleanUpOnLeave() {
450   }
451
452   @Override
453   public void updateDraggedImage(final Image image, final Point dropPoint, final Point imageOffset) {
454   }
455 }