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
;
23 import javax
.swing
.plaf
.TreeUI
;
24 import javax
.swing
.tree
.DefaultMutableTreeNode
;
25 import javax
.swing
.tree
.TreeCellRenderer
;
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
;
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
) {
66 addPropertyChangeListener(new PropertyChangeListener() {
67 public void propertyChange(PropertyChangeEvent evt
) {
68 if (FONT_PROPERTY_NAME
.equalsIgnoreCase(evt
.getPropertyName())) {
76 protected void setMinHeight(int height
) {
78 myHeightCalculated
= Math
.max(myMinHeight
, myHeightCalculated
);
81 protected void setTextInsets(Insets textInsets
) {
82 myTextInsets
= textInsets
;
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();
120 if (mySelected
&& myHasFocus
){
121 bgColor
= UIUtil
.getTreeSelectionBackground();
122 fgColor
= UIUtil
.getTreeSelectionForeground();
125 bgColor
= UIUtil
.getTreeTextBackground();
126 fgColor
= getForeground();
131 g
.fillRect(borderX
, borderY
, borderW
, borderH
);
135 g
.setColor(UIUtil
.getTreeSelectionBorderColor());
136 UIUtil
.drawDottedRectangle(g
, borderX
, borderY
, borderX
+ borderW
- 1, borderY
+ borderH
- 1);
142 if (myTooSmall
) { // TODO ???
146 int fontHeight
= getCurrFontMetrics().getHeight();
147 int currBaseLine
= getCurrFontMetrics().getAscent();
148 currBaseLine
+= myTextInsets
.top
;
149 g
.setFont(getFont());
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
) {
167 for (int i
= 0; i
< lines
.length
; i
++) {
168 myTextLength
+= lines
[i
].length();
172 myWrapsCalculated
= false;
173 myHeightCalculated
= -1;
174 myWrapsCalculatedForWidth
= -1;
177 public void setIcon(Icon 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() {
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
) {
212 myWrapsCalculated
= false;
215 int wrapsCount
= calculateWraps(currwidth
);
216 myTooSmall
= (wrapsCount
== -1);
218 wrapsCount
= myTextLength
;
220 int fontHeight
= getCurrFontMetrics().getHeight();
221 myHeightCalculated
= wrapsCount
* fontHeight
+ myTextInsets
.top
+ myTextInsets
.bottom
;
222 myHeightCalculated
= Math
.max(myMinHeight
, myHeightCalculated
);
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
;
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) {
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
))) {
266 if (currChar
> currFirst
) {
271 myWraps
.add(aLine
.substring(currFirst
, currLast
+ 1));
272 currFirst
= currLast
+ 1;
273 while ((currFirst
<= lineLastChar
) && (Character
.isWhitespace(aLine
.charAt(currFirst
)))) {
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
) {
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));
295 if (currWidth
> viewWidth
) {
297 if (currChar
<= firstChar
) {
300 currWidth
-= getCurrFontMetrics().charWidth(line
.charAt(currChar
+ 1));
301 if (currWidth
<= viewWidth
) {
307 if (currChar
> lastChar
) {
310 currWidth
+= getCurrFontMetrics().charWidth(line
.charAt(currChar
));
311 if (currWidth
>= viewWidth
) {
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();
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());
370 setSize(getPreferredSize());
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
;
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
);
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);
447 // public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
448 // return myDelegatee.getScrollableUnitIncrement(visibleRect, orientation, direction);
451 // public boolean getScrollableTracksViewportWidth() {
452 // return myDelegatee.getScrollableTracksViewportWidth();
455 // public Dimension getPreferredScrollableViewportSize() {
456 // return myDelegatee.getPreferredScrollableViewportSize();
459 // public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
460 // return myDelegatee.getScrollableBlockIncrement(visibleRect, orientation, direction);
463 // public boolean getScrollableTracksViewportHeight() {
464 // return myDelegatee.getScrollableTracksViewportHeight();