| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.xdebugger.impl.ui.tree; |
| |
| import com.intellij.ide.dnd.aware.DnDAwareTree; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.actionSystem.*; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.fileEditor.FileEditorManager; |
| import com.intellij.openapi.keymap.KeymapManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Condition; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vcs.changes.issueLinks.TreeLinkMouseListener; |
| import com.intellij.ui.DoubleClickListener; |
| import com.intellij.ui.PopupHandler; |
| import com.intellij.ui.TreeSpeedSearch; |
| import com.intellij.util.SingleAlarm; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.Convertor; |
| import com.intellij.util.containers.TransferToEDTQueue; |
| import com.intellij.util.ui.TextTransferable; |
| import com.intellij.util.ui.tree.TreeModelAdapter; |
| import com.intellij.xdebugger.XSourcePosition; |
| import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider; |
| import com.intellij.xdebugger.frame.XDebuggerTreeNodeHyperlink; |
| import com.intellij.xdebugger.impl.actions.XDebuggerActions; |
| import com.intellij.xdebugger.impl.frame.XValueMarkers; |
| import com.intellij.xdebugger.impl.ui.tree.nodes.*; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.event.TreeModelEvent; |
| import javax.swing.tree.DefaultTreeModel; |
| import javax.swing.tree.TreeNode; |
| import javax.swing.tree.TreePath; |
| import java.awt.*; |
| import java.awt.datatransfer.Transferable; |
| import java.awt.event.KeyAdapter; |
| import java.awt.event.KeyEvent; |
| import java.awt.event.MouseEvent; |
| import java.util.List; |
| |
| /** |
| * @author nik |
| */ |
| public class XDebuggerTree extends DnDAwareTree implements DataProvider, Disposable { |
| private final TransferToEDTQueue<Runnable> myLaterInvocator = TransferToEDTQueue.createRunnableMerger("XDebuggerTree later invocator", 100); |
| |
| private static final DataKey<XDebuggerTree> XDEBUGGER_TREE_KEY = DataKey.create("xdebugger.tree"); |
| private final SingleAlarm myAlarm = new SingleAlarm(new Runnable() { |
| @Override |
| public void run() { |
| final Editor editor = FileEditorManager.getInstance(myProject).getSelectedTextEditor(); |
| if (editor != null) { |
| editor.getComponent().revalidate(); |
| editor.getComponent().repaint(); |
| } |
| } |
| }, 100, this); |
| |
| private static final Convertor<TreePath, String> SPEED_SEARCH_CONVERTER = new Convertor<TreePath, String>() { |
| @Override |
| public String convert(TreePath o) { |
| String text = null; |
| if (o != null) { |
| final Object node = o.getLastPathComponent(); |
| if (node instanceof XDebuggerTreeNode) { |
| text = ((XDebuggerTreeNode)node).getText().toString(); |
| } |
| } |
| return StringUtil.notNullize(text); |
| } |
| }; |
| |
| private static final TransferHandler DEFAULT_TRANSFER_HANDLER = new TransferHandler() { |
| @Override |
| protected Transferable createTransferable(JComponent c) { |
| if (!(c instanceof XDebuggerTree)) { |
| return null; |
| } |
| XDebuggerTree tree = (XDebuggerTree)c; |
| //noinspection deprecation |
| TreePath[] selectedPaths = tree.getSelectionPaths(); |
| if (selectedPaths == null || selectedPaths.length == 0) { |
| return null; |
| } |
| |
| StringBuilder plainBuf = new StringBuilder(); |
| StringBuilder htmlBuf = new StringBuilder(); |
| htmlBuf.append("<html>\n<body>\n<ul>\n"); |
| TextTransferable.ColoredStringBuilder coloredTextContainer = new TextTransferable.ColoredStringBuilder(); |
| for (TreePath path : selectedPaths) { |
| htmlBuf.append(" <li>"); |
| Object node = path.getLastPathComponent(); |
| if (node != null) { |
| if (node instanceof XDebuggerTreeNode) { |
| ((XDebuggerTreeNode)node).appendToComponent(coloredTextContainer); |
| coloredTextContainer.appendTo(plainBuf, htmlBuf); |
| } |
| else { |
| String text = node.toString(); |
| plainBuf.append(text); |
| htmlBuf.append(text); |
| } |
| } |
| plainBuf.append('\n'); |
| htmlBuf.append("</li>\n"); |
| } |
| |
| // remove the last newline |
| plainBuf.setLength(plainBuf.length() - 1); |
| htmlBuf.append("</ul>\n</body>\n</html>"); |
| return new TextTransferable(htmlBuf.toString(), plainBuf.toString()); |
| } |
| |
| @Override |
| public int getSourceActions(JComponent c) { |
| return COPY; |
| } |
| }; |
| |
| private final DefaultTreeModel myTreeModel; |
| private final Project myProject; |
| private final XDebuggerEditorsProvider myEditorsProvider; |
| private XSourcePosition mySourcePosition; |
| private final List<XDebuggerTreeListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); |
| private final XValueMarkers<?,?> myValueMarkers; |
| |
| public XDebuggerTree(final @NotNull Project project, |
| final @NotNull XDebuggerEditorsProvider editorsProvider, |
| final @Nullable XSourcePosition sourcePosition, |
| final @NotNull String popupActionGroupId, @Nullable XValueMarkers<?, ?> valueMarkers) { |
| myValueMarkers = valueMarkers; |
| myProject = project; |
| myEditorsProvider = editorsProvider; |
| mySourcePosition = sourcePosition; |
| myTreeModel = new DefaultTreeModel(null); |
| myTreeModel.addTreeModelListener(new TreeModelAdapter() { |
| @Override |
| public void treeNodesChanged(TreeModelEvent e) { |
| updateEditor(); |
| } |
| |
| @Override |
| public void treeNodesInserted(TreeModelEvent e) { |
| updateEditor(); |
| } |
| |
| @Override |
| public void treeNodesRemoved(TreeModelEvent e) { |
| updateEditor(); |
| } |
| |
| @Override |
| public void treeStructureChanged(TreeModelEvent e) { |
| updateEditor(); |
| } |
| }); |
| setModel(myTreeModel); |
| setCellRenderer(new XDebuggerTreeRenderer()); |
| new TreeLinkMouseListener(new XDebuggerTreeRenderer()) { |
| @Override |
| protected void handleTagClick(@Nullable Object tag, @NotNull MouseEvent event) { |
| if (tag instanceof XDebuggerTreeNodeHyperlink) { |
| ((XDebuggerTreeNodeHyperlink)tag).onClick(event); |
| } |
| } |
| }.installOn(this); |
| setRootVisible(false); |
| setShowsRootHandles(true); |
| |
| new DoubleClickListener() { |
| @Override |
| protected boolean onDoubleClick(MouseEvent e) { |
| return expandIfEllipsis(); |
| } |
| }.installOn(this); |
| |
| addKeyListener(new KeyAdapter() { |
| @Override |
| public void keyPressed(KeyEvent e) { |
| int key = e.getKeyCode(); |
| if (key == KeyEvent.VK_ENTER || key == KeyEvent.VK_SPACE || key == KeyEvent.VK_RIGHT) { |
| expandIfEllipsis(); |
| } |
| } |
| }); |
| |
| if (Boolean.valueOf(System.getProperty("xdebugger.variablesView.rss"))) { |
| new XDebuggerTreeSpeedSearch(this, SPEED_SEARCH_CONVERTER); |
| } |
| else { |
| new TreeSpeedSearch(this, SPEED_SEARCH_CONVERTER); |
| } |
| |
| final ActionManager actionManager = ActionManager.getInstance(); |
| addMouseListener(new PopupHandler() { |
| @Override |
| public void invokePopup(final Component comp, final int x, final int y) { |
| ActionGroup group = (ActionGroup)actionManager.getAction(popupActionGroupId); |
| actionManager.createActionPopupMenu(ActionPlaces.UNKNOWN, group).getComponent().show(comp, x, y); |
| } |
| }); |
| registerShortcuts(); |
| |
| setTransferHandler(DEFAULT_TRANSFER_HANDLER); |
| } |
| |
| private void updateEditor() { |
| myAlarm.cancelAndRequest(); |
| } |
| |
| private boolean expandIfEllipsis() { |
| MessageTreeNode[] treeNodes = getSelectedNodes(MessageTreeNode.class, null); |
| if (treeNodes.length == 1) { |
| MessageTreeNode node = treeNodes[0]; |
| if (node.isEllipsis()) { |
| TreeNode parent = node.getParent(); |
| if (parent instanceof XValueContainerNode) { |
| ((XValueContainerNode)parent).startComputingChildren(); |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| public void addTreeListener(@NotNull XDebuggerTreeListener listener) { |
| myListeners.add(listener); |
| } |
| |
| public void removeTreeListener(@NotNull XDebuggerTreeListener listener) { |
| myListeners.remove(listener); |
| } |
| |
| public void setRoot(XDebuggerTreeNode root, final boolean rootVisible) { |
| setRootVisible(rootVisible); |
| myTreeModel.setRoot(root); |
| } |
| |
| public XDebuggerTreeNode getRoot() { |
| return (XDebuggerTreeNode)myTreeModel.getRoot(); |
| } |
| |
| @Nullable |
| public XSourcePosition getSourcePosition() { |
| return mySourcePosition; |
| } |
| |
| public void setSourcePosition(final @Nullable XSourcePosition sourcePosition) { |
| mySourcePosition = sourcePosition; |
| } |
| |
| @NotNull |
| public XDebuggerEditorsProvider getEditorsProvider() { |
| return myEditorsProvider; |
| } |
| |
| @NotNull |
| public Project getProject() { |
| return myProject; |
| } |
| |
| @Nullable |
| public XValueMarkers<?, ?> getValueMarkers() { |
| return myValueMarkers; |
| } |
| |
| public DefaultTreeModel getTreeModel() { |
| return myTreeModel; |
| } |
| |
| @Override |
| @Nullable |
| public Object getData(@NonNls final String dataId) { |
| if (XDEBUGGER_TREE_KEY.is(dataId)) { |
| return this; |
| } |
| if (PlatformDataKeys.PREDEFINED_TEXT.is(dataId)) { |
| XValueNodeImpl[] selectedNodes = getSelectedNodes(XValueNodeImpl.class, null); |
| if (selectedNodes.length == 1 && selectedNodes[0].getFullValueEvaluator() == null) { |
| return selectedNodes[0].getRawValue(); |
| } |
| } |
| return null; |
| } |
| |
| public void rebuildAndRestore(final XDebuggerTreeState treeState) { |
| Object rootNode = myTreeModel.getRoot(); |
| if (rootNode instanceof XDebuggerTreeNode) { |
| ((XDebuggerTreeNode)rootNode).clearChildren(); |
| treeState.restoreState(this); |
| repaint(); |
| } |
| } |
| |
| public void childrenLoaded(final @NotNull XDebuggerTreeNode node, |
| final @NotNull List<XValueContainerNode<?>> children, |
| final boolean last) { |
| for (XDebuggerTreeListener listener : myListeners) { |
| listener.childrenLoaded(node, children, last); |
| } |
| } |
| |
| public void nodeLoaded(final @NotNull RestorableStateNode node, final @NotNull String name) { |
| for (XDebuggerTreeListener listener : myListeners) { |
| listener.nodeLoaded(node, name); |
| } |
| } |
| |
| public void markNodesObsolete() { |
| Object root = myTreeModel.getRoot(); |
| if (root instanceof XValueContainerNode<?>) { |
| markNodesObsolete((XValueContainerNode<?>)root); |
| } |
| } |
| |
| @Override |
| public void dispose() { |
| ActionManager actionManager = ActionManager.getInstance(); |
| actionManager.getAction(XDebuggerActions.SET_VALUE).unregisterCustomShortcutSet(this); |
| actionManager.getAction(XDebuggerActions.COPY_VALUE).unregisterCustomShortcutSet(this); |
| actionManager.getAction(XDebuggerActions.JUMP_TO_SOURCE).unregisterCustomShortcutSet(this); |
| actionManager.getAction(XDebuggerActions.JUMP_TO_TYPE_SOURCE).unregisterCustomShortcutSet(this); |
| actionManager.getAction(XDebuggerActions.MARK_OBJECT).unregisterCustomShortcutSet(this); |
| } |
| |
| private void registerShortcuts() { |
| ActionManager actionManager = ActionManager.getInstance(); |
| actionManager.getAction(XDebuggerActions.SET_VALUE) |
| .registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0)), this); |
| actionManager.getAction(XDebuggerActions.COPY_VALUE).registerCustomShortcutSet(CommonShortcuts.getCopy(), this); |
| actionManager.getAction(XDebuggerActions.JUMP_TO_SOURCE).registerCustomShortcutSet(CommonShortcuts.getEditSource(), this); |
| Shortcut[] editTypeShortcuts = KeymapManager.getInstance().getActiveKeymap().getShortcuts(XDebuggerActions.EDIT_TYPE_SOURCE); |
| actionManager.getAction(XDebuggerActions.JUMP_TO_TYPE_SOURCE).registerCustomShortcutSet(new CustomShortcutSet(editTypeShortcuts), this); |
| actionManager.getAction(XDebuggerActions.MARK_OBJECT) |
| .registerCustomShortcutSet(new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts("ToggleBookmark")), this); |
| } |
| |
| private static void markNodesObsolete(final XValueContainerNode<?> node) { |
| node.setObsolete(); |
| List<? extends XValueContainerNode<?>> loadedChildren = node.getLoadedChildren(); |
| if (loadedChildren != null) { |
| for (XValueContainerNode<?> child : loadedChildren) { |
| markNodesObsolete(child); |
| } |
| } |
| } |
| |
| @Nullable |
| public static XDebuggerTree getTree(final AnActionEvent e) { |
| return e.getData(XDEBUGGER_TREE_KEY); |
| } |
| |
| @Nullable |
| public static XDebuggerTree getTree(DataContext context) { |
| return XDEBUGGER_TREE_KEY.getData(context); |
| } |
| |
| public TransferToEDTQueue<Runnable> getLaterInvocator() { |
| return myLaterInvocator; |
| } |
| |
| public void expandNodesOnLoad(final Condition<TreeNode> nodeFilter) { |
| addTreeListener(new XDebuggerTreeListener() { |
| @Override |
| public void nodeLoaded(@NotNull RestorableStateNode node, String name) { |
| if (nodeFilter.value(node) && !node.isLeaf()) { |
| // cause children computing |
| node.getChildCount(); |
| } |
| } |
| |
| @Override |
| public void childrenLoaded(@NotNull XDebuggerTreeNode node, @NotNull List<XValueContainerNode<?>> children, boolean last) { |
| if (nodeFilter.value(node)) { |
| expandPath(node.getPath()); |
| } |
| } |
| }); |
| } |
| } |