/* * Copyright (C) 2014 The Android Open Source Project * * 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.android.tools.idea.ui; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.JBColor; import com.intellij.ui.LoadingNode; import com.intellij.ui.SimpleTextAttributes; import com.intellij.ui.components.JBScrollPane; import com.intellij.util.ui.EmptyIcon; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.tree.WideSelectionTreeUI; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.plaf.TreeUI; import javax.swing.plaf.basic.BasicTreeUI; 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; public abstract class MultilineColoredTreeCellRenderer extends WrapAwareColoredComponent implements TreeCellRenderer { @NonNls protected static final String FONT_PROPERTY_NAME = "font"; private static final Icon LOADING_NODE_ICON = new EmptyIcon(8, 16); @NotNull private final Insets myLabelInsets = new Insets(1, 2, 1, 2); @Nullable private String myPrefix; private int myPrefixWidth; private int myMinHeight; /** * Defines whether the tree is selected or not */ protected boolean mySelected; /** * Defines whether the tree has focus or not */ private boolean myFocused; private boolean myFocusedCalculated; @Nullable protected JTree myTree; private boolean myOpaque = true; protected MultilineColoredTreeCellRenderer() { setWrapText(true); addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (FONT_PROPERTY_NAME.equalsIgnoreCase(evt.getPropertyName())) { onFontChanged(); } } }); } protected void setMinHeight(int height) { myMinHeight = height; } private void onFontChanged() { resetTextLayoutCache(); } @NotNull private FontMetrics getCurrFontMetrics() { return getFontMetrics(getFont()); } public void setText(@NotNull String[] lines, @Nullable String prefix) { myPrefix = prefix; for (int i = 0; i < lines.length; i++) { append(lines[i]); if (i < lines.length - 1) { appendLineBreak(); } } } @Override protected void beforePaintText(@NotNull Graphics g, int x, int textBaseLine) { if (!StringUtil.isEmpty(myPrefix)) { g.drawString(myPrefix, x - myPrefixWidth + 1, textBaseLine); } } @NotNull @Override public Dimension getMinimumSize() { Dimension preferredSize = getPreferredSize(); Dimension result = new Dimension(preferredSize); Insets padding = getIpad(); result.width = Math.max(result.width, padding.left + padding.right); result.height = Math.max(myMinHeight, Math.max(result.height, padding.top + padding.bottom)); return result; } private static int getChildIndent(@NotNull JTree tree) { TreeUI newUI = tree.getUI(); if (newUI instanceof BasicTreeUI) { BasicTreeUI ui = (BasicTreeUI)newUI; return ui.getLeftChildIndent() + ui.getRightChildIndent(); } else { return ((Integer)UIUtil.getTreeLeftChildIndent()).intValue() + ((Integer)UIUtil.getTreeRightChildIndent()).intValue(); } } private static int getAvailableWidth(@NotNull Object forValue, @NotNull 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(@NotNull JTree tree, @Nullable Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus); public void customizeCellRenderer(@NotNull JTree tree, @NotNull Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { setFont(UIUtil.getTreeFont()); initComponent(tree, value, selected, expanded, leaf, row, hasFocus); int availWidth = getAvailableWidth(value, tree); if (availWidth > 0) { setSize(availWidth, 100); // height will be calculated automatically } int leftInset = myLabelInsets.left; Icon icon = getIcon(); if (icon != null) { leftInset += icon.getIconWidth() + 2; } if (!StringUtil.isEmpty(myPrefix)) { myPrefixWidth = getCurrFontMetrics().stringWidth(myPrefix) + 5; leftInset += myPrefixWidth; } setIpad(new Insets(myLabelInsets.top, leftInset, myLabelInsets.bottom, myLabelInsets.right)); if (icon != null) { setMinHeight(icon.getIconHeight()); } else { setMinHeight(1); } setSize(getPreferredSize()); resetTextLayoutCache(); } @SuppressWarnings("IfStatementWithIdenticalBranches") @Override public final Component getTreeCellRendererComponent(@NotNull JTree tree, @NotNull Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { myTree = tree; clear(); mySelected = selected; myFocusedCalculated = false; // We paint background if and only if tree path is selected and tree has focus. // If path is selected and tree is not focused then we just paint focused border. if (UIUtil.isFullRowSelectionLAF()) { setBackground(selected ? UIUtil.getTreeSelectionBackground() : null); } else if (tree.getUI() instanceof WideSelectionTreeUI && ((WideSelectionTreeUI)tree.getUI()).isWideSelection()) { setPaintFocusBorder(false); if (selected) { setBackground(hasFocus ? UIUtil.getTreeSelectionBackground() : UIUtil.getTreeUnfocusedSelectionBackground()); } } else if (selected) { setPaintFocusBorder(true); if (isFocused()) { setBackground(UIUtil.getTreeSelectionBackground()); } else { setBackground(null); } } else { setBackground(null); } if (value instanceof LoadingNode) { setForeground(JBColor.GRAY); setIcon(LOADING_NODE_ICON); } else { setForeground(tree.getForeground()); setIcon(null); } if (UIUtil.isUnderGTKLookAndFeel()) { super.setOpaque(false); // avoid nasty background super.setIconOpaque(false); } else if (UIUtil.isUnderNimbusLookAndFeel() && selected && hasFocus) { super.setOpaque(false); // avoid erasing Nimbus focus frame super.setIconOpaque(false); } else if (tree.getUI() instanceof WideSelectionTreeUI && ((WideSelectionTreeUI)tree.getUI()).isWideSelection()) { super.setOpaque(false); // avoid erasing Nimbus focus frame super.setIconOpaque(false); } else { super.setOpaque(myOpaque || selected && hasFocus || selected && isFocused()); // draw selection background even for non-opaque tree } if (tree.getUI() instanceof WideSelectionTreeUI && UIUtil.isUnderAquaBasedLookAndFeel()) { setMyBorder(null); setIpad(new Insets(0, 2, 0, 2)); } customizeCellRenderer(tree, value, selected, expanded, leaf, row, hasFocus); return this; } @NotNull public static JScrollPane installRenderer(@NotNull final JTree tree, @NotNull final MultilineColoredTreeCellRenderer renderer) { final TreeCellRenderer defaultRenderer = tree.getCellRenderer(); JScrollPane scrollPane = new JBScrollPane(tree){ private int myAddRemoveCounter = 0; private boolean myShouldResetCaches = false; @Override public void setSize(Dimension d) { boolean isChanged = getWidth() != d.width || myShouldResetCaches; super.setSize(d); if (isChanged) resetCaches(); } @Override public void setBounds(int x, int y, int width, int height) { boolean isChanged = width != getWidth() || myShouldResetCaches; super.setBounds(x, y, width, height); if (isChanged) resetCaches(); } private void resetCaches() { resetHeightCache(tree, defaultRenderer, renderer); myShouldResetCaches = false; } @Override public void addNotify() { super.addNotify(); if (myAddRemoveCounter == 0) myShouldResetCaches = true; myAddRemoveCounter++; } @Override public void removeNotify() { super.removeNotify(); myAddRemoveCounter--; } }; scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS); scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); tree.setCellRenderer(renderer); scrollPane.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { resetHeightCache(tree, defaultRenderer, renderer); } @Override 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(@NotNull final JTree tree, @NotNull final TreeCellRenderer defaultRenderer, @NotNull final MultilineColoredTreeCellRenderer renderer) { tree.setCellRenderer(defaultRenderer); tree.setCellRenderer(renderer); } @Nullable public JTree getTree() { return myTree; } protected final boolean isFocused() { if (!myFocusedCalculated) { myFocused = calcFocusedState(); myFocusedCalculated = true; } return myFocused; } protected boolean calcFocusedState() { return myTree != null && myTree.hasFocus(); } @Override public void setOpaque(boolean isOpaque) { myOpaque = isOpaque; super.setOpaque(isOpaque); } @Nullable @Override public Font getFont() { Font font = super.getFont(); // Cell renderers could have no parent and no explicit set font. // Take tree font in this case. if (font != null) return font; JTree tree = getTree(); return tree != null ? tree.getFont() : null; } /** * When the item is selected then we use default tree's selection foreground. * It guaranties readability of selected text in any LAF. */ @Override public void append(@NotNull @Nls String fragment, @NotNull SimpleTextAttributes attributes, boolean isMainText) { if (mySelected && isFocused()) { super.append(fragment, new SimpleTextAttributes(attributes.getStyle(), UIUtil.getTreeSelectionForeground()), isMainText); } else if (mySelected && UIUtil.isUnderAquaBasedLookAndFeel()) { super.append(fragment, new SimpleTextAttributes(attributes.getStyle(), UIUtil.getTreeForeground()), isMainText); } else { super.append(fragment, attributes, isMainText); } } }