TreeUi: auto-expand nodes fixed and "loading" node is back again
[fedora-idea.git] / platform-api / src / com / intellij / ui / treeStructure / Tree.java
blob0be29e5d18ab7bd891e61d36b3c6f27f15fcf86e
1 /*
2 * Copyright 2000-2007 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.TestableUi;
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.util.ui.AsyncProcessIcon;
25 import com.intellij.util.ui.UIUtil;
26 import org.jetbrains.annotations.Nullable;
28 import javax.swing.*;
29 import javax.swing.plaf.TreeUI;
30 import javax.swing.plaf.basic.BasicTreeUI;
31 import javax.swing.text.Position;
32 import javax.swing.tree.*;
33 import java.awt.*;
34 import java.awt.dnd.Autoscroll;
35 import java.awt.event.FocusAdapter;
36 import java.awt.event.FocusEvent;
37 import java.awt.event.MouseAdapter;
38 import java.awt.event.MouseEvent;
39 import java.lang.reflect.Array;
40 import java.util.ArrayList;
41 import java.util.Map;
43 public class Tree extends JTree implements Autoscroll, TestableUi {
45 private AsyncProcessIcon myBusyIcon;
46 private boolean myBusy;
47 private Rectangle myLastVisibleRec;
49 public Tree() {
50 initTree_();
53 public Tree(TreeModel treemodel) {
54 super(treemodel);
55 initTree_();
58 public Tree(TreeNode root) {
59 super(root);
60 initTree_();
63 private void initTree_() {
64 addMouseListener(new MyMouseListener());
65 if (Patches.SUN_BUG_ID_4893787) {
66 addFocusListener(new MyFocusListener());
69 addFocusListener(new SelectionFixer());
71 setCellRenderer(new NodeRenderer());
74 @Override
75 public void addNotify() {
76 super.addNotify();
78 updateBusy();
81 @Override
82 public void removeNotify() {
83 super.removeNotify();
85 if (myBusyIcon != null) {
86 Disposer.dispose(myBusyIcon);
87 myBusyIcon = null;
91 @Override
92 public void doLayout() {
93 super.doLayout();
95 updateBusyIconLocation();
98 private void updateBusyIconLocation() {
99 if (myBusyIcon != null) {
100 final Rectangle rec = getVisibleRect();
102 final Dimension iconSize = myBusyIcon.getPreferredSize();
104 final Rectangle newBounds = new Rectangle(rec.x + rec.width - iconSize.width, rec.y, iconSize.width, iconSize.height);
105 if (!newBounds.equals(myBusyIcon.getBounds())) {
106 myBusyIcon.setBounds(newBounds);
107 repaint();
113 * Hack to prevent loosing multiple selection on Mac when clicking Ctrl+Left Mouse Button.
114 * See faulty code at BasicTreeUI.selectPathForEvent():2245
116 * @param e
118 protected void processMouseEvent(MouseEvent e) {
119 if (SystemInfo.isMac) {
120 if (SwingUtilities.isLeftMouseButton(e) && e.isControlDown() && e.getID() == MouseEvent.MOUSE_PRESSED) {
121 int modifiers = (e.getModifiers() & ~(MouseEvent.CTRL_MASK | MouseEvent.BUTTON1_MASK)) | MouseEvent.BUTTON3_MASK;
122 e = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), modifiers, e.getX(), e.getY(), e.getClickCount(), true,
123 MouseEvent.BUTTON3);
126 super.processMouseEvent(e);
130 * Disable Sun's speedsearch
132 public TreePath getNextMatch(String prefix, int startingRow, Position.Bias bias) {
133 return null;
136 private static final int AUTOSCROLL_MARGIN = 10;
138 public Insets getAutoscrollInsets() {
139 return new Insets(getLocation().y + AUTOSCROLL_MARGIN, 0, getParent().getHeight() - AUTOSCROLL_MARGIN, getWidth() - 1);
142 public void autoscroll(Point p) {
143 int realrow = getClosestRowForLocation(p.x, p.y);
144 if (getLocation().y + p.y <= AUTOSCROLL_MARGIN) {
145 if (realrow >= 1) realrow--;
147 else {
148 if (realrow < getRowCount() - 1) realrow++;
150 scrollRowToVisible(realrow);
153 @Override
154 protected void paintComponent(Graphics g) {
155 g.setColor(getBackground());
156 g.fillRect(0, 0, getWidth(), getHeight());
158 paintNodeContent(g);
160 super.paintComponent(g);
163 protected boolean highlightSingleNode() {
164 return true;
167 private void paintNodeContent(Graphics g) {
168 if (!(getUI() instanceof BasicTreeUI)) return;
170 final AbstractTreeBuilder builder = AbstractTreeBuilder.getBuilderFor(this);
171 if (builder == null || builder.isDisposed()) return;
173 GraphicsConfig config = new GraphicsConfig(g);
174 config.setAntialiasing(true);
176 final AbstractTreeStructure structure = builder.getTreeStructure();
178 for (int eachRow = 0; eachRow < getRowCount(); eachRow++) {
179 final TreePath path = getPathForRow(eachRow);
180 PresentableNodeDescriptor node = toPresentableNode(path.getLastPathComponent());
181 if (node == null) continue;
183 if (!node.isContentHighlighted()) continue;
185 if (highlightSingleNode()) {
186 if (node.isContentHighlighted()) {
187 final TreePath nodePath = getPath(node);
189 Rectangle rect;
191 final Rectangle parentRect = getPathBounds(nodePath);
192 if (isExpanded(nodePath)) {
193 final int[] max = getMax(node, structure);
194 rect = new Rectangle(parentRect.x, parentRect.y, Math.max((int) parentRect.getMaxX(), max[1]) - parentRect.x - 1,
195 Math.max((int) parentRect.getMaxY(), max[0]) - parentRect.y - 1);
197 else {
198 rect = parentRect;
201 if (rect != null) {
202 final Color highlightColor = node.getHighlightColor();
203 g.setColor(highlightColor);
204 g.fillRoundRect(rect.x, rect.y, rect.width, rect.height, 4, 4);
205 g.setColor(highlightColor.darker());
206 g.drawRoundRect(rect.x, rect.y, rect.width, rect.height, 4, 4);
210 else {
211 //todo: to investigate why it might happen under 1.6: http://www.productiveme.net:8080/browse/PM-217
212 if (node.getParentDescriptor() == null) continue;
214 final Object[] kids = structure.getChildElements(node);
215 if (kids.length == 0) continue;
217 PresentableNodeDescriptor first = null;
218 PresentableNodeDescriptor last = null;
219 int lastIndex = -1;
220 for (int i = 0; i < kids.length; i++) {
221 final Object kid = kids[i];
222 if (kid instanceof PresentableNodeDescriptor) {
223 PresentableNodeDescriptor eachKid = (PresentableNodeDescriptor) kid;
224 if (!node.isHighlightableContentNode(eachKid)) continue;
225 if (first == null) {
226 first = eachKid;
228 last = eachKid;
229 lastIndex = i;
233 if (first == null || last == null) continue;
234 Rectangle firstBounds = getPathBounds(getPath(first));
236 if (isExpanded(getPath(last))) {
237 if (lastIndex + 1 < kids.length) {
238 final Object child = kids[lastIndex + 1];
239 if (child instanceof PresentableNodeDescriptor) {
240 PresentableNodeDescriptor nextKid = (PresentableNodeDescriptor) child;
241 int nextRow = getRowForPath(getPath(nextKid));
242 last = toPresentableNode(getPathForRow(nextRow - 1).getLastPathComponent());
245 else {
246 NodeDescriptor parentNode = node.getParentDescriptor();
247 if (parentNode instanceof PresentableNodeDescriptor) {
248 final PresentableNodeDescriptor ppd = (PresentableNodeDescriptor)parentNode;
249 int nodeIndex = node.getIndex();
250 if (nodeIndex + 1 < structure.getChildElements(ppd).length) {
251 PresentableNodeDescriptor nextChild = ppd.getChildToHighlightAt(nodeIndex + 1);
252 int nextRow = getRowForPath(getPath(nextChild));
253 last = toPresentableNode(getPathForRow(nextRow - 1).getLastPathComponent());
255 else {
256 int lastRow = getRowForPath(getPath(last));
257 PresentableNodeDescriptor lastParent = last;
258 boolean lastWasFound = false;
259 for (int i = lastRow + 1; i < getRowCount(); i++) {
260 PresentableNodeDescriptor eachNode = toPresentableNode(getPathForRow(i).getLastPathComponent());
261 if (!node.isParentOf(eachNode)) {
262 last = lastParent;
263 lastWasFound = true;
264 break;
266 lastParent = eachNode;
268 if (!lastWasFound) {
269 last = toPresentableNode(getPathForRow(getRowCount() - 1).getLastPathComponent());
276 Rectangle lastBounds = getPathBounds(getPath(last));
278 if (firstBounds == null || lastBounds == null) continue;
280 Rectangle toPaint = new Rectangle(firstBounds.x, firstBounds.y, 0, (int)lastBounds.getMaxY() - firstBounds.y - 1);
282 toPaint.width = getWidth() - toPaint.x - 4;
284 final Color highlightColor = first.getHighlightColor();
285 g.setColor(highlightColor);
286 g.fillRoundRect(toPaint.x, toPaint.y, toPaint.width, toPaint.height, 4, 4);
287 g.setColor(highlightColor.darker());
288 g.drawRoundRect(toPaint.x, toPaint.y, toPaint.width, toPaint.height, 4, 4);
292 config.restore();
295 private int[] getMax(final PresentableNodeDescriptor node, final AbstractTreeStructure structure) {
296 int x = 0;
297 int y = 0;
298 final Object[] children = structure.getChildElements(node);
299 for (final Object child : children) {
300 if (child instanceof PresentableNodeDescriptor) {
301 final TreePath childPath = getPath((PresentableNodeDescriptor)child);
302 if (childPath != null) {
303 if (isExpanded(childPath)) {
304 final int[] tmp = getMax((PresentableNodeDescriptor)child, structure);
305 y = Math.max(y, tmp[0]);
306 x = Math.max(x, tmp[1]);
309 final Rectangle r = getPathBounds(childPath);
310 if (r != null) {
311 y = Math.max(y, (int)r.getMaxY());
312 x = Math.max(x, (int)r.getMaxX());
318 return new int[]{y, x};
321 @Nullable
322 private static PresentableNodeDescriptor toPresentableNode(final Object pathComponent) {
323 if (!(pathComponent instanceof DefaultMutableTreeNode)) return null;
324 final Object userObject = ((DefaultMutableTreeNode)pathComponent).getUserObject();
325 if (!(userObject instanceof PresentableNodeDescriptor)) return null;
326 return (PresentableNodeDescriptor)userObject;
329 public TreePath getPath(PresentableNodeDescriptor node) {
330 final AbstractTreeBuilder builder = AbstractTreeBuilder.getBuilderFor(this);
331 final DefaultMutableTreeNode treeNode = builder.getNodeForElement(node);
333 return treeNode != null ? new TreePath(treeNode.getPath()) : new TreePath(node);
336 private class MyMouseListener extends MouseAdapter {
337 public void mousePressed(MouseEvent mouseevent) {
338 if (!SwingUtilities.isLeftMouseButton(mouseevent) &&
339 (SwingUtilities.isRightMouseButton(mouseevent) || SwingUtilities.isMiddleMouseButton(mouseevent))) {
340 TreePath treepath = getPathForLocation(mouseevent.getX(), mouseevent.getY());
341 if (treepath != null) {
342 if (getSelectionModel().getSelectionMode() != TreeSelectionModel.SINGLE_TREE_SELECTION) {
343 TreePath[] selectionPaths = getSelectionModel().getSelectionPaths();
344 if (selectionPaths != null) {
345 for (TreePath selectionPath : selectionPaths) {
346 if (selectionPath == treepath) return;
350 getSelectionModel().setSelectionPath(treepath);
357 * This is patch for 4893787 SUN bug. The problem is that the BasicTreeUI.FocusHandler repaints
358 * only lead selection index on focus changes. It's a problem with multiple selected nodes.
360 private class MyFocusListener extends FocusAdapter {
361 private void focusChanges() {
362 TreePath[] paths = getSelectionPaths();
363 if (paths != null) {
364 TreeUI ui = getUI();
365 for (int i = paths.length - 1; i >= 0; i--) {
366 Rectangle bounds = ui.getPathBounds(Tree.this, paths[i]);
367 if (bounds != null) {
368 repaint(bounds);
374 public void focusGained(FocusEvent e) {
375 focusChanges();
378 public void focusLost(FocusEvent e) {
379 focusChanges();
383 private class SelectionFixer extends FocusAdapter {
384 public void focusGained(final FocusEvent e) {
385 final TreePath[] paths = getSelectionPaths();
386 if (paths == null || paths.length == 0) {
387 for (int eachRow = 0; eachRow < getRowCount(); eachRow++) {
388 final TreePath path = getPathForRow(eachRow);
389 if (path != null && isVisible(path)) {
390 setSelectionPath(path);
391 break;
398 public final void setLineStyleAngled() {
399 UIUtil.setLineStyleAngled(this);
402 public <T> T[] getSelectedNodes(Class<T> nodeType, @Nullable NodeFilter<T> filter) {
403 TreePath[] paths = getSelectionPaths();
404 if (paths == null) return (T[])Array.newInstance(nodeType, 0);
406 ArrayList<T> nodes = new ArrayList<T>();
407 for (int i = 0; i < paths.length; i++) {
408 Object last = paths[i].getLastPathComponent();
409 if (nodeType.isAssignableFrom(last.getClass())) {
410 if (filter != null && !filter.accept((T)last)) continue;
411 nodes.add((T)last);
414 T[] result = (T[])Array.newInstance(nodeType, nodes.size());
415 nodes.toArray(result);
416 return result;
419 public static interface NodeFilter<T> {
420 boolean accept(T node);
423 public void putInfo(Map<String, String> info) {
424 final TreePath[] selection = getSelectionPaths();
425 if (selection == null) return;
427 StringBuffer nodesText = new StringBuffer();
429 for (TreePath eachPath : selection) {
430 final Object eachNode = eachPath.getLastPathComponent();
431 final Component c =
432 getCellRenderer().getTreeCellRendererComponent(this, eachNode, false, false, false, getRowForPath(eachPath), false);
434 if (c != null) {
435 if (nodesText.length() > 0) {
436 nodesText.append(";");
438 nodesText.append(c.toString());
442 if (nodesText.length() > 0) {
443 info.put("selectedNodes", nodesText.toString());
447 public void setPaintBusy(boolean paintBusy) {
448 if (myBusy == paintBusy) return;
450 myBusy = paintBusy;
451 updateBusy();
454 @Override
455 public void paint(Graphics g) {
456 super.paint(g);
458 final Rectangle visible = getVisibleRect();
460 if (!visible.equals(myLastVisibleRec)) {
461 updateBusyIconLocation();
464 myLastVisibleRec = visible;
467 private void updateBusy() {
468 if (myBusy) {
469 if (myBusyIcon == null) {
470 myBusyIcon = new AsyncProcessIcon(toString());
471 myBusyIcon.setPaintPassiveIcon(false);
472 add(myBusyIcon);
476 if (myBusyIcon != null) {
477 if (myBusy) {
478 myBusyIcon.resume();
479 } else {
480 myBusyIcon.suspend();
481 SwingUtilities.invokeLater(new Runnable() {
482 public void run() {
483 if (myBusyIcon != null) {
484 repaint();
489 updateBusyIconLocation();