changedUpdate exception
[fedora-idea.git] / platform / platform-impl / src / com / intellij / ui / MultilineTreeCellRenderer.java
blob3bba1d2bd46a281a7d7544a30a93326b582c7e77
1 /*
2 * Copyright 2000-2009 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;
18 import com.intellij.util.ArrayUtil;
19 import com.intellij.util.ui.UIUtil;
20 import org.jetbrains.annotations.NonNls;
22 import javax.swing.*;
23 import javax.swing.plaf.TreeUI;
24 import javax.swing.tree.DefaultMutableTreeNode;
25 import javax.swing.tree.TreeCellRenderer;
26 import java.awt.*;
27 import java.awt.event.ComponentAdapter;
28 import java.awt.event.ComponentEvent;
29 import java.beans.PropertyChangeEvent;
30 import java.beans.PropertyChangeListener;
31 import java.util.ArrayList;
33 public abstract class MultilineTreeCellRenderer extends JComponent implements javax.swing.tree.TreeCellRenderer {
35 private boolean myWrapsCalculated = false;
36 private boolean myTooSmall = false;
37 private int myHeightCalculated = -1;
38 private int myWrapsCalculatedForWidth = -1;
40 private ArrayList myWraps = new ArrayList();
42 private int myMinHeight = 1;
43 private Insets myTextInsets;
44 private final Insets myLabelInsets = new Insets(1, 2, 1, 2);
46 private boolean mySelected;
47 private boolean myHasFocus;
49 private Icon myIcon;
50 private String[] myLines = ArrayUtil.EMPTY_STRING_ARRAY;
51 private String myPrefix;
52 private int myTextLength;
53 private int myPrefixWidth;
54 @NonNls protected static final String FONT_PROPERTY_NAME = "font";
57 public MultilineTreeCellRenderer() {
58 myTextInsets = new Insets(0,0,0,0);
60 addComponentListener(new ComponentAdapter() {
61 public void componentResized(ComponentEvent e) {
62 onSizeChanged();
64 });
66 addPropertyChangeListener(new PropertyChangeListener() {
67 public void propertyChange(PropertyChangeEvent evt) {
68 if (FONT_PROPERTY_NAME.equalsIgnoreCase(evt.getPropertyName())) {
69 onFontChanged();
72 });
76 protected void setMinHeight(int height) {
77 myMinHeight = height;
78 myHeightCalculated = Math.max(myMinHeight, myHeightCalculated);
81 protected void setTextInsets(Insets textInsets) {
82 myTextInsets = textInsets;
83 onSizeChanged();
86 private void onFontChanged() {
87 myWrapsCalculated = false;
90 private void onSizeChanged() {
91 int currWidth = getWidth();
92 if (currWidth != myWrapsCalculatedForWidth) {
93 myWrapsCalculated = false;
94 myHeightCalculated = -1;
95 myWrapsCalculatedForWidth = -1;
99 private FontMetrics getCurrFontMetrics() {
100 return getFontMetrics(getFont());
103 public void paint(Graphics g) {
104 int height = getHeight();
105 int width = getWidth();
106 int borderX = myLabelInsets.left - 1;
107 int borderY = myLabelInsets.top - 1;
108 int borderW = width - borderX - myLabelInsets.right + 2;
109 int borderH = height - borderY - myLabelInsets.bottom + 1;
111 if (myIcon != null) {
112 int verticalIconPosition = (height - myIcon.getIconHeight())/2;
113 myIcon.paintIcon(this, g, 0, verticalIconPosition);
114 borderX += myIcon.getIconWidth();
115 borderW -= myIcon.getIconWidth();
118 Color bgColor;
119 Color fgColor;
120 if (mySelected && myHasFocus){
121 bgColor = UIUtil.getTreeSelectionBackground();
122 fgColor = UIUtil.getTreeSelectionForeground();
124 else{
125 bgColor = UIUtil.getTreeTextBackground();
126 fgColor = getForeground();
129 // fill background
130 g.setColor(bgColor);
131 g.fillRect(borderX, borderY, borderW, borderH);
133 // draw border
134 if (mySelected) {
135 g.setColor(UIUtil.getTreeSelectionBorderColor());
136 UIUtil.drawDottedRectangle(g, borderX, borderY, borderX + borderW - 1, borderY + borderH - 1);
139 // paint text
140 recalculateWraps();
142 if (myTooSmall) { // TODO ???
143 return;
146 int fontHeight = getCurrFontMetrics().getHeight();
147 int currBaseLine = getCurrFontMetrics().getAscent();
148 currBaseLine += myTextInsets.top;
149 g.setFont(getFont());
150 g.setColor(fgColor);
151 UIUtil.applyRenderingHints(g);
153 if (myPrefix != null) {
154 g.drawString(myPrefix, myTextInsets.left - myPrefixWidth + 1, currBaseLine);
157 for (int i = 0; i < myWraps.size(); i++) {
158 String currLine = (String)myWraps.get(i);
159 g.drawString(currLine, myTextInsets.left, currBaseLine);
160 currBaseLine += fontHeight; // first is getCurrFontMetrics().getAscent()
164 public void setText(String[] lines, String prefix) {
165 myLines = lines;
166 myTextLength = 0;
167 for (int i = 0; i < lines.length; i++) {
168 myTextLength += lines[i].length();
170 myPrefix = prefix;
172 myWrapsCalculated = false;
173 myHeightCalculated = -1;
174 myWrapsCalculatedForWidth = -1;
177 public void setIcon(Icon icon) {
178 myIcon = icon;
180 myWrapsCalculated = false;
181 myHeightCalculated = -1;
182 myWrapsCalculatedForWidth = -1;
185 public Dimension getMinimumSize() {
186 if (getFont() != null) {
187 int minHeight = getCurrFontMetrics().getHeight();
188 return new Dimension(minHeight, minHeight);
190 return new Dimension(
191 MIN_WIDTH + myTextInsets.left + myTextInsets.right,
192 MIN_WIDTH + myTextInsets.top + myTextInsets.bottom
196 private static final int MIN_WIDTH = 10;
198 // Calculates height for current width.
199 public Dimension getPreferredSize() {
200 recalculateWraps();
201 return new Dimension(myWrapsCalculatedForWidth, myHeightCalculated);
204 // Calculate wraps for the current width
205 private void recalculateWraps() {
206 int currwidth = getWidth();
207 if (myWrapsCalculated) {
208 if (currwidth == myWrapsCalculatedForWidth) {
209 return;
211 else {
212 myWrapsCalculated = false;
215 int wrapsCount = calculateWraps(currwidth);
216 myTooSmall = (wrapsCount == -1);
217 if (myTooSmall) {
218 wrapsCount = myTextLength;
220 int fontHeight = getCurrFontMetrics().getHeight();
221 myHeightCalculated = wrapsCount * fontHeight + myTextInsets.top + myTextInsets.bottom;
222 myHeightCalculated = Math.max(myMinHeight, myHeightCalculated);
224 int maxWidth = 0;
225 for (int i=0; i < myWraps.size(); i++) {
226 String s = (String)myWraps.get(i);
227 int width = getCurrFontMetrics().stringWidth(s);
228 maxWidth = Math.max(maxWidth, width);
231 myWrapsCalculatedForWidth = myTextInsets.left + maxWidth + myTextInsets.right;
232 myWrapsCalculated = true;
235 private int calculateWraps(int width) {
236 myTooSmall = width < MIN_WIDTH;
237 if (myTooSmall) {
238 return -1;
241 int result = 0;
242 myWraps = new ArrayList();
244 for (int i = 0; i < myLines.length; i++) {
245 String aLine = myLines[i];
246 int lineFirstChar = 0;
247 int lineLastChar = aLine.length() - 1;
248 int currFirst = lineFirstChar;
249 int printableWidth = width - myTextInsets.left - myTextInsets.right;
250 if (aLine.length() == 0) {
251 myWraps.add(aLine);
252 result++;
254 else {
255 while (currFirst <= lineLastChar) {
256 int currLast = calculateLastVisibleChar(aLine, printableWidth, currFirst, lineLastChar);
257 if (currLast < lineLastChar) {
258 int currChar = currLast + 1;
259 if (!Character.isWhitespace(aLine.charAt(currChar))) {
260 while (currChar >= currFirst) {
261 if (Character.isWhitespace(aLine.charAt(currChar))) {
262 break;
264 currChar--;
266 if (currChar > currFirst) {
267 currLast = currChar;
271 myWraps.add(aLine.substring(currFirst, currLast + 1));
272 currFirst = currLast + 1;
273 while ((currFirst <= lineLastChar) && (Character.isWhitespace(aLine.charAt(currFirst)))) {
274 currFirst++;
276 result++;
280 return result;
283 private int calculateLastVisibleChar(String line, int viewWidth, int firstChar, int lastChar) {
284 if (firstChar == lastChar) return lastChar;
285 if (firstChar > lastChar) throw new IllegalArgumentException("firstChar=" + firstChar + ", lastChar=" + lastChar);
286 int totalWidth = getCurrFontMetrics().stringWidth(line.substring(firstChar, lastChar + 1));
287 if (totalWidth == 0 || viewWidth > totalWidth) {
288 return lastChar;
290 else {
291 int newApprox = (lastChar - firstChar + 1) * viewWidth / totalWidth;
292 int currChar = firstChar + Math.max(newApprox - 1, 0);
293 int currWidth = getCurrFontMetrics().stringWidth(line.substring(firstChar, currChar + 1));
294 while (true) {
295 if (currWidth > viewWidth) {
296 currChar--;
297 if (currChar <= firstChar) {
298 return firstChar;
300 currWidth -= getCurrFontMetrics().charWidth(line.charAt(currChar + 1));
301 if (currWidth <= viewWidth) {
302 return currChar;
305 else {
306 currChar++;
307 if (currChar > lastChar) {
308 return lastChar;
310 currWidth += getCurrFontMetrics().charWidth(line.charAt(currChar));
311 if (currWidth >= viewWidth) {
312 return currChar - 1;
319 private int getChildIndent(JTree tree) {
320 TreeUI newUI = tree.getUI();
321 if (newUI instanceof javax.swing.plaf.basic.BasicTreeUI) {
322 javax.swing.plaf.basic.BasicTreeUI btreeui = (javax.swing.plaf.basic.BasicTreeUI)newUI;
323 return btreeui.getLeftChildIndent() + btreeui.getRightChildIndent();
325 else {
326 return ((Integer)UIUtil.getTreeLeftChildIndent()).intValue() + ((Integer)UIUtil.getTreeRightChildIndent()).intValue();
330 private int getAvailableWidth(Object forValue, JTree tree) {
331 DefaultMutableTreeNode node = (DefaultMutableTreeNode)forValue;
332 int busyRoom = tree.getInsets().left + tree.getInsets().right + getChildIndent(tree) * node.getLevel();
333 return tree.getVisibleRect().width - busyRoom - 2;
336 protected abstract void initComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus);
338 public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
339 setFont(UIUtil.getTreeFont());
341 initComponent(tree, value, selected, expanded, leaf, row, hasFocus);
343 mySelected = selected;
344 myHasFocus = hasFocus;
346 int availWidth = getAvailableWidth(value, tree);
347 if (availWidth > 0) {
348 setSize(availWidth, 100); // height will be calculated automatically
351 int leftInset = myLabelInsets.left;
353 if (myIcon != null) {
354 leftInset += myIcon.getIconWidth() + 2;
357 if (myPrefix != null) {
358 myPrefixWidth = getCurrFontMetrics().stringWidth(myPrefix) + 5;
359 leftInset += myPrefixWidth;
362 setTextInsets(new Insets(myLabelInsets.top, leftInset, myLabelInsets.bottom, myLabelInsets.right));
363 if (myIcon != null) {
364 setMinHeight(myIcon.getIconHeight());
366 else {
367 setMinHeight(1);
370 setSize(getPreferredSize());
371 recalculateWraps();
373 return this;
376 public static JScrollPane installRenderer(final JTree tree, final MultilineTreeCellRenderer renderer) {
377 final TreeCellRenderer defaultRenderer = tree.getCellRenderer();
379 JScrollPane scrollpane = new JScrollPane(tree){
380 private int myAddRemoveCounter = 0;
381 private boolean myShouldResetCaches = false;
382 public void setSize(Dimension d) {
383 boolean isChanged = getWidth() != d.width || myShouldResetCaches;
384 super.setSize(d);
385 if (isChanged) resetCaches();
388 public void reshape(int x, int y, int w, int h) {
389 boolean isChanged = w != getWidth() || myShouldResetCaches;
390 super.reshape(x, y, w, h);
391 if (isChanged) resetCaches();
394 private void resetCaches() {
395 resetHeightCache(tree, defaultRenderer, renderer);
396 myShouldResetCaches = false;
399 public void addNotify() {
400 super.addNotify(); //To change body of overriden methods use Options | File Templates.
401 if (myAddRemoveCounter == 0) myShouldResetCaches = true;
402 myAddRemoveCounter++;
405 public void removeNotify() {
406 super.removeNotify(); //To change body of overriden methods use Options | File Templates.
407 myAddRemoveCounter--;
410 scrollpane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
411 scrollpane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
413 tree.setCellRenderer(renderer);
415 scrollpane.addComponentListener(new ComponentAdapter() {
416 public void componentResized(ComponentEvent e) {
417 resetHeightCache(tree, defaultRenderer, renderer);
420 public void componentShown(ComponentEvent e) {
421 // componentResized not called when adding to opened tool window.
422 // Seems to be BUG#4765299, however I failed to create same code to reproduce it.
423 // To reproduce it with IDEA: 1. remove this method, 2. Start any Ant task, 3. Keep message window open 4. start Ant task again.
424 resetHeightCache(tree, defaultRenderer, renderer);
428 return scrollpane;
431 private static void resetHeightCache(final JTree tree,
432 final TreeCellRenderer defaultRenderer,
433 final MultilineTreeCellRenderer renderer) {
434 tree.setCellRenderer(defaultRenderer);
435 tree.setCellRenderer(renderer);
438 // private static class DelegatingScrollablePanel extends JPanel implements Scrollable {
439 // private final Scrollable myDelegatee;
441 // public DelegatingScrollablePanel(Scrollable delegatee) {
442 // super(new BorderLayout(0, 0));
443 // myDelegatee = delegatee;
444 // add((JComponent)delegatee, BorderLayout.CENTER);
445 // }
447 // public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
448 // return myDelegatee.getScrollableUnitIncrement(visibleRect, orientation, direction);
449 // }
451 // public boolean getScrollableTracksViewportWidth() {
452 // return myDelegatee.getScrollableTracksViewportWidth();
453 // }
455 // public Dimension getPreferredScrollableViewportSize() {
456 // return myDelegatee.getPreferredScrollableViewportSize();
457 // }
459 // public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
460 // return myDelegatee.getScrollableBlockIncrement(visibleRect, orientation, direction);
461 // }
463 // public boolean getScrollableTracksViewportHeight() {
464 // return myDelegatee.getScrollableTracksViewportHeight();
465 // }
466 // }