replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / MultilineTreeCellRenderer.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.ui;
17
18 import com.intellij.ide.ui.UISettings;
19 import com.intellij.openapi.util.text.StringUtil;
20 import com.intellij.ui.components.JBScrollPane;
21 import com.intellij.util.ArrayUtil;
22 import com.intellij.util.SystemProperties;
23 import com.intellij.util.ui.UIUtil;
24 import com.intellij.util.ui.tree.WideSelectionTreeUI;
25 import org.jetbrains.annotations.NonNls;
26
27 import javax.accessibility.Accessible;
28 import javax.accessibility.AccessibleContext;
29 import javax.accessibility.AccessibleRole;
30 import javax.swing.*;
31 import javax.swing.plaf.TreeUI;
32 import javax.swing.tree.DefaultMutableTreeNode;
33 import javax.swing.tree.TreeCellRenderer;
34 import java.awt.*;
35 import java.awt.event.ComponentAdapter;
36 import java.awt.event.ComponentEvent;
37 import java.beans.PropertyChangeEvent;
38 import java.beans.PropertyChangeListener;
39 import java.util.ArrayList;
40
41 public abstract class MultilineTreeCellRenderer extends JComponent implements Accessible, TreeCellRenderer {
42
43   private boolean myWrapsCalculated = false;
44   private boolean myTooSmall = false;
45   private int myHeightCalculated = -1;
46   private int myWrapsCalculatedForWidth = -1;
47
48   private ArrayList myWraps = new ArrayList();
49
50   private int myMinHeight = 1;
51   private Insets myTextInsets;
52   private final Insets myLabelInsets = new Insets(1, 2, 1, 2);
53
54   private boolean mySelected;
55   private boolean myHasFocus;
56
57   private Icon myIcon;
58   private String[] myLines = ArrayUtil.EMPTY_STRING_ARRAY;
59   private String myPrefix;
60   private int myTextLength;
61   private int myPrefixWidth;
62   @NonNls protected static final String FONT_PROPERTY_NAME = "font";
63   private JTree myTree;
64
65
66   public MultilineTreeCellRenderer() {
67     myTextInsets = new Insets(0,0,0,0);
68
69     addComponentListener(new ComponentAdapter() {
70       public void componentResized(ComponentEvent e) {
71         onSizeChanged();
72       }
73     });
74
75     addPropertyChangeListener(new PropertyChangeListener() {
76       public void propertyChange(PropertyChangeEvent evt) {
77         if (FONT_PROPERTY_NAME.equalsIgnoreCase(evt.getPropertyName())) {
78           onFontChanged();
79         }
80       }
81     });
82     updateUI();
83   }
84
85   @Override
86   public void updateUI() {
87     UISettings.setupComponentAntialiasing(this);
88   }
89
90   protected void setMinHeight(int height) {
91     myMinHeight = height;
92     myHeightCalculated = Math.max(myMinHeight, myHeightCalculated);
93   }
94
95   protected void setTextInsets(Insets textInsets) {
96     myTextInsets = textInsets;
97     onSizeChanged();
98   }
99
100   private void onFontChanged() {
101     myWrapsCalculated = false;
102   }
103
104   private void onSizeChanged() {
105     int currWidth = getWidth();
106     if (currWidth != myWrapsCalculatedForWidth) {
107       myWrapsCalculated = false;
108       myHeightCalculated = -1;
109       myWrapsCalculatedForWidth = -1;
110     }
111   }
112
113   private FontMetrics getCurrFontMetrics() {
114     return getFontMetrics(getFont());
115   }
116
117   public void paint(Graphics g) {
118     int height = getHeight();
119     int width = getWidth();
120     int borderX = myLabelInsets.left - 1;
121     int borderY = myLabelInsets.top - 1;
122     int borderW = width - borderX - myLabelInsets.right + 2;
123     int borderH = height - borderY - myLabelInsets.bottom + 1;
124
125     if (myIcon != null) {
126       int verticalIconPosition = (height - myIcon.getIconHeight())/2;
127       myIcon.paintIcon(this, g, 0, isIconVerticallyCentered() ? verticalIconPosition : myTextInsets.top);
128       borderX += myIcon.getIconWidth();
129       borderW -= myIcon.getIconWidth();
130     }
131
132     Color bgColor;
133     Color fgColor;
134     if (mySelected && myHasFocus){
135       bgColor = UIUtil.getTreeSelectionBackground();
136       fgColor = UIUtil.getTreeSelectionForeground();
137     }
138     else{
139       bgColor = UIUtil.getTreeTextBackground();
140       fgColor = getForeground();
141     }
142
143     // fill background
144     if (!WideSelectionTreeUI.isWideSelection(myTree)) {
145       g.setColor(bgColor);
146       g.fillRect(borderX, borderY, borderW, borderH);
147
148       // draw border
149       if (mySelected) {
150         g.setColor(UIUtil.getTreeSelectionBorderColor());
151         UIUtil.drawDottedRectangle(g, borderX, borderY, borderX + borderW - 1, borderY + borderH - 1);
152       }
153     }
154
155     // paint text
156     recalculateWraps();
157
158     if (myTooSmall) { // TODO ???
159       return;
160     }
161
162     int fontHeight = getCurrFontMetrics().getHeight();
163     int currBaseLine = getCurrFontMetrics().getAscent();
164     currBaseLine += myTextInsets.top;
165     g.setFont(getFont());
166     g.setColor(fgColor);
167     UISettings.setupAntialiasing(g);
168
169     if (!StringUtil.isEmpty(myPrefix)) {
170       g.drawString(myPrefix, myTextInsets.left - myPrefixWidth + 1, currBaseLine);
171     }
172
173     for (int i = 0; i < myWraps.size(); i++) {
174       String currLine = (String)myWraps.get(i);
175       g.drawString(currLine, myTextInsets.left, currBaseLine);
176       currBaseLine += fontHeight;  // first is getCurrFontMetrics().getAscent()
177     }
178   }
179
180   public void setText(String[] lines, String prefix) {
181     myLines = lines;
182     myTextLength = 0;
183     for (int i = 0; i < lines.length; i++) {
184       myTextLength += lines[i].length();
185     }
186     myPrefix = prefix;
187
188     myWrapsCalculated = false;
189     myHeightCalculated = -1;
190     myWrapsCalculatedForWidth = -1;
191   }
192
193   public void setIcon(Icon icon) {
194     myIcon = icon;
195
196     myWrapsCalculated = false;
197     myHeightCalculated = -1;
198     myWrapsCalculatedForWidth = -1;
199   }
200
201   public Dimension getMinimumSize() {
202     if (getFont() != null) {
203       int minHeight = getCurrFontMetrics().getHeight();
204       return new Dimension(minHeight, minHeight);
205     }
206     return new Dimension(
207       MIN_WIDTH + myTextInsets.left + myTextInsets.right,
208       MIN_WIDTH + myTextInsets.top + myTextInsets.bottom
209     );
210   }
211
212   private static final int MIN_WIDTH = 10;
213
214   // Calculates height for current width.
215   public Dimension getPreferredSize() {
216     recalculateWraps();
217     return new Dimension(myWrapsCalculatedForWidth, myHeightCalculated);
218   }
219
220   // Calculate wraps for the current width
221   private void recalculateWraps() {
222     int currwidth = getWidth();
223     if (myWrapsCalculated) {
224       if (currwidth == myWrapsCalculatedForWidth) {
225         return;
226       }
227       else {
228         myWrapsCalculated = false;
229       }
230     }
231     int wrapsCount = calculateWraps(currwidth);
232     myTooSmall = (wrapsCount == -1);
233     if (myTooSmall) {
234       wrapsCount = myTextLength;
235     }
236     int fontHeight = getCurrFontMetrics().getHeight();
237     myHeightCalculated = wrapsCount * fontHeight + myTextInsets.top + myTextInsets.bottom;
238     myHeightCalculated = Math.max(myMinHeight, myHeightCalculated);
239
240     int maxWidth = 0;
241     for (int i=0; i < myWraps.size(); i++) {
242       String s = (String)myWraps.get(i);
243       int width = getCurrFontMetrics().stringWidth(s);
244       maxWidth = Math.max(maxWidth, width);
245     }
246
247     myWrapsCalculatedForWidth = myTextInsets.left + maxWidth + myTextInsets.right;
248     myWrapsCalculated = true;
249   }
250
251   private int calculateWraps(int width) {
252     myTooSmall = width < MIN_WIDTH;
253     if (myTooSmall) {
254       return -1;
255     }
256
257     int result = 0;
258     myWraps = new ArrayList();
259
260     for (int i = 0; i < myLines.length; i++) {
261       String aLine = myLines[i];
262       int lineFirstChar = 0;
263       int lineLastChar = aLine.length() - 1;
264       int currFirst = lineFirstChar;
265       int printableWidth = width - myTextInsets.left - myTextInsets.right;
266       if (aLine.length() == 0) {
267         myWraps.add(aLine);
268         result++;
269       }
270       else {
271         while (currFirst <= lineLastChar) {
272           int currLast = calculateLastVisibleChar(aLine, printableWidth, currFirst, lineLastChar);
273           if (currLast < lineLastChar) {
274             int currChar = currLast + 1;
275             if (!Character.isWhitespace(aLine.charAt(currChar))) {
276               while (currChar >= currFirst) {
277                 if (Character.isWhitespace(aLine.charAt(currChar))) {
278                   break;
279                 }
280                 currChar--;
281               }
282               if (currChar > currFirst) {
283                 currLast = currChar;
284               }
285             }
286           }
287           myWraps.add(aLine.substring(currFirst, currLast + 1));
288           currFirst = currLast + 1;
289           while ((currFirst <= lineLastChar) && (Character.isWhitespace(aLine.charAt(currFirst)))) {
290             currFirst++;
291           }
292           result++;
293         }
294       }
295     }
296     return result;
297   }
298
299   private int calculateLastVisibleChar(String line, int viewWidth, int firstChar, int lastChar) {
300     if (firstChar == lastChar) return lastChar;
301     if (firstChar > lastChar) throw new IllegalArgumentException("firstChar=" + firstChar + ", lastChar=" + lastChar);
302     int totalWidth = getCurrFontMetrics().stringWidth(line.substring(firstChar, lastChar + 1));
303     if (totalWidth == 0 || viewWidth > totalWidth) {
304       return lastChar;
305     }
306     else {
307       int newApprox = (lastChar - firstChar + 1) * viewWidth / totalWidth;
308       int currChar = firstChar + Math.max(newApprox - 1, 0);
309       int currWidth = getCurrFontMetrics().stringWidth(line.substring(firstChar, currChar + 1));
310       while (true) {
311         if (currWidth > viewWidth) {
312           currChar--;
313           if (currChar <= firstChar) {
314             return firstChar;
315           }
316           currWidth -= getCurrFontMetrics().charWidth(line.charAt(currChar + 1));
317           if (currWidth <= viewWidth) {
318             return currChar;
319           }
320         }
321         else {
322           currChar++;
323           if (currChar > lastChar) {
324             return lastChar;
325           }
326           currWidth += getCurrFontMetrics().charWidth(line.charAt(currChar));
327           if (currWidth >= viewWidth) {
328             return currChar - 1;
329           }
330         }
331       }
332     }
333   }
334
335   private int getChildIndent(JTree tree) {
336     TreeUI newUI = tree.getUI();
337     if (newUI instanceof javax.swing.plaf.basic.BasicTreeUI) {
338       javax.swing.plaf.basic.BasicTreeUI btreeui = (javax.swing.plaf.basic.BasicTreeUI)newUI;
339       return btreeui.getLeftChildIndent() + btreeui.getRightChildIndent();
340     }
341     else {
342       return ((Integer)UIUtil.getTreeLeftChildIndent()).intValue() + ((Integer)UIUtil.getTreeRightChildIndent()).intValue();
343     }
344   }
345
346   private int getAvailableWidth(Object forValue, JTree tree) {
347     DefaultMutableTreeNode node = (DefaultMutableTreeNode)forValue;
348     int busyRoom = tree.getInsets().left + tree.getInsets().right + getChildIndent(tree) * node.getLevel();
349     return tree.getVisibleRect().width - busyRoom - 2;
350   }
351
352   protected abstract void initComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus);
353
354   public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
355     setFont(UIUtil.getTreeFont());
356
357     initComponent(tree, value, selected, expanded, leaf, row, hasFocus);
358
359     mySelected = selected;
360     myHasFocus = hasFocus;
361
362     myTree = tree;
363
364     int availWidth = getAvailableWidth(value, tree);
365     if (availWidth > 0) {
366       setSize(availWidth, 100);     // height will be calculated automatically
367     }
368
369     int leftInset = myLabelInsets.left;
370
371     if (myIcon != null) {
372       leftInset += myIcon.getIconWidth() + 2;
373     }
374
375     if (!StringUtil.isEmpty(myPrefix)) {
376       myPrefixWidth = getCurrFontMetrics().stringWidth(myPrefix) + 5;
377       leftInset += myPrefixWidth;
378     }
379
380     setTextInsets(new Insets(myLabelInsets.top, leftInset, myLabelInsets.bottom, myLabelInsets.right));
381     if (myIcon != null) {
382       setMinHeight(myIcon.getIconHeight());
383     }
384     else {
385       setMinHeight(1);
386     }
387
388     setSize(getPreferredSize());
389     recalculateWraps();
390
391     return this;
392   }
393
394   /**
395    * Returns {@code true} if icon should be vertically centered. Otherwise, icon will be placed on top
396    * @return
397    */
398   protected boolean isIconVerticallyCentered() {
399     return false;
400   }
401
402   public static JScrollPane installRenderer(final JTree tree, final MultilineTreeCellRenderer renderer) {
403     final TreeCellRenderer defaultRenderer = tree.getCellRenderer();
404
405     JScrollPane scrollpane = new JBScrollPane(tree){
406       private int myAddRemoveCounter = 0;
407       private boolean myShouldResetCaches = false;
408       public void setSize(Dimension d) {
409         boolean isChanged = getWidth() != d.width || myShouldResetCaches;
410         super.setSize(d);
411         if (isChanged) resetCaches();
412       }
413
414       public void reshape(int x, int y, int w, int h) {
415         boolean isChanged = w != getWidth() || myShouldResetCaches;
416         super.reshape(x, y, w, h);
417         if (isChanged) resetCaches();
418       }
419
420       private void resetCaches() {
421         resetHeightCache(tree, defaultRenderer, renderer);
422         myShouldResetCaches = false;
423       }
424
425       public void addNotify() {
426         super.addNotify();
427         if (myAddRemoveCounter == 0) myShouldResetCaches = true;
428         myAddRemoveCounter++;
429       }
430
431       public void removeNotify() {
432         super.removeNotify();
433         myAddRemoveCounter--;
434       }
435     };
436     scrollpane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
437     scrollpane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
438
439     tree.setCellRenderer(renderer);
440
441     scrollpane.addComponentListener(new ComponentAdapter() {
442       public void componentResized(ComponentEvent e) {
443         resetHeightCache(tree, defaultRenderer, renderer);
444       }
445
446       public void componentShown(ComponentEvent e) {
447         // componentResized not called when adding to opened tool window.
448         // Seems to be BUG#4765299, however I failed to create same code to reproduce it.
449         // To reproduce it with IDEA: 1. remove this method, 2. Start any Ant task, 3. Keep message window open 4. start Ant task again.
450         resetHeightCache(tree, defaultRenderer, renderer);
451       }
452     });
453
454     return scrollpane;
455   }
456
457   private static void resetHeightCache(final JTree tree,
458                                        final TreeCellRenderer defaultRenderer,
459                                        final MultilineTreeCellRenderer renderer) {
460     tree.setCellRenderer(defaultRenderer);
461     tree.setCellRenderer(renderer);
462   }
463
464 //  private static class DelegatingScrollablePanel extends JPanel implements Scrollable {
465 //    private final Scrollable myDelegatee;
466 //
467 //    public DelegatingScrollablePanel(Scrollable delegatee) {
468 //      super(new BorderLayout(0, 0));
469 //      myDelegatee = delegatee;
470 //      add((JComponent)delegatee, BorderLayout.CENTER);
471 //    }
472 //
473 //    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
474 //      return myDelegatee.getScrollableUnitIncrement(visibleRect, orientation, direction);
475 //    }
476 //
477 //    public boolean getScrollableTracksViewportWidth() {
478 //      return myDelegatee.getScrollableTracksViewportWidth();
479 //    }
480 //
481 //    public Dimension getPreferredScrollableViewportSize() {
482 //      return myDelegatee.getPreferredScrollableViewportSize();
483 //    }
484 //
485 //    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
486 //      return myDelegatee.getScrollableBlockIncrement(visibleRect, orientation, direction);
487 //    }
488 //
489 //    public boolean getScrollableTracksViewportHeight() {
490 //      return myDelegatee.getScrollableTracksViewportHeight();
491 //    }
492 //  }
493
494   @Override
495   public AccessibleContext getAccessibleContext() {
496     if (accessibleContext == null) {
497       accessibleContext = new AccessibleMultilineTreeCellRenderer();
498     }
499     return accessibleContext;
500   }
501
502   protected class AccessibleMultilineTreeCellRenderer extends AccessibleJComponent {
503     @Override
504     public String getAccessibleName() {
505       String name = accessibleName;
506       if (name == null) {
507         name = (String)getClientProperty(AccessibleContext.ACCESSIBLE_NAME_PROPERTY);
508       }
509
510       if (name == null) {
511         StringBuilder sb = new StringBuilder();
512         for (String aLine : myLines) {
513           sb.append(aLine);
514           sb.append(SystemProperties.getLineSeparator());
515         }
516         if (sb.length() > 0) name = sb.toString();
517       }
518
519       if (name == null) {
520         name = super.getAccessibleName();
521       }
522       return name;
523     }
524
525     @Override
526     public AccessibleRole getAccessibleRole() {
527       return AccessibleRole.LABEL;
528     }
529   }
530 }
531