package com.explodingpixels.macwidgets.plaf;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JTree;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.AbstractLayoutCache;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import com.explodingpixels.macwidgets.MacFontUtils;
import com.explodingpixels.macwidgets.MacWidgetFactory;
import com.explodingpixels.macwidgets.SourceList;
import com.explodingpixels.macwidgets.SourceListBadgeContentProvider;
import com.explodingpixels.macwidgets.SourceListCategory;
import com.explodingpixels.macwidgets.SourceListColorScheme;
import com.explodingpixels.macwidgets.SourceListCountBadgeRenderer;
import com.explodingpixels.macwidgets.SourceListModel;
import com.explodingpixels.macwidgets.SourceListStandardColorScheme;
import com.explodingpixels.painter.FocusStatePainter;
import com.explodingpixels.painter.RectanglePainter;
import com.explodingpixels.widgets.IconProvider;
import com.explodingpixels.widgets.TextProvider;
import com.explodingpixels.widgets.TreeUtils;
import com.explodingpixels.widgets.WindowUtils;
import com.jgoodies.forms.builder.PanelBuilder;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
/**
* <p>
* A UI delegate that paints a {@link javax.swing.JTree} as an <a href="http://developer.apple.com/documentation/UserExperience/Conceptual/AppleHIGuidelines/XHIGWindows/chapter_18_section_4.html#//apple_ref/doc/uid/20000961-CHDDIGDE">Apple defined</a>
* Source List. Consider using this UI delegate with
* {@link com.explodingpixels.macwidgets.MacWidgetFactory#createSourceListScrollPane(javax.swing.JComponent)}.
* </p>
* <p>
* For the best development experience, it is recommended that you migrate your code to use the
* {@link com.explodingpixels.macwidgets.SourceList} with the {@link com.explodingpixels.macwidgets.SourceListModel}, as this component abstracts away many of the
* complexities of {@code JTree}.
* </p>
* <p>
* Pictured below are the two different rendering styles of a Source List: focused and unfocused.
* The corresponding {@code JTree}'s focusable property drives this rendering style.
* </p>
* <br>
* <table>
* <tr><td align="center"><img src="../../../../../graphics/iTunesSourceList.png"></td>
* <td align="center"><img src="../../../../../graphics/MailSourceList.png"></td></tr>
* <tr><td align="center"><font size="2" face="arial"><b>Focusable SourceList<b></font></td>
* <td align="center"><font size="2" face="arial"><b>Non-focusable SourceList<b></font></td></tr>
* </table>
* <br>
* <h3>Providing Category and Item text and icons</h3>
* <p/>
* During the rendering process, each Category and Item node will be consulted for the text to be
* displayed. The renderer determines the text based on these prioritized checks:
* <ol>
* <li>If the node is an instance of {@link javax.swing.tree.DefaultMutableTreeNode}, and the
* {@link javax.swing.tree.DefaultMutableTreeNode#getUserObject()} is an instance of {@link com.explodingpixels.widgets.TextProvider}, then
* the {@code TextProvider} will be queried for the node text.</li>
* <li>If no implementation of {@code TextProvider} is found, the standard
* {@link javax.swing.JTree#convertValueToText(Object, boolean, boolean, boolean, int, boolean)} method will
* be consulted.</li>
* </ol>
* Also, during rendering, each Item node will be consulted for an icon. Similarly to the above
* mechanism for determining text, the render determines a nodes icon by the following check:
* <ol>
* <li>If the node is an instance of {@link javax.swing.tree.DefaultMutableTreeNode}, and the
* {@link javax.swing.tree.DefaultMutableTreeNode#getUserObject()} is an instance of {@link com.explodingpixels.widgets.IconProvider}, then
* the {@code IconProvider} will be queried for the node icon.</li>
* </ol>
*/
public class SourceListTreeUI extends BasicTreeUI {
private Font categoryFont = MacFontUtils.BOLD_LABEL_FONT;
private Font itemFont = MacFontUtils.DEFAULT_LABEL_FONT;
private Font itemSelectedFont = itemFont.deriveFont(Font.BOLD);
private static final Color TRANSPARENT_COLOR = new Color(0, 0, 0, 0);
private final String SELECT_NEXT = "selectNext";
private final String SELECT_PREVIOUS = "selectPrevious";
private SourceListColorScheme fColorScheme;
private FocusStatePainter fBackgroundPainter;
private FocusStatePainter fSelectionBackgroundPainter;
private CustomTreeModelListener fTreeModelListener = new CustomTreeModelListener();
@Override
protected void completeUIInstall() {
super.completeUIInstall();
tree.setSelectionModel(new SourceListTreeSelectionModel());
tree.setOpaque(false);
tree.setRootVisible(false);
tree.setLargeModel(true);
tree.setRootVisible(false);
tree.setShowsRootHandles(true);
// TODO key height off font size.
tree.setRowHeight(20);
// install the default color scheme.
setColorScheme(new SourceListStandardColorScheme());
}
public Font getCategoryFont() {
return categoryFont;
}
public void setCategoryFont(Font categoryFont) {
this.categoryFont = categoryFont;
}
public Font getItemFont() {
return itemFont;
}
public void setItemFont(Font itemFont) {
this.itemFont = itemFont;
}
public Font getItemSelectedFont() {
return itemSelectedFont;
}
public void setItemSelectedFont(Font itemSelectedFont) {
this.itemSelectedFont = itemSelectedFont;
}
@Override
protected void installListeners() {
super.installListeners();
// install a property change listener that repaints the JTree when the parent window's
// focus state changes.
WindowUtils.installJComponentRepainterOnWindowFocusChanged(tree);
}
@Override
protected void installKeyboardActions() {
super.installKeyboardActions();
tree.getInputMap().put(KeyStroke.getKeyStroke("pressed DOWN"), SELECT_NEXT);
tree.getInputMap().put(KeyStroke.getKeyStroke("pressed UP"), SELECT_PREVIOUS);
tree.getActionMap().put(SELECT_NEXT, createNextAction());
tree.getActionMap().put(SELECT_PREVIOUS, createPreviousAction());
}
@Override
protected void setModel(TreeModel model) {
// if there was a previously installed TreeModel, uninstall our listener from it.
if (treeModel != null) {
treeModel.removeTreeModelListener(fTreeModelListener);
}
super.setModel(model);
// install our listener on the new TreeModel if neccessary.
if (model != null) {
model.addTreeModelListener(new CustomTreeModelListener());
}
}
/**
* Gets the {@link com.explodingpixels.macwidgets.SourceListColorScheme} that this {@code SourceListTreeUI} uses to paint.
*
* @return the {@link com.explodingpixels.macwidgets.SourceListColorScheme} that this {@code SourceList} uses to paint.
*/
public SourceListColorScheme getColorScheme() {
return fColorScheme;
}
/**
* Sets the {@link com.explodingpixels.macwidgets.SourceListColorScheme} that this {@code SourceListTreeUI} uses to paint.
*
* @param colorScheme the {@link com.explodingpixels.macwidgets.SourceListColorScheme} that this {@code SourceList} uses to
* paint.
*/
public void setColorScheme(SourceListColorScheme colorScheme) {
checkColorSchemeNotNull(colorScheme);
fColorScheme = colorScheme;
fBackgroundPainter = new FocusStatePainter(
new RectanglePainter(fColorScheme.getActiveBackgroundColor()),
new RectanglePainter(fColorScheme.getActiveBackgroundColor()),
new RectanglePainter(fColorScheme.getInactiveBackgroundColor()));
fSelectionBackgroundPainter = new FocusStatePainter(
fColorScheme.getActiveFocusedSelectedItemPainter(),
fColorScheme.getActiveUnfocusedSelectedItemPainter(),
fColorScheme.getInactiveSelectedItemPainter());
// create a new tree cell renderer in order to pick up the new colors.
tree.setCellRenderer(new SourceListTreeCellRenderer());
installDisclosureIcons();
}
private void installDisclosureIcons() {
// install the collapsed and expanded icons as well as the margins to indent nodes.
setCollapsedIcon(fColorScheme.getUnselectedCollapsedIcon());
setExpandedIcon(fColorScheme.getUnselectedExpandedIcon());
int indent = fColorScheme.getUnselectedCollapsedIcon().getIconWidth() / 2 + 4;
setLeftChildIndent(indent);
setRightChildIndent(indent);
}
@Override
protected void paintExpandControl(Graphics g, Rectangle clipBounds, Insets insets,
Rectangle bounds, TreePath path, int row, boolean isExpanded,
boolean hasBeenExpanded, boolean isLeaf) {
// if the given path is selected, then
boolean isPathSelected = tree.getSelectionModel().isPathSelected(path);
Icon expandIcon = isPathSelected ? fColorScheme.getSelectedExpandedIcon()
: fColorScheme.getUnselectedExpandedIcon();
Icon collapseIcon = isPathSelected ? fColorScheme.getSelectedCollapsedIcon()
: fColorScheme.getUnselectedCollapsedIcon();
Object categoryOrItem =
((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
boolean setIcon = !(categoryOrItem instanceof SourceListCategory)
|| ((SourceListCategory) categoryOrItem).isCollapsable();
setExpandedIcon(setIcon ? expandIcon : null);
setCollapsedIcon(setIcon ? collapseIcon : null);
super.paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
}
@Override
protected AbstractLayoutCache.NodeDimensions createNodeDimensions() {
return new NodeDimensionsHandler() {
@Override
public Rectangle getNodeDimensions(
Object value, int row, int depth, boolean expanded, Rectangle size) {
Rectangle dimensions = super.getNodeDimensions(value, row, depth, expanded, size);
int containerWidth = tree.getParent() instanceof JViewport
? tree.getParent().getWidth() : tree.getWidth();
dimensions.width = containerWidth - getRowX(row, depth);
return dimensions;
}
};
}
@Override
public Rectangle getPathBounds(JTree tree, TreePath path) {
Rectangle bounds = super.getPathBounds(tree, path);
// if there are valid bounds for the given path, then stretch them to fill the entire width
// of the tree. this allows repaints on focus events to follow the standard code path, and
// still repaint the entire selected area.
if (bounds != null) {
bounds.x = 0;
bounds.width = tree.getWidth();
}
return bounds;
}
@Override
public void paint(Graphics g, JComponent c) {
// TODO use c.getVisibleRect to trim painting to minimum rectangle.
// paint the background for the tree.
Graphics2D backgroundGraphics = (Graphics2D) g.create();
fBackgroundPainter.paint(backgroundGraphics, c, c.getWidth(), c.getHeight());
backgroundGraphics.dispose();
// TODO use c.getVisibleRect to trim painting to minimum rectangle.
// paint the background for the selected entry, if there is one.
int selectedRow = getSelectionModel().getLeadSelectionRow();
if (selectedRow >= 0 && tree.isVisible(tree.getPathForRow(selectedRow))) {
Rectangle bounds = tree.getRowBounds(selectedRow);
Graphics2D selectionBackgroundGraphics = (Graphics2D) g.create();
selectionBackgroundGraphics.translate(0, bounds.y);
fSelectionBackgroundPainter.paint(
selectionBackgroundGraphics, c, c.getWidth(), bounds.height);
selectionBackgroundGraphics.dispose();
}
super.paint(g, c);
}
@Override
protected void paintHorizontalLine(Graphics g, JComponent c, int y, int left, int right) {
// do nothing - don't paint horizontal lines.
}
@Override
protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets,
TreePath path) {
// do nothing - don't paint vertical lines.
}
@Override
protected void selectPathForEvent(TreePath path, MouseEvent event) {
// only forward on the selection event if an area other than the expand/collapse control
// was clicked. this typically isn't an issue with regular Swing JTrees, however, SourceList
// tree nodes fill the entire width of the tree. thus their bounds are underneath the
// expand/collapse control.
if (!isLocationInExpandControl(path, event.getX(), event.getY())) {
super.selectPathForEvent(path, event);
}
}
private Action createNextAction() {
return new AbstractAction() {
public void actionPerformed(ActionEvent e) {
int selectedRow = tree.getLeadSelectionRow();
int rowToSelect = selectedRow + 1;
while (rowToSelect >= 0 && rowToSelect < tree.getRowCount()) {
if (isItemRow(rowToSelect)) {
tree.setSelectionRow(rowToSelect);
break;
} else {
rowToSelect++;
}
}
}
};
}
private Action createPreviousAction() {
return new AbstractAction() {
public void actionPerformed(ActionEvent e) {
int selectedRow = tree.getLeadSelectionRow();
int rowToSelect = selectedRow - 1;
while (rowToSelect >= 0 && rowToSelect < tree.getRowCount()) {
if (isItemRow(rowToSelect)) {
tree.setSelectionRow(rowToSelect);
break;
} else {
rowToSelect--;
}
}
}
};
}
// Utility methods. ///////////////////////////////////////////////////////////////////////////
private boolean isCategoryRow(int row) {
return !isItemRow(row);
}
private boolean isItemRow(int row) {
return isItemPath(tree.getPathForRow(row));
}
private boolean isItemPath(TreePath path) {
return path != null && path.getPathCount() > 2;
}
private String getTextForNode(TreeNode node, boolean selected, boolean expanded, boolean leaf,
int row, boolean hasFocus) {
String retVal;
if (node instanceof DefaultMutableTreeNode
&& ((DefaultMutableTreeNode) node).getUserObject() instanceof TextProvider) {
Object userObject = ((DefaultMutableTreeNode) node).getUserObject();
retVal = ((TextProvider) userObject).getText();
} else {
retVal = tree.convertValueToText(node, selected, expanded, leaf, row, hasFocus);
}
return retVal;
}
private Icon getIconForNode(TreeNode node) {
Icon retVal = null;
if (node instanceof DefaultMutableTreeNode
&& ((DefaultMutableTreeNode) node).getUserObject() instanceof IconProvider) {
Object userObject = ((DefaultMutableTreeNode) node).getUserObject();
retVal = ((IconProvider) userObject).getIcon();
}
return retVal;
}
private static void checkColorSchemeNotNull(SourceListColorScheme colorScheme) {
if (colorScheme == null) {
throw new IllegalArgumentException("The given SourceListColorScheme cannot be null.");
}
}
// Custom TreeModelListener. //////////////////////////////////////////////////////////////////
private class CustomTreeModelListener implements TreeModelListener {
public void treeNodesChanged(TreeModelEvent e) {
// no implementation.
}
public void treeNodesInserted(TreeModelEvent e) {
TreePath path = e.getTreePath();
Object root = tree.getModel().getRoot();
TreePath pathToRoot = new TreePath(root);
if (path != null && path.getParentPath() != null
&& path.getParentPath().getLastPathComponent().equals(root)
&& !tree.isExpanded(pathToRoot)) {
TreeUtils.expandPathOnEdt(tree, new TreePath(root));
}
}
public void treeNodesRemoved(TreeModelEvent e) {
// no implementation.
}
public void treeStructureChanged(TreeModelEvent e) {
// no implementation.
}
}
// Custom TreeCellRenderer. ///////////////////////////////////////////////////////////////////
private class SourceListTreeCellRenderer implements TreeCellRenderer {
private CategoryTreeCellRenderer iCategoryRenderer = new CategoryTreeCellRenderer();
private ItemTreeCellRenderer iItemRenderer = new ItemTreeCellRenderer();
public Component getTreeCellRendererComponent(
JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
TreeCellRenderer render = isCategoryRow(row) ? iCategoryRenderer : iItemRenderer;
return render.getTreeCellRendererComponent(
tree, value, selected, expanded, leaf, row, hasFocus);
}
}
private class CategoryTreeCellRenderer implements TreeCellRenderer {
private JLabel fLabel = MacWidgetFactory.makeEmphasizedLabel(new JLabel(),
fColorScheme.getCategoryTextColor(),
fColorScheme.getCategoryTextColor(),
fColorScheme.getCategoryTextShadowColor());
private CategoryTreeCellRenderer() {
}
public Component getTreeCellRendererComponent(
JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
fLabel.setFont(getCategoryFont());
TreeNode node = (TreeNode) value;
fLabel.setText(getTextForNode(node, selected, expanded, leaf, row, hasFocus).toUpperCase());
return fLabel;
}
}
private class ItemTreeCellRenderer implements TreeCellRenderer {
private PanelBuilder fBuilder;
private SourceListCountBadgeRenderer fCountRenderer = new SourceListCountBadgeRenderer(
fColorScheme.getSelectedBadgeColor(), fColorScheme.getActiveUnselectedBadgeColor(),
fColorScheme.getInativeUnselectedBadgeColor(), fColorScheme.getBadgeTextColor());
private JLabel fSelectedLabel = MacWidgetFactory.makeEmphasizedLabel(new JLabel(),
fColorScheme.getSelectedItemTextColor(),
fColorScheme.getSelectedItemTextColor(),
fColorScheme.getSelectedItemFontShadowColor());
private JLabel fUnselectedLabel = MacWidgetFactory.makeEmphasizedLabel(new JLabel(),
fColorScheme.getUnselectedItemTextColor(),
fColorScheme.getUnselectedItemTextColor(),
TRANSPARENT_COLOR);
private ItemTreeCellRenderer() {
// definte the FormLayout columns and rows.
FormLayout layout = new FormLayout("fill:0px:grow, 5px, p, 5px", "3px, fill:p:grow, 3px");
// create the builders with our panels as the component to be filled.
fBuilder = new PanelBuilder(layout);
fBuilder.getPanel().setOpaque(false);
}
public Component getTreeCellRendererComponent(
JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
fSelectedLabel.setFont(getItemSelectedFont());
fUnselectedLabel.setFont(getItemFont());
TreeNode node = (TreeNode) value;
JLabel label = selected ? fSelectedLabel : fUnselectedLabel;
label.setText(getTextForNode(node, selected, expanded, leaf, row, hasFocus));
label.setIcon(getIconForNode(node));
fBuilder.getPanel().removeAll();
CellConstraints cc = new CellConstraints();
fBuilder.add(label, cc.xywh(1, 1, 1, 3));
if (value instanceof DefaultMutableTreeNode
&& ((DefaultMutableTreeNode) value).getUserObject() instanceof SourceListBadgeContentProvider) {
Object userObject = ((DefaultMutableTreeNode) node).getUserObject();
SourceListBadgeContentProvider badgeContentProvider =
(SourceListBadgeContentProvider) userObject;
if (badgeContentProvider.getCounterValue() > 0) {
fBuilder.add(fCountRenderer.getComponent(), cc.xy(3, 2, "center, fill"));
fCountRenderer.setState(badgeContentProvider.getCounterValue(), selected);
}
}
return fBuilder.getPanel();
}
}
// SourceListTreeSelectionModel implementation. ///////////////////////////////////////////////
private class SourceListTreeSelectionModel extends DefaultTreeSelectionModel {
public SourceListTreeSelectionModel() {
setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
}
private boolean canSelect(TreePath path) {
return isItemPath(path);
}
@Override
public void setSelectionPath(TreePath path) {
if (canSelect(path)) {
super.setSelectionPath(path);
}
}
@Override
public void setSelectionPaths(TreePath[] paths) {
if (canSelect(paths[0])) {
super.setSelectionPaths(paths);
}
}
}
}