2 * Copyright 2000-2010 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.ui.treeStructure;
18 import com.intellij.Patches;
19 import com.intellij.ide.util.treeView.*;
20 import com.intellij.openapi.ui.Queryable;
21 import com.intellij.openapi.util.Disposer;
22 import com.intellij.openapi.util.SystemInfo;
23 import com.intellij.openapi.wm.impl.content.GraphicsConfig;
24 import com.intellij.ui.ComponentWithExpandableItems;
25 import com.intellij.ui.ExpandableItemsHandler;
26 import com.intellij.ui.ExpandableItemsHandlerFactory;
27 import com.intellij.ui.SimpleTextAttributes;
28 import com.intellij.util.ui.AsyncProcessIcon;
29 import com.intellij.util.ui.ComponentWithEmptyText;
30 import com.intellij.util.ui.EmptyTextHelper;
31 import com.intellij.util.ui.UIUtil;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
36 import javax.swing.event.TreeSelectionEvent;
37 import javax.swing.plaf.TreeUI;
38 import javax.swing.plaf.basic.BasicTreeUI;
39 import javax.swing.text.Position;
40 import javax.swing.tree.*;
42 import java.awt.dnd.Autoscroll;
43 import java.awt.event.*;
44 import java.lang.reflect.Array;
45 import java.util.ArrayList;
48 public class Tree extends JTree implements ComponentWithEmptyText, ComponentWithExpandableItems<Integer>, Autoscroll, Queryable {
49 private EmptyTextHelper myEmptyTextHelper;
50 private ExpandableItemsHandler<Integer> myExpandableItemsHandler;
52 private AsyncProcessIcon myBusyIcon;
53 private boolean myBusy;
54 private Rectangle myLastVisibleRec;
56 private Dimension myHoldSize;
57 private final MySelectionModel mySelectionModel = new MySelectionModel();
63 public Tree(TreeModel treemodel) {
68 public Tree(TreeNode root) {
73 private void initTree_() {
74 myEmptyTextHelper = new EmptyTextHelper(this) {
76 protected boolean isEmpty() {
77 return Tree.this.isEmpty();
81 myExpandableItemsHandler = ExpandableItemsHandlerFactory.install(this);
83 addMouseListener(new MyMouseListener());
84 if (Patches.SUN_BUG_ID_4893787) {
85 addFocusListener(new MyFocusListener());
88 setCellRenderer(new NodeRenderer());
90 setSelectionModel(mySelectionModel);
94 public void setUI(final TreeUI ui) {
96 if (SystemInfo.isMac && !isCustomUI() && UIUtil.isUnderAquaLookAndFeel() && !(ui instanceof UIUtil.MacTreeUI)) {
97 actualUI = new UIUtil.MacTreeUI(isMacWideSelection());
100 super.setUI(actualUI);
103 public boolean isEmpty() {
104 TreeModel model = getModel();
105 if (model == null) return true;
106 if (model.getRoot() == null) return true;
107 return !isRootVisible() && model.getChildCount(model.getRoot()) == 0;
110 protected boolean isCustomUI() {
114 protected boolean isMacWideSelection() {
118 public String getEmptyText() {
119 return myEmptyTextHelper.getEmptyText();
122 public void setEmptyText(String emptyText) {
123 myEmptyTextHelper.setEmptyText(emptyText);
126 public void setEmptyText(String emptyText, SimpleTextAttributes attrs) {
127 myEmptyTextHelper.setEmptyText(emptyText, attrs);
130 public void clearEmptyText() {
131 myEmptyTextHelper.clearEmptyText();
134 public void appendEmptyText(String text, SimpleTextAttributes attrs) {
135 myEmptyTextHelper.appendEmptyText(text, attrs);
138 public void appendEmptyText(String text, SimpleTextAttributes attrs, ActionListener listener) {
139 myEmptyTextHelper.appendEmptyText(text, attrs, listener);
143 public ExpandableItemsHandler<Integer> getExpandableItemsHandler() {
144 return myExpandableItemsHandler;
148 public void addNotify() {
155 public void removeNotify() {
156 super.removeNotify();
158 if (myBusyIcon != null) {
160 Disposer.dispose(myBusyIcon);
166 public void doLayout() {
169 updateBusyIconLocation();
172 private void updateBusyIconLocation() {
173 if (myBusyIcon != null) {
174 final Rectangle rec = getVisibleRect();
176 final Dimension iconSize = myBusyIcon.getPreferredSize();
178 final Rectangle newBounds = new Rectangle(rec.x + rec.width - iconSize.width, rec.y, iconSize.width, iconSize.height);
179 if (!newBounds.equals(myBusyIcon.getBounds())) {
180 myBusyIcon.setBounds(newBounds);
187 public void paint(Graphics g) {
188 final Rectangle visible = getVisibleRect();
190 if (!AbstractTreeBuilder.isToPaintSelection(this)) {
191 mySelectionModel.holdSelection();
197 if (!visible.equals(myLastVisibleRec)) {
198 updateBusyIconLocation();
201 myLastVisibleRec = visible;
204 mySelectionModel.unholdSelection();
208 public void setPaintBusy(boolean paintBusy) {
209 if (myBusy == paintBusy) return;
215 private void updateBusy() {
217 if (myBusyIcon == null) {
218 myBusyIcon = new AsyncProcessIcon(toString()).setUseMask(false);
219 myBusyIcon.setOpaque(false);
220 myBusyIcon.setPaintPassiveIcon(false);
222 myBusyIcon.addMouseListener(new MouseAdapter() {
224 public void mousePressed(MouseEvent e) {
225 if (!UIUtil.isActionClick(e)) return;
226 AbstractTreeBuilder builder = AbstractTreeBuilder.getBuilderFor(Tree.this);
227 if (builder != null) {
228 builder.cancelUpdate();
235 if (myBusyIcon != null) {
238 myBusyIcon.setToolTipText("Update is in progress. Click to cancel");
241 myBusyIcon.suspend();
242 myBusyIcon.setToolTipText(null);
243 SwingUtilities.invokeLater(new Runnable() {
245 if (myBusyIcon != null) {
251 updateBusyIconLocation();
255 protected boolean paintNodes() {
260 protected void paintComponent(Graphics g) {
262 g.setColor(getBackground());
263 g.fillRect(0, 0, getWidth(), getHeight());
268 super.paintComponent(g);
269 myEmptyTextHelper.paint(g);
273 * Hack to prevent loosing multiple selection on Mac when clicking Ctrl+Left Mouse Button.
274 * See faulty code at BasicTreeUI.selectPathForEvent():2245
278 protected void processMouseEvent(MouseEvent e) {
279 if (SystemInfo.isMac) {
280 if (SwingUtilities.isLeftMouseButton(e) && e.isControlDown() && e.getID() == MouseEvent.MOUSE_PRESSED) {
281 int modifiers = (e.getModifiers() & ~(MouseEvent.CTRL_MASK | MouseEvent.BUTTON1_MASK)) | MouseEvent.BUTTON3_MASK;
282 e = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), modifiers, e.getX(), e.getY(), e.getClickCount(), true,
286 super.processMouseEvent(e);
290 * Disable Sun's speedsearch
292 public TreePath getNextMatch(String prefix, int startingRow, Position.Bias bias) {
296 private static final int AUTOSCROLL_MARGIN = 10;
298 public Insets getAutoscrollInsets() {
299 return new Insets(getLocation().y + AUTOSCROLL_MARGIN, 0, getParent().getHeight() - AUTOSCROLL_MARGIN, getWidth() - 1);
302 public void autoscroll(Point p) {
303 int realrow = getClosestRowForLocation(p.x, p.y);
304 if (getLocation().y + p.y <= AUTOSCROLL_MARGIN) {
305 if (realrow >= 1) realrow--;
308 if (realrow < getRowCount() - 1) realrow++;
310 scrollRowToVisible(realrow);
313 protected boolean highlightSingleNode() {
317 private void paintNodeContent(Graphics g) {
318 if (!(getUI() instanceof BasicTreeUI)) return;
320 final AbstractTreeBuilder builder = AbstractTreeBuilder.getBuilderFor(this);
321 if (builder == null || builder.isDisposed()) return;
323 GraphicsConfig config = new GraphicsConfig(g);
324 config.setAntialiasing(true);
326 final AbstractTreeStructure structure = builder.getTreeStructure();
328 for (int eachRow = 0; eachRow < getRowCount(); eachRow++) {
329 final TreePath path = getPathForRow(eachRow);
330 PresentableNodeDescriptor node = toPresentableNode(path.getLastPathComponent());
331 if (node == null) continue;
333 if (!node.isContentHighlighted()) continue;
335 if (highlightSingleNode()) {
336 if (node.isContentHighlighted()) {
337 final TreePath nodePath = getPath(node);
341 final Rectangle parentRect = getPathBounds(nodePath);
342 if (isExpanded(nodePath)) {
343 final int[] max = getMax(node, structure);
344 rect = new Rectangle(parentRect.x, parentRect.y, Math.max((int) parentRect.getMaxX(), max[1]) - parentRect.x - 1,
345 Math.max((int) parentRect.getMaxY(), max[0]) - parentRect.y - 1);
352 final Color highlightColor = node.getHighlightColor();
353 g.setColor(highlightColor);
354 g.fillRoundRect(rect.x, rect.y, rect.width, rect.height, 4, 4);
355 g.setColor(highlightColor.darker());
356 g.drawRoundRect(rect.x, rect.y, rect.width, rect.height, 4, 4);
361 //todo: to investigate why it might happen under 1.6: http://www.productiveme.net:8080/browse/PM-217
362 if (node.getParentDescriptor() == null) continue;
364 final Object[] kids = structure.getChildElements(node);
365 if (kids.length == 0) continue;
367 PresentableNodeDescriptor first = null;
368 PresentableNodeDescriptor last = null;
370 for (int i = 0; i < kids.length; i++) {
371 final Object kid = kids[i];
372 if (kid instanceof PresentableNodeDescriptor) {
373 PresentableNodeDescriptor eachKid = (PresentableNodeDescriptor) kid;
374 if (!node.isHighlightableContentNode(eachKid)) continue;
383 if (first == null || last == null) continue;
384 Rectangle firstBounds = getPathBounds(getPath(first));
386 if (isExpanded(getPath(last))) {
387 if (lastIndex + 1 < kids.length) {
388 final Object child = kids[lastIndex + 1];
389 if (child instanceof PresentableNodeDescriptor) {
390 PresentableNodeDescriptor nextKid = (PresentableNodeDescriptor) child;
391 int nextRow = getRowForPath(getPath(nextKid));
392 last = toPresentableNode(getPathForRow(nextRow - 1).getLastPathComponent());
396 NodeDescriptor parentNode = node.getParentDescriptor();
397 if (parentNode instanceof PresentableNodeDescriptor) {
398 final PresentableNodeDescriptor ppd = (PresentableNodeDescriptor)parentNode;
399 int nodeIndex = node.getIndex();
400 if (nodeIndex + 1 < structure.getChildElements(ppd).length) {
401 PresentableNodeDescriptor nextChild = ppd.getChildToHighlightAt(nodeIndex + 1);
402 int nextRow = getRowForPath(getPath(nextChild));
403 TreePath prevPath = getPathForRow(nextRow - 1);
404 if (prevPath != null) {
405 last = toPresentableNode(prevPath.getLastPathComponent());
409 int lastRow = getRowForPath(getPath(last));
410 PresentableNodeDescriptor lastParent = last;
411 boolean lastWasFound = false;
412 for (int i = lastRow + 1; i < getRowCount(); i++) {
413 PresentableNodeDescriptor eachNode = toPresentableNode(getPathForRow(i).getLastPathComponent());
414 if (!node.isParentOf(eachNode)) {
419 lastParent = eachNode;
422 last = toPresentableNode(getPathForRow(getRowCount() - 1).getLastPathComponent());
429 if (last == null) continue;
430 Rectangle lastBounds = getPathBounds(getPath(last));
432 if (firstBounds == null || lastBounds == null) continue;
434 Rectangle toPaint = new Rectangle(firstBounds.x, firstBounds.y, 0, (int)lastBounds.getMaxY() - firstBounds.y - 1);
436 toPaint.width = getWidth() - toPaint.x - 4;
438 final Color highlightColor = first.getHighlightColor();
439 g.setColor(highlightColor);
440 g.fillRoundRect(toPaint.x, toPaint.y, toPaint.width, toPaint.height, 4, 4);
441 g.setColor(highlightColor.darker());
442 g.drawRoundRect(toPaint.x, toPaint.y, toPaint.width, toPaint.height, 4, 4);
449 private int[] getMax(final PresentableNodeDescriptor node, final AbstractTreeStructure structure) {
452 final Object[] children = structure.getChildElements(node);
453 for (final Object child : children) {
454 if (child instanceof PresentableNodeDescriptor) {
455 final TreePath childPath = getPath((PresentableNodeDescriptor)child);
456 if (childPath != null) {
457 if (isExpanded(childPath)) {
458 final int[] tmp = getMax((PresentableNodeDescriptor)child, structure);
459 y = Math.max(y, tmp[0]);
460 x = Math.max(x, tmp[1]);
463 final Rectangle r = getPathBounds(childPath);
465 y = Math.max(y, (int)r.getMaxY());
466 x = Math.max(x, (int)r.getMaxX());
472 return new int[]{y, x};
476 private static PresentableNodeDescriptor toPresentableNode(final Object pathComponent) {
477 if (!(pathComponent instanceof DefaultMutableTreeNode)) return null;
478 final Object userObject = ((DefaultMutableTreeNode)pathComponent).getUserObject();
479 if (!(userObject instanceof PresentableNodeDescriptor)) return null;
480 return (PresentableNodeDescriptor)userObject;
483 public TreePath getPath(PresentableNodeDescriptor node) {
484 final AbstractTreeBuilder builder = AbstractTreeBuilder.getBuilderFor(this);
485 final DefaultMutableTreeNode treeNode = builder.getNodeForElement(node);
487 return treeNode != null ? new TreePath(treeNode.getPath()) : new TreePath(node);
490 private static class MySelectionModel extends DefaultTreeSelectionModel {
492 private TreePath[] myHeldSelection;
495 protected void fireValueChanged(TreeSelectionEvent e) {
496 if (myHeldSelection == null) {
497 super.fireValueChanged(e);
501 public void holdSelection() {
502 myHeldSelection = getSelectionPaths();
506 public void unholdSelection() {
507 if (myHeldSelection != null) {
508 setSelectionPaths(myHeldSelection);
509 myHeldSelection = null;
514 private class MyMouseListener extends MouseAdapter {
515 public void mousePressed(MouseEvent mouseevent) {
516 if (!SwingUtilities.isLeftMouseButton(mouseevent) &&
517 (SwingUtilities.isRightMouseButton(mouseevent) || SwingUtilities.isMiddleMouseButton(mouseevent))) {
518 TreePath treepath = getPathForLocation(mouseevent.getX(), mouseevent.getY());
519 if (treepath != null) {
520 if (getSelectionModel().getSelectionMode() != TreeSelectionModel.SINGLE_TREE_SELECTION) {
521 TreePath[] selectionPaths = getSelectionModel().getSelectionPaths();
522 if (selectionPaths != null) {
523 for (TreePath selectionPath : selectionPaths) {
524 if (selectionPath != null && selectionPath.equals(treepath)) return;
528 getSelectionModel().setSelectionPath(treepath);
535 * This is patch for 4893787 SUN bug. The problem is that the BasicTreeUI.FocusHandler repaints
536 * only lead selection index on focus changes. It's a problem with multiple selected nodes.
538 private class MyFocusListener extends FocusAdapter {
539 private void focusChanges() {
540 TreePath[] paths = getSelectionPaths();
543 for (int i = paths.length - 1; i >= 0; i--) {
544 Rectangle bounds = ui.getPathBounds(Tree.this, paths[i]);
545 if (bounds != null) {
552 public void focusGained(FocusEvent e) {
556 public void focusLost(FocusEvent e) {
561 public final void setLineStyleAngled() {
562 UIUtil.setLineStyleAngled(this);
565 public <T> T[] getSelectedNodes(Class<T> nodeType, @Nullable NodeFilter<T> filter) {
566 TreePath[] paths = getSelectionPaths();
567 if (paths == null) return (T[])Array.newInstance(nodeType, 0);
569 ArrayList<T> nodes = new ArrayList<T>();
570 for (TreePath path : paths) {
571 Object last = path.getLastPathComponent();
572 if (nodeType.isAssignableFrom(last.getClass())) {
573 if (filter != null && !filter.accept((T)last)) continue;
577 T[] result = (T[])Array.newInstance(nodeType, nodes.size());
578 nodes.toArray(result);
582 public interface NodeFilter<T> {
583 boolean accept(T node);
586 public void putInfo(Map<String, String> info) {
587 final TreePath[] selection = getSelectionPaths();
588 if (selection == null) return;
590 StringBuffer nodesText = new StringBuffer();
592 for (TreePath eachPath : selection) {
593 final Object eachNode = eachPath.getLastPathComponent();
595 getCellRenderer().getTreeCellRendererComponent(this, eachNode, false, false, false, getRowForPath(eachPath), false);
598 if (nodesText.length() > 0) {
599 nodesText.append(";");
601 nodesText.append(c.toString());
605 if (nodesText.length() > 0) {
606 info.put("selectedNodes", nodesText.toString());
610 public void setHoldSize(boolean hold) {
611 if (hold && myHoldSize == null) {
612 myHoldSize = getPreferredSize();
613 } else if (!hold && myHoldSize != null) {
619 public Dimension getPreferredSize() {
620 Dimension size = super.getPreferredSize();
622 if (myHoldSize != null) {
623 size.width = Math.max(size.width, myHoldSize.width);
624 size.height = Math.max(size.height, myHoldSize.height);