/platform/platform-impl/src/com/intellij/ui/MultilineTreeCellRenderer.java

https://bitbucket.org/nbargnesi/idea · Java · 476 lines · 355 code · 69 blank · 52 comment · 67 complexity · 924d74e47de34a03f038a341d8de9129 MD5 · raw file

  1. /*
  2. * Copyright 2000-2012 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. import com.intellij.openapi.util.text.StringUtil;
  18. import com.intellij.ui.components.JBScrollPane;
  19. import com.intellij.util.ArrayUtil;
  20. import com.intellij.util.ui.UIUtil;
  21. import com.intellij.util.ui.tree.WideSelectionTreeUI;
  22. import org.jetbrains.annotations.NonNls;
  23. import javax.swing.*;
  24. import javax.swing.plaf.TreeUI;
  25. import javax.swing.tree.DefaultMutableTreeNode;
  26. import javax.swing.tree.TreeCellRenderer;
  27. import java.awt.*;
  28. import java.awt.event.ComponentAdapter;
  29. import java.awt.event.ComponentEvent;
  30. import java.beans.PropertyChangeEvent;
  31. import java.beans.PropertyChangeListener;
  32. import java.util.ArrayList;
  33. public abstract class MultilineTreeCellRenderer extends JComponent implements TreeCellRenderer {
  34. private boolean myWrapsCalculated = false;
  35. private boolean myTooSmall = false;
  36. private int myHeightCalculated = -1;
  37. private int myWrapsCalculatedForWidth = -1;
  38. private ArrayList myWraps = new ArrayList();
  39. private int myMinHeight = 1;
  40. private Insets myTextInsets;
  41. private final Insets myLabelInsets = new Insets(1, 2, 1, 2);
  42. private boolean mySelected;
  43. private boolean myHasFocus;
  44. private Icon myIcon;
  45. private String[] myLines = ArrayUtil.EMPTY_STRING_ARRAY;
  46. private String myPrefix;
  47. private int myTextLength;
  48. private int myPrefixWidth;
  49. @NonNls protected static final String FONT_PROPERTY_NAME = "font";
  50. private JTree myTree;
  51. public MultilineTreeCellRenderer() {
  52. myTextInsets = new Insets(0,0,0,0);
  53. addComponentListener(new ComponentAdapter() {
  54. public void componentResized(ComponentEvent e) {
  55. onSizeChanged();
  56. }
  57. });
  58. addPropertyChangeListener(new PropertyChangeListener() {
  59. public void propertyChange(PropertyChangeEvent evt) {
  60. if (FONT_PROPERTY_NAME.equalsIgnoreCase(evt.getPropertyName())) {
  61. onFontChanged();
  62. }
  63. }
  64. });
  65. }
  66. protected void setMinHeight(int height) {
  67. myMinHeight = height;
  68. myHeightCalculated = Math.max(myMinHeight, myHeightCalculated);
  69. }
  70. protected void setTextInsets(Insets textInsets) {
  71. myTextInsets = textInsets;
  72. onSizeChanged();
  73. }
  74. private void onFontChanged() {
  75. myWrapsCalculated = false;
  76. }
  77. private void onSizeChanged() {
  78. int currWidth = getWidth();
  79. if (currWidth != myWrapsCalculatedForWidth) {
  80. myWrapsCalculated = false;
  81. myHeightCalculated = -1;
  82. myWrapsCalculatedForWidth = -1;
  83. }
  84. }
  85. private FontMetrics getCurrFontMetrics() {
  86. return getFontMetrics(getFont());
  87. }
  88. public void paint(Graphics g) {
  89. int height = getHeight();
  90. int width = getWidth();
  91. int borderX = myLabelInsets.left - 1;
  92. int borderY = myLabelInsets.top - 1;
  93. int borderW = width - borderX - myLabelInsets.right + 2;
  94. int borderH = height - borderY - myLabelInsets.bottom + 1;
  95. if (myIcon != null) {
  96. int verticalIconPosition = (height - myIcon.getIconHeight())/2;
  97. myIcon.paintIcon(this, g, 0, verticalIconPosition);
  98. borderX += myIcon.getIconWidth();
  99. borderW -= myIcon.getIconWidth();
  100. }
  101. Color bgColor;
  102. Color fgColor;
  103. if (mySelected && myHasFocus){
  104. bgColor = UIUtil.getTreeSelectionBackground();
  105. fgColor = UIUtil.getTreeSelectionForeground();
  106. }
  107. else{
  108. bgColor = UIUtil.getTreeTextBackground();
  109. fgColor = getForeground();
  110. }
  111. // fill background
  112. if (!(myTree.getUI() instanceof WideSelectionTreeUI) || !((WideSelectionTreeUI)myTree.getUI()).isWideSelection()) {
  113. g.setColor(bgColor);
  114. g.fillRect(borderX, borderY, borderW, borderH);
  115. // draw border
  116. if (mySelected) {
  117. g.setColor(UIUtil.getTreeSelectionBorderColor());
  118. UIUtil.drawDottedRectangle(g, borderX, borderY, borderX + borderW - 1, borderY + borderH - 1);
  119. }
  120. }
  121. // paint text
  122. recalculateWraps();
  123. if (myTooSmall) { // TODO ???
  124. return;
  125. }
  126. int fontHeight = getCurrFontMetrics().getHeight();
  127. int currBaseLine = getCurrFontMetrics().getAscent();
  128. currBaseLine += myTextInsets.top;
  129. g.setFont(getFont());
  130. g.setColor(fgColor);
  131. UIUtil.applyRenderingHints(g);
  132. if (!StringUtil.isEmpty(myPrefix)) {
  133. g.drawString(myPrefix, myTextInsets.left - myPrefixWidth + 1, currBaseLine);
  134. }
  135. for (int i = 0; i < myWraps.size(); i++) {
  136. String currLine = (String)myWraps.get(i);
  137. g.drawString(currLine, myTextInsets.left, currBaseLine);
  138. currBaseLine += fontHeight; // first is getCurrFontMetrics().getAscent()
  139. }
  140. }
  141. public void setText(String[] lines, String prefix) {
  142. myLines = lines;
  143. myTextLength = 0;
  144. for (int i = 0; i < lines.length; i++) {
  145. myTextLength += lines[i].length();
  146. }
  147. myPrefix = prefix;
  148. myWrapsCalculated = false;
  149. myHeightCalculated = -1;
  150. myWrapsCalculatedForWidth = -1;
  151. }
  152. public void setIcon(Icon icon) {
  153. myIcon = icon;
  154. myWrapsCalculated = false;
  155. myHeightCalculated = -1;
  156. myWrapsCalculatedForWidth = -1;
  157. }
  158. public Dimension getMinimumSize() {
  159. if (getFont() != null) {
  160. int minHeight = getCurrFontMetrics().getHeight();
  161. return new Dimension(minHeight, minHeight);
  162. }
  163. return new Dimension(
  164. MIN_WIDTH + myTextInsets.left + myTextInsets.right,
  165. MIN_WIDTH + myTextInsets.top + myTextInsets.bottom
  166. );
  167. }
  168. private static final int MIN_WIDTH = 10;
  169. // Calculates height for current width.
  170. public Dimension getPreferredSize() {
  171. recalculateWraps();
  172. return new Dimension(myWrapsCalculatedForWidth, myHeightCalculated);
  173. }
  174. // Calculate wraps for the current width
  175. private void recalculateWraps() {
  176. int currwidth = getWidth();
  177. if (myWrapsCalculated) {
  178. if (currwidth == myWrapsCalculatedForWidth) {
  179. return;
  180. }
  181. else {
  182. myWrapsCalculated = false;
  183. }
  184. }
  185. int wrapsCount = calculateWraps(currwidth);
  186. myTooSmall = (wrapsCount == -1);
  187. if (myTooSmall) {
  188. wrapsCount = myTextLength;
  189. }
  190. int fontHeight = getCurrFontMetrics().getHeight();
  191. myHeightCalculated = wrapsCount * fontHeight + myTextInsets.top + myTextInsets.bottom;
  192. myHeightCalculated = Math.max(myMinHeight, myHeightCalculated);
  193. int maxWidth = 0;
  194. for (int i=0; i < myWraps.size(); i++) {
  195. String s = (String)myWraps.get(i);
  196. int width = getCurrFontMetrics().stringWidth(s);
  197. maxWidth = Math.max(maxWidth, width);
  198. }
  199. myWrapsCalculatedForWidth = myTextInsets.left + maxWidth + myTextInsets.right;
  200. myWrapsCalculated = true;
  201. }
  202. private int calculateWraps(int width) {
  203. myTooSmall = width < MIN_WIDTH;
  204. if (myTooSmall) {
  205. return -1;
  206. }
  207. int result = 0;
  208. myWraps = new ArrayList();
  209. for (int i = 0; i < myLines.length; i++) {
  210. String aLine = myLines[i];
  211. int lineFirstChar = 0;
  212. int lineLastChar = aLine.length() - 1;
  213. int currFirst = lineFirstChar;
  214. int printableWidth = width - myTextInsets.left - myTextInsets.right;
  215. if (aLine.length() == 0) {
  216. myWraps.add(aLine);
  217. result++;
  218. }
  219. else {
  220. while (currFirst <= lineLastChar) {
  221. int currLast = calculateLastVisibleChar(aLine, printableWidth, currFirst, lineLastChar);
  222. if (currLast < lineLastChar) {
  223. int currChar = currLast + 1;
  224. if (!Character.isWhitespace(aLine.charAt(currChar))) {
  225. while (currChar >= currFirst) {
  226. if (Character.isWhitespace(aLine.charAt(currChar))) {
  227. break;
  228. }
  229. currChar--;
  230. }
  231. if (currChar > currFirst) {
  232. currLast = currChar;
  233. }
  234. }
  235. }
  236. myWraps.add(aLine.substring(currFirst, currLast + 1));
  237. currFirst = currLast + 1;
  238. while ((currFirst <= lineLastChar) && (Character.isWhitespace(aLine.charAt(currFirst)))) {
  239. currFirst++;
  240. }
  241. result++;
  242. }
  243. }
  244. }
  245. return result;
  246. }
  247. private int calculateLastVisibleChar(String line, int viewWidth, int firstChar, int lastChar) {
  248. if (firstChar == lastChar) return lastChar;
  249. if (firstChar > lastChar) throw new IllegalArgumentException("firstChar=" + firstChar + ", lastChar=" + lastChar);
  250. int totalWidth = getCurrFontMetrics().stringWidth(line.substring(firstChar, lastChar + 1));
  251. if (totalWidth == 0 || viewWidth > totalWidth) {
  252. return lastChar;
  253. }
  254. else {
  255. int newApprox = (lastChar - firstChar + 1) * viewWidth / totalWidth;
  256. int currChar = firstChar + Math.max(newApprox - 1, 0);
  257. int currWidth = getCurrFontMetrics().stringWidth(line.substring(firstChar, currChar + 1));
  258. while (true) {
  259. if (currWidth > viewWidth) {
  260. currChar--;
  261. if (currChar <= firstChar) {
  262. return firstChar;
  263. }
  264. currWidth -= getCurrFontMetrics().charWidth(line.charAt(currChar + 1));
  265. if (currWidth <= viewWidth) {
  266. return currChar;
  267. }
  268. }
  269. else {
  270. currChar++;
  271. if (currChar > lastChar) {
  272. return lastChar;
  273. }
  274. currWidth += getCurrFontMetrics().charWidth(line.charAt(currChar));
  275. if (currWidth >= viewWidth) {
  276. return currChar - 1;
  277. }
  278. }
  279. }
  280. }
  281. }
  282. private int getChildIndent(JTree tree) {
  283. TreeUI newUI = tree.getUI();
  284. if (newUI instanceof javax.swing.plaf.basic.BasicTreeUI) {
  285. javax.swing.plaf.basic.BasicTreeUI btreeui = (javax.swing.plaf.basic.BasicTreeUI)newUI;
  286. return btreeui.getLeftChildIndent() + btreeui.getRightChildIndent();
  287. }
  288. else {
  289. return ((Integer)UIUtil.getTreeLeftChildIndent()).intValue() + ((Integer)UIUtil.getTreeRightChildIndent()).intValue();
  290. }
  291. }
  292. private int getAvailableWidth(Object forValue, JTree tree) {
  293. DefaultMutableTreeNode node = (DefaultMutableTreeNode)forValue;
  294. int busyRoom = tree.getInsets().left + tree.getInsets().right + getChildIndent(tree) * node.getLevel();
  295. return tree.getVisibleRect().width - busyRoom - 2;
  296. }
  297. protected abstract void initComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus);
  298. public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
  299. setFont(UIUtil.getTreeFont());
  300. initComponent(tree, value, selected, expanded, leaf, row, hasFocus);
  301. mySelected = selected;
  302. myHasFocus = hasFocus;
  303. myTree = tree;
  304. int availWidth = getAvailableWidth(value, tree);
  305. if (availWidth > 0) {
  306. setSize(availWidth, 100); // height will be calculated automatically
  307. }
  308. int leftInset = myLabelInsets.left;
  309. if (myIcon != null) {
  310. leftInset += myIcon.getIconWidth() + 2;
  311. }
  312. if (!StringUtil.isEmpty(myPrefix)) {
  313. myPrefixWidth = getCurrFontMetrics().stringWidth(myPrefix) + 5;
  314. leftInset += myPrefixWidth;
  315. }
  316. setTextInsets(new Insets(myLabelInsets.top, leftInset, myLabelInsets.bottom, myLabelInsets.right));
  317. if (myIcon != null) {
  318. setMinHeight(myIcon.getIconHeight());
  319. }
  320. else {
  321. setMinHeight(1);
  322. }
  323. setSize(getPreferredSize());
  324. recalculateWraps();
  325. return this;
  326. }
  327. public static JScrollPane installRenderer(final JTree tree, final MultilineTreeCellRenderer renderer) {
  328. final TreeCellRenderer defaultRenderer = tree.getCellRenderer();
  329. JScrollPane scrollpane = new JBScrollPane(tree){
  330. private int myAddRemoveCounter = 0;
  331. private boolean myShouldResetCaches = false;
  332. public void setSize(Dimension d) {
  333. boolean isChanged = getWidth() != d.width || myShouldResetCaches;
  334. super.setSize(d);
  335. if (isChanged) resetCaches();
  336. }
  337. public void reshape(int x, int y, int w, int h) {
  338. boolean isChanged = w != getWidth() || myShouldResetCaches;
  339. super.reshape(x, y, w, h);
  340. if (isChanged) resetCaches();
  341. }
  342. private void resetCaches() {
  343. resetHeightCache(tree, defaultRenderer, renderer);
  344. myShouldResetCaches = false;
  345. }
  346. public void addNotify() {
  347. super.addNotify(); //To change body of overriden methods use Options | File Templates.
  348. if (myAddRemoveCounter == 0) myShouldResetCaches = true;
  349. myAddRemoveCounter++;
  350. }
  351. public void removeNotify() {
  352. super.removeNotify(); //To change body of overriden methods use Options | File Templates.
  353. myAddRemoveCounter--;
  354. }
  355. };
  356. scrollpane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
  357. scrollpane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
  358. tree.setCellRenderer(renderer);
  359. scrollpane.addComponentListener(new ComponentAdapter() {
  360. public void componentResized(ComponentEvent e) {
  361. resetHeightCache(tree, defaultRenderer, renderer);
  362. }
  363. public void componentShown(ComponentEvent e) {
  364. // componentResized not called when adding to opened tool window.
  365. // Seems to be BUG#4765299, however I failed to create same code to reproduce it.
  366. // To reproduce it with IDEA: 1. remove this method, 2. Start any Ant task, 3. Keep message window open 4. start Ant task again.
  367. resetHeightCache(tree, defaultRenderer, renderer);
  368. }
  369. });
  370. return scrollpane;
  371. }
  372. private static void resetHeightCache(final JTree tree,
  373. final TreeCellRenderer defaultRenderer,
  374. final MultilineTreeCellRenderer renderer) {
  375. tree.setCellRenderer(defaultRenderer);
  376. tree.setCellRenderer(renderer);
  377. }
  378. // private static class DelegatingScrollablePanel extends JPanel implements Scrollable {
  379. // private final Scrollable myDelegatee;
  380. //
  381. // public DelegatingScrollablePanel(Scrollable delegatee) {
  382. // super(new BorderLayout(0, 0));
  383. // myDelegatee = delegatee;
  384. // add((JComponent)delegatee, BorderLayout.CENTER);
  385. // }
  386. //
  387. // public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
  388. // return myDelegatee.getScrollableUnitIncrement(visibleRect, orientation, direction);
  389. // }
  390. //
  391. // public boolean getScrollableTracksViewportWidth() {
  392. // return myDelegatee.getScrollableTracksViewportWidth();
  393. // }
  394. //
  395. // public Dimension getPreferredScrollableViewportSize() {
  396. // return myDelegatee.getPreferredScrollableViewportSize();
  397. // }
  398. //
  399. // public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
  400. // return myDelegatee.getScrollableBlockIncrement(visibleRect, orientation, direction);
  401. // }
  402. //
  403. // public boolean getScrollableTracksViewportHeight() {
  404. // return myDelegatee.getScrollableTracksViewportHeight();
  405. // }
  406. // }
  407. }