| /* |
| * Copyright 2000-2012 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.ui; |
| |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.ui.components.JBScrollPane; |
| import com.intellij.util.ArrayUtil; |
| import com.intellij.util.ui.UIUtil; |
| import com.intellij.util.ui.tree.WideSelectionTreeUI; |
| import org.jetbrains.annotations.NonNls; |
| |
| import javax.swing.*; |
| import javax.swing.plaf.TreeUI; |
| import javax.swing.tree.DefaultMutableTreeNode; |
| import javax.swing.tree.TreeCellRenderer; |
| import java.awt.*; |
| import java.awt.event.ComponentAdapter; |
| import java.awt.event.ComponentEvent; |
| import java.beans.PropertyChangeEvent; |
| import java.beans.PropertyChangeListener; |
| import java.util.ArrayList; |
| |
| public abstract class MultilineTreeCellRenderer extends JComponent implements TreeCellRenderer { |
| |
| private boolean myWrapsCalculated = false; |
| private boolean myTooSmall = false; |
| private int myHeightCalculated = -1; |
| private int myWrapsCalculatedForWidth = -1; |
| |
| private ArrayList myWraps = new ArrayList(); |
| |
| private int myMinHeight = 1; |
| private Insets myTextInsets; |
| private final Insets myLabelInsets = new Insets(1, 2, 1, 2); |
| |
| private boolean mySelected; |
| private boolean myHasFocus; |
| |
| private Icon myIcon; |
| private String[] myLines = ArrayUtil.EMPTY_STRING_ARRAY; |
| private String myPrefix; |
| private int myTextLength; |
| private int myPrefixWidth; |
| @NonNls protected static final String FONT_PROPERTY_NAME = "font"; |
| private JTree myTree; |
| |
| |
| public MultilineTreeCellRenderer() { |
| myTextInsets = new Insets(0,0,0,0); |
| |
| addComponentListener(new ComponentAdapter() { |
| public void componentResized(ComponentEvent e) { |
| onSizeChanged(); |
| } |
| }); |
| |
| addPropertyChangeListener(new PropertyChangeListener() { |
| public void propertyChange(PropertyChangeEvent evt) { |
| if (FONT_PROPERTY_NAME.equalsIgnoreCase(evt.getPropertyName())) { |
| onFontChanged(); |
| } |
| } |
| }); |
| |
| } |
| |
| protected void setMinHeight(int height) { |
| myMinHeight = height; |
| myHeightCalculated = Math.max(myMinHeight, myHeightCalculated); |
| } |
| |
| protected void setTextInsets(Insets textInsets) { |
| myTextInsets = textInsets; |
| onSizeChanged(); |
| } |
| |
| private void onFontChanged() { |
| myWrapsCalculated = false; |
| } |
| |
| private void onSizeChanged() { |
| int currWidth = getWidth(); |
| if (currWidth != myWrapsCalculatedForWidth) { |
| myWrapsCalculated = false; |
| myHeightCalculated = -1; |
| myWrapsCalculatedForWidth = -1; |
| } |
| } |
| |
| private FontMetrics getCurrFontMetrics() { |
| return getFontMetrics(getFont()); |
| } |
| |
| public void paint(Graphics g) { |
| int height = getHeight(); |
| int width = getWidth(); |
| int borderX = myLabelInsets.left - 1; |
| int borderY = myLabelInsets.top - 1; |
| int borderW = width - borderX - myLabelInsets.right + 2; |
| int borderH = height - borderY - myLabelInsets.bottom + 1; |
| |
| if (myIcon != null) { |
| int verticalIconPosition = (height - myIcon.getIconHeight())/2; |
| myIcon.paintIcon(this, g, 0, verticalIconPosition); |
| borderX += myIcon.getIconWidth(); |
| borderW -= myIcon.getIconWidth(); |
| } |
| |
| Color bgColor; |
| Color fgColor; |
| if (mySelected && myHasFocus){ |
| bgColor = UIUtil.getTreeSelectionBackground(); |
| fgColor = UIUtil.getTreeSelectionForeground(); |
| } |
| else{ |
| bgColor = UIUtil.getTreeTextBackground(); |
| fgColor = getForeground(); |
| } |
| |
| // fill background |
| if (!(myTree.getUI() instanceof WideSelectionTreeUI) || !((WideSelectionTreeUI)myTree.getUI()).isWideSelection()) { |
| g.setColor(bgColor); |
| g.fillRect(borderX, borderY, borderW, borderH); |
| |
| // draw border |
| if (mySelected) { |
| g.setColor(UIUtil.getTreeSelectionBorderColor()); |
| UIUtil.drawDottedRectangle(g, borderX, borderY, borderX + borderW - 1, borderY + borderH - 1); |
| } |
| } |
| |
| // paint text |
| recalculateWraps(); |
| |
| if (myTooSmall) { // TODO ??? |
| return; |
| } |
| |
| int fontHeight = getCurrFontMetrics().getHeight(); |
| int currBaseLine = getCurrFontMetrics().getAscent(); |
| currBaseLine += myTextInsets.top; |
| g.setFont(getFont()); |
| g.setColor(fgColor); |
| UIUtil.applyRenderingHints(g); |
| |
| if (!StringUtil.isEmpty(myPrefix)) { |
| g.drawString(myPrefix, myTextInsets.left - myPrefixWidth + 1, currBaseLine); |
| } |
| |
| for (int i = 0; i < myWraps.size(); i++) { |
| String currLine = (String)myWraps.get(i); |
| g.drawString(currLine, myTextInsets.left, currBaseLine); |
| currBaseLine += fontHeight; // first is getCurrFontMetrics().getAscent() |
| } |
| } |
| |
| public void setText(String[] lines, String prefix) { |
| myLines = lines; |
| myTextLength = 0; |
| for (int i = 0; i < lines.length; i++) { |
| myTextLength += lines[i].length(); |
| } |
| myPrefix = prefix; |
| |
| myWrapsCalculated = false; |
| myHeightCalculated = -1; |
| myWrapsCalculatedForWidth = -1; |
| } |
| |
| public void setIcon(Icon icon) { |
| myIcon = icon; |
| |
| myWrapsCalculated = false; |
| myHeightCalculated = -1; |
| myWrapsCalculatedForWidth = -1; |
| } |
| |
| public Dimension getMinimumSize() { |
| if (getFont() != null) { |
| int minHeight = getCurrFontMetrics().getHeight(); |
| return new Dimension(minHeight, minHeight); |
| } |
| return new Dimension( |
| MIN_WIDTH + myTextInsets.left + myTextInsets.right, |
| MIN_WIDTH + myTextInsets.top + myTextInsets.bottom |
| ); |
| } |
| |
| private static final int MIN_WIDTH = 10; |
| |
| // Calculates height for current width. |
| public Dimension getPreferredSize() { |
| recalculateWraps(); |
| return new Dimension(myWrapsCalculatedForWidth, myHeightCalculated); |
| } |
| |
| // Calculate wraps for the current width |
| private void recalculateWraps() { |
| int currwidth = getWidth(); |
| if (myWrapsCalculated) { |
| if (currwidth == myWrapsCalculatedForWidth) { |
| return; |
| } |
| else { |
| myWrapsCalculated = false; |
| } |
| } |
| int wrapsCount = calculateWraps(currwidth); |
| myTooSmall = (wrapsCount == -1); |
| if (myTooSmall) { |
| wrapsCount = myTextLength; |
| } |
| int fontHeight = getCurrFontMetrics().getHeight(); |
| myHeightCalculated = wrapsCount * fontHeight + myTextInsets.top + myTextInsets.bottom; |
| myHeightCalculated = Math.max(myMinHeight, myHeightCalculated); |
| |
| int maxWidth = 0; |
| for (int i=0; i < myWraps.size(); i++) { |
| String s = (String)myWraps.get(i); |
| int width = getCurrFontMetrics().stringWidth(s); |
| maxWidth = Math.max(maxWidth, width); |
| } |
| |
| myWrapsCalculatedForWidth = myTextInsets.left + maxWidth + myTextInsets.right; |
| myWrapsCalculated = true; |
| } |
| |
| private int calculateWraps(int width) { |
| myTooSmall = width < MIN_WIDTH; |
| if (myTooSmall) { |
| return -1; |
| } |
| |
| int result = 0; |
| myWraps = new ArrayList(); |
| |
| for (int i = 0; i < myLines.length; i++) { |
| String aLine = myLines[i]; |
| int lineFirstChar = 0; |
| int lineLastChar = aLine.length() - 1; |
| int currFirst = lineFirstChar; |
| int printableWidth = width - myTextInsets.left - myTextInsets.right; |
| if (aLine.length() == 0) { |
| myWraps.add(aLine); |
| result++; |
| } |
| else { |
| while (currFirst <= lineLastChar) { |
| int currLast = calculateLastVisibleChar(aLine, printableWidth, currFirst, lineLastChar); |
| if (currLast < lineLastChar) { |
| int currChar = currLast + 1; |
| if (!Character.isWhitespace(aLine.charAt(currChar))) { |
| while (currChar >= currFirst) { |
| if (Character.isWhitespace(aLine.charAt(currChar))) { |
| break; |
| } |
| currChar--; |
| } |
| if (currChar > currFirst) { |
| currLast = currChar; |
| } |
| } |
| } |
| myWraps.add(aLine.substring(currFirst, currLast + 1)); |
| currFirst = currLast + 1; |
| while ((currFirst <= lineLastChar) && (Character.isWhitespace(aLine.charAt(currFirst)))) { |
| currFirst++; |
| } |
| result++; |
| } |
| } |
| } |
| return result; |
| } |
| |
| private int calculateLastVisibleChar(String line, int viewWidth, int firstChar, int lastChar) { |
| if (firstChar == lastChar) return lastChar; |
| if (firstChar > lastChar) throw new IllegalArgumentException("firstChar=" + firstChar + ", lastChar=" + lastChar); |
| int totalWidth = getCurrFontMetrics().stringWidth(line.substring(firstChar, lastChar + 1)); |
| if (totalWidth == 0 || viewWidth > totalWidth) { |
| return lastChar; |
| } |
| else { |
| int newApprox = (lastChar - firstChar + 1) * viewWidth / totalWidth; |
| int currChar = firstChar + Math.max(newApprox - 1, 0); |
| int currWidth = getCurrFontMetrics().stringWidth(line.substring(firstChar, currChar + 1)); |
| while (true) { |
| if (currWidth > viewWidth) { |
| currChar--; |
| if (currChar <= firstChar) { |
| return firstChar; |
| } |
| currWidth -= getCurrFontMetrics().charWidth(line.charAt(currChar + 1)); |
| if (currWidth <= viewWidth) { |
| return currChar; |
| } |
| } |
| else { |
| currChar++; |
| if (currChar > lastChar) { |
| return lastChar; |
| } |
| currWidth += getCurrFontMetrics().charWidth(line.charAt(currChar)); |
| if (currWidth >= viewWidth) { |
| return currChar - 1; |
| } |
| } |
| } |
| } |
| } |
| |
| private int getChildIndent(JTree tree) { |
| TreeUI newUI = tree.getUI(); |
| if (newUI instanceof javax.swing.plaf.basic.BasicTreeUI) { |
| javax.swing.plaf.basic.BasicTreeUI btreeui = (javax.swing.plaf.basic.BasicTreeUI)newUI; |
| return btreeui.getLeftChildIndent() + btreeui.getRightChildIndent(); |
| } |
| else { |
| return ((Integer)UIUtil.getTreeLeftChildIndent()).intValue() + ((Integer)UIUtil.getTreeRightChildIndent()).intValue(); |
| } |
| } |
| |
| private int getAvailableWidth(Object forValue, JTree tree) { |
| DefaultMutableTreeNode node = (DefaultMutableTreeNode)forValue; |
| int busyRoom = tree.getInsets().left + tree.getInsets().right + getChildIndent(tree) * node.getLevel(); |
| return tree.getVisibleRect().width - busyRoom - 2; |
| } |
| |
| protected abstract void initComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus); |
| |
| public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { |
| setFont(UIUtil.getTreeFont()); |
| |
| initComponent(tree, value, selected, expanded, leaf, row, hasFocus); |
| |
| mySelected = selected; |
| myHasFocus = hasFocus; |
| |
| myTree = tree; |
| |
| int availWidth = getAvailableWidth(value, tree); |
| if (availWidth > 0) { |
| setSize(availWidth, 100); // height will be calculated automatically |
| } |
| |
| int leftInset = myLabelInsets.left; |
| |
| if (myIcon != null) { |
| leftInset += myIcon.getIconWidth() + 2; |
| } |
| |
| if (!StringUtil.isEmpty(myPrefix)) { |
| myPrefixWidth = getCurrFontMetrics().stringWidth(myPrefix) + 5; |
| leftInset += myPrefixWidth; |
| } |
| |
| setTextInsets(new Insets(myLabelInsets.top, leftInset, myLabelInsets.bottom, myLabelInsets.right)); |
| if (myIcon != null) { |
| setMinHeight(myIcon.getIconHeight()); |
| } |
| else { |
| setMinHeight(1); |
| } |
| |
| setSize(getPreferredSize()); |
| recalculateWraps(); |
| |
| return this; |
| } |
| |
| public static JScrollPane installRenderer(final JTree tree, final MultilineTreeCellRenderer renderer) { |
| final TreeCellRenderer defaultRenderer = tree.getCellRenderer(); |
| |
| JScrollPane scrollpane = new JBScrollPane(tree){ |
| private int myAddRemoveCounter = 0; |
| private boolean myShouldResetCaches = false; |
| public void setSize(Dimension d) { |
| boolean isChanged = getWidth() != d.width || myShouldResetCaches; |
| super.setSize(d); |
| if (isChanged) resetCaches(); |
| } |
| |
| public void reshape(int x, int y, int w, int h) { |
| boolean isChanged = w != getWidth() || myShouldResetCaches; |
| super.reshape(x, y, w, h); |
| if (isChanged) resetCaches(); |
| } |
| |
| private void resetCaches() { |
| resetHeightCache(tree, defaultRenderer, renderer); |
| myShouldResetCaches = false; |
| } |
| |
| public void addNotify() { |
| super.addNotify(); //To change body of overriden methods use Options | File Templates. |
| if (myAddRemoveCounter == 0) myShouldResetCaches = true; |
| myAddRemoveCounter++; |
| } |
| |
| public void removeNotify() { |
| super.removeNotify(); //To change body of overriden methods use Options | File Templates. |
| myAddRemoveCounter--; |
| } |
| }; |
| scrollpane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); |
| scrollpane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); |
| |
| tree.setCellRenderer(renderer); |
| |
| scrollpane.addComponentListener(new ComponentAdapter() { |
| public void componentResized(ComponentEvent e) { |
| resetHeightCache(tree, defaultRenderer, renderer); |
| } |
| |
| public void componentShown(ComponentEvent e) { |
| // componentResized not called when adding to opened tool window. |
| // Seems to be BUG#4765299, however I failed to create same code to reproduce it. |
| // To reproduce it with IDEA: 1. remove this method, 2. Start any Ant task, 3. Keep message window open 4. start Ant task again. |
| resetHeightCache(tree, defaultRenderer, renderer); |
| } |
| }); |
| |
| return scrollpane; |
| } |
| |
| private static void resetHeightCache(final JTree tree, |
| final TreeCellRenderer defaultRenderer, |
| final MultilineTreeCellRenderer renderer) { |
| tree.setCellRenderer(defaultRenderer); |
| tree.setCellRenderer(renderer); |
| } |
| |
| // private static class DelegatingScrollablePanel extends JPanel implements Scrollable { |
| // private final Scrollable myDelegatee; |
| // |
| // public DelegatingScrollablePanel(Scrollable delegatee) { |
| // super(new BorderLayout(0, 0)); |
| // myDelegatee = delegatee; |
| // add((JComponent)delegatee, BorderLayout.CENTER); |
| // } |
| // |
| // public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { |
| // return myDelegatee.getScrollableUnitIncrement(visibleRect, orientation, direction); |
| // } |
| // |
| // public boolean getScrollableTracksViewportWidth() { |
| // return myDelegatee.getScrollableTracksViewportWidth(); |
| // } |
| // |
| // public Dimension getPreferredScrollableViewportSize() { |
| // return myDelegatee.getPreferredScrollableViewportSize(); |
| // } |
| // |
| // public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { |
| // return myDelegatee.getScrollableBlockIncrement(visibleRect, orientation, direction); |
| // } |
| // |
| // public boolean getScrollableTracksViewportHeight() { |
| // return myDelegatee.getScrollableTracksViewportHeight(); |
| // } |
| // } |
| } |
| |