/*
* Copyright (c) 2009 Kathryn Huxtable and Kenneth Orr.
*
* This file is part of the SeaGlass Pluggable Look and Feel.
*
* 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.
*
* $Id: org.eclipse.jdt.ui.prefs 172 2009-10-06 18:31:12Z kathryn@kathrynhuxtable.org $
*/
package com.seaglasslookandfeel.ui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Enumeration;
import javax.swing.DefaultCellEditor;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.LookAndFeel;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.plaf.synth.ColorType;
import javax.swing.plaf.synth.Region;
import javax.swing.plaf.synth.SynthConstants;
import javax.swing.plaf.synth.SynthContext;
import javax.swing.plaf.synth.SynthLabelUI;
import javax.swing.plaf.synth.SynthStyle;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import sun.swing.plaf.synth.SynthIcon;
import com.seaglasslookandfeel.SeaGlassContext;
import com.seaglasslookandfeel.SeaGlassLookAndFeel;
import com.seaglasslookandfeel.SeaGlassStyle;
import com.seaglasslookandfeel.component.SeaGlassIcon;
/**
* @author Kathryn Huxtable
*
*/
public class SeaGlassTreeUI extends BasicTreeUI implements PropertyChangeListener, SeaglassUI {
private SynthStyle style;
private SynthStyle cellStyle;
private SeaGlassContext paintContext;
private boolean drawHorizontalLines;
private boolean drawVerticalLines;
private Object linesStyle;
private int padding;
private boolean useTreeColors;
private Icon expandedIconWrapper = new ExpandedIconWrapper();
/**
* Creates a new UI object for the given component.
*
* @param x
* component to create UI object for
* @return the UI object
*/
public static ComponentUI createUI(JComponent x) {
return new SeaGlassTreeUI();
}
/**
* @inheritDoc
*/
@Override
public Icon getExpandedIcon() {
return expandedIconWrapper;
}
/**
* @inheritDoc
*/
@Override
protected void installDefaults() {
updateStyle(tree);
}
private void updateStyle(JTree tree) {
SeaGlassContext context = getContext(tree, ENABLED);
SynthStyle oldStyle = style;
style = SeaGlassLookAndFeel.updateStyle(context, this);
if (style != oldStyle) {
Object value;
setExpandedIcon(style.getIcon(context, "Tree.expandedIcon"));
setCollapsedIcon(style.getIcon(context, "Tree.collapsedIcon"));
setLeftChildIndent(style.getInt(context, "Tree.leftChildIndent", 0));
setRightChildIndent(style.getInt(context, "Tree.rightChildIndent", 0));
drawHorizontalLines = style.getBoolean(context, "Tree.drawHorizontalLines", true);
drawVerticalLines = style.getBoolean(context, "Tree.drawVerticalLines", true);
linesStyle = style.get(context, "Tree.linesStyle");
value = style.get(context, "Tree.rowHeight");
if (value != null) {
LookAndFeel.installProperty(tree, "rowHeight", value);
}
value = style.get(context, "Tree.scrollsOnExpand");
LookAndFeel.installProperty(tree, "scrollsOnExpand", value != null ? value : Boolean.TRUE);
padding = style.getInt(context, "Tree.padding", 0);
largeModel = (tree.isLargeModel() && tree.getRowHeight() > 0);
useTreeColors = style.getBoolean(context, "Tree.rendererUseTreeColors", true);
Boolean showsRootHandles = style.getBoolean(context, "Tree.showsRootHandles", Boolean.TRUE);
LookAndFeel.installProperty(tree, JTree.SHOWS_ROOT_HANDLES_PROPERTY, showsRootHandles);
if (oldStyle != null) {
uninstallKeyboardActions();
installKeyboardActions();
}
}
context.dispose();
context = getContext(tree, Region.TREE_CELL, ENABLED);
cellStyle = SeaGlassLookAndFeel.updateStyle(context, this);
context.dispose();
}
/**
* @inheritDoc
*/
@Override
protected void installListeners() {
super.installListeners();
tree.addPropertyChangeListener(this);
}
/**
* @inheritDoc
*/
@Override
public SeaGlassContext getContext(JComponent c) {
return getContext(c, SeaGlassLookAndFeel.getComponentState(c));
}
private SeaGlassContext getContext(JComponent c, int state) {
return SeaGlassContext.getContext(SeaGlassContext.class, c, SeaGlassLookAndFeel.getRegion(c), style, state);
}
private SeaGlassContext getContext(JComponent c, Region region) {
return getContext(c, region, getComponentState(c, region));
}
private SeaGlassContext getContext(JComponent c, Region region, int state) {
return SeaGlassContext.getContext(SeaGlassContext.class, c, region, cellStyle, state);
}
private int getComponentState(JComponent c, Region region) {
// Always treat the cell as selected, will be adjusted appropriately
// when painted.
return ENABLED | SELECTED;
}
/**
* @inheritDoc
*/
@Override
protected TreeCellEditor createDefaultCellEditor() {
TreeCellRenderer renderer = tree.getCellRenderer();
DefaultTreeCellEditor editor;
if (renderer != null && (renderer instanceof DefaultTreeCellRenderer)) {
editor = new SynthTreeCellEditor(tree, (DefaultTreeCellRenderer) renderer);
} else {
editor = new SynthTreeCellEditor(tree, null);
}
return editor;
}
/**
* @inheritDoc
*/
@Override
protected TreeCellRenderer createDefaultCellRenderer() {
return new SynthTreeCellRenderer();
}
/**
* @inheritDoc
*/
@Override
protected void uninstallDefaults() {
SeaGlassContext context = getContext(tree, ENABLED);
style.uninstallDefaults(context);
context.dispose();
style = null;
context = getContext(tree, Region.TREE_CELL, ENABLED);
cellStyle.uninstallDefaults(context);
context.dispose();
cellStyle = null;
if (tree.getTransferHandler() instanceof UIResource) {
tree.setTransferHandler(null);
}
}
/**
* @inheritDoc
*/
@Override
protected void uninstallListeners() {
super.uninstallListeners();
tree.removePropertyChangeListener(this);
}
/**
* Notifies this UI delegate to repaint the specified component. This method
* paints the component background, then calls the
* {@link #paint(SynthContext,Graphics)} method.
*
* <p>
* In general, this method does not need to be overridden by subclasses. All
* Look and Feel rendering code should reside in the {@code paint} method.
*
* @param g
* the {@code Graphics} object used for painting
* @param c
* the component being painted
* @see #paint(SynthContext,Graphics)
*/
@Override
public void update(Graphics g, JComponent c) {
SeaGlassContext context = getContext(c);
SeaGlassLookAndFeel.update(context, g);
context.getPainter().paintTreeBackground(context, g, 0, 0, c.getWidth(), c.getHeight());
paint(context, g);
context.dispose();
}
/**
* @inheritDoc
*/
@Override
public void paintBorder(SynthContext context, Graphics g, int x, int y, int w, int h) {
((SeaGlassContext)context).getPainter().paintTreeBorder(context, g, x, y, w, h);
}
/**
* Paints the specified component according to the Look and Feel.
* <p>
* This method is not used by Synth Look and Feel. Painting is handled by
* the {@link #paint(SynthContext,Graphics)} method.
*
* @param g
* the {@code Graphics} object used for painting
* @param c
* the component being painted
* @see #paint(SynthContext,Graphics)
*/
@Override
public void paint(Graphics g, JComponent c) {
SeaGlassContext context = getContext(c);
paint(context, g);
context.dispose();
}
/**
* Paints the specified component.
*
* @param context
* context for the component being painted
* @param g
* the {@code Graphics} object used for painting
* @see #update(Graphics,JComponent)
*/
protected void paint(SynthContext context, Graphics g) {
paintContext = (SeaGlassContext)context;
updateLeadSelectionRow();
Rectangle paintBounds = g.getClipBounds();
Insets insets = tree.getInsets();
TreePath initialPath = getClosestPathForLocation(tree, 0, paintBounds.y);
Enumeration paintingEnumerator = treeState.getVisiblePathsFrom(initialPath);
int row = treeState.getRowForPath(initialPath);
int endY = paintBounds.y + paintBounds.height;
TreeModel treeModel = tree.getModel();
SeaGlassContext cellContext = getContext(tree, Region.TREE_CELL);
drawingCache.clear();
setHashColor(context.getStyle().getColor(context, ColorType.FOREGROUND));
if (paintingEnumerator != null) {
// First pass, draw the rows
boolean done = false;
boolean isExpanded;
boolean hasBeenExpanded;
boolean isLeaf;
Rectangle rowBounds = new Rectangle(0, 0, tree.getWidth(), 0);
Rectangle bounds;
TreePath path;
TreeCellRenderer renderer = tree.getCellRenderer();
DefaultTreeCellRenderer dtcr = (renderer instanceof DefaultTreeCellRenderer) ? (DefaultTreeCellRenderer) renderer : null;
configureRenderer(cellContext);
while (!done && paintingEnumerator.hasMoreElements()) {
path = (TreePath) paintingEnumerator.nextElement();
if (path != null) {
isLeaf = treeModel.isLeaf(path.getLastPathComponent());
if (isLeaf) {
isExpanded = hasBeenExpanded = false;
} else {
isExpanded = treeState.getExpandedState(path);
hasBeenExpanded = tree.hasBeenExpanded(path);
}
bounds = getPathBounds(tree, path);
rowBounds.y = bounds.y;
rowBounds.height = bounds.height;
paintRow(renderer, dtcr, paintContext, cellContext, g, paintBounds, insets, bounds, rowBounds, path, row, isExpanded,
hasBeenExpanded, isLeaf);
if ((bounds.y + bounds.height) >= endY) {
done = true;
}
} else {
done = true;
}
row++;
}
// Draw the connecting lines and controls.
// Find each parent and have them draw a line to their last child
boolean rootVisible = tree.isRootVisible();
TreePath parentPath = initialPath;
parentPath = parentPath.getParentPath();
while (parentPath != null) {
paintVerticalPartOfLeg(g, paintBounds, insets, parentPath);
drawingCache.put(parentPath, Boolean.TRUE);
parentPath = parentPath.getParentPath();
}
done = false;
paintingEnumerator = treeState.getVisiblePathsFrom(initialPath);
while (!done && paintingEnumerator.hasMoreElements()) {
path = (TreePath) paintingEnumerator.nextElement();
if (path != null) {
isLeaf = treeModel.isLeaf(path.getLastPathComponent());
if (isLeaf) {
isExpanded = hasBeenExpanded = false;
} else {
isExpanded = treeState.getExpandedState(path);
hasBeenExpanded = tree.hasBeenExpanded(path);
}
bounds = getPathBounds(tree, path);
// See if the vertical line to the parent has been drawn.
parentPath = path.getParentPath();
if (parentPath != null) {
if (drawingCache.get(parentPath) == null) {
paintVerticalPartOfLeg(g, paintBounds, insets, parentPath);
drawingCache.put(parentPath, Boolean.TRUE);
}
paintHorizontalPartOfLeg(g, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
} else if (rootVisible && row == 0) {
paintHorizontalPartOfLeg(g, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
}
if (shouldPaintExpandControl(path, row, isExpanded, hasBeenExpanded, isLeaf)) {
paintExpandControl(g, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
}
if ((bounds.y + bounds.height) >= endY) {
done = true;
}
} else {
done = true;
}
row++;
}
}
cellContext.dispose();
paintDropLine(g);
// Empty out the renderer pane, allowing renderers to be gc'ed.
rendererPane.removeAll();
paintContext = null;
}
private void configureRenderer(SeaGlassContext context) {
TreeCellRenderer renderer = tree.getCellRenderer();
if (renderer instanceof DefaultTreeCellRenderer) {
DefaultTreeCellRenderer r = (DefaultTreeCellRenderer) renderer;
SeaGlassStyle style = (SeaGlassStyle)context.getStyle();
context.setComponentState(ENABLED | SELECTED);
Color color = r.getTextSelectionColor();
if (color == null || (color instanceof UIResource)) {
r.setTextSelectionColor(style.getColor(context, ColorType.TEXT_FOREGROUND));
}
color = r.getBackgroundSelectionColor();
if (color == null || (color instanceof UIResource)) {
r.setBackgroundSelectionColor(style.getColor(context, ColorType.TEXT_BACKGROUND));
}
context.setComponentState(ENABLED);
color = r.getTextNonSelectionColor();
if (color == null || color instanceof UIResource) {
r.setTextNonSelectionColor(style.getColorForState(context, ColorType.TEXT_FOREGROUND));
}
color = r.getBackgroundNonSelectionColor();
if (color == null || color instanceof UIResource) {
r.setBackgroundNonSelectionColor(style.getColorForState(context, ColorType.TEXT_BACKGROUND));
}
}
}
/**
* @inheritDoc
*/
@Override
protected void paintHorizontalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets, Rectangle bounds, TreePath path, int row,
boolean isExpanded, boolean hasBeenExpanded, boolean isLeaf) {
if (drawHorizontalLines) {
super.paintHorizontalPartOfLeg(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
}
}
/**
* @inheritDoc
*/
@Override
protected void paintHorizontalLine(Graphics g, JComponent c, int y, int left, int right) {
paintContext.getStyle().getGraphicsUtils(paintContext)
.drawLine(paintContext, "Tree.horizontalLine", g, left, y, right, y, linesStyle);
}
/**
* @inheritDoc
*/
@Override
protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets, TreePath path) {
if (drawVerticalLines) {
super.paintVerticalPartOfLeg(g, clipBounds, insets, path);
}
}
/**
* @inheritDoc
*/
@Override
protected void paintVerticalLine(Graphics g, JComponent c, int x, int top, int bottom) {
paintContext.getStyle().getGraphicsUtils(paintContext)
.drawLine(paintContext, "Tree.verticalLine", g, x, top, x, bottom, linesStyle);
}
private void paintRow(TreeCellRenderer renderer, DefaultTreeCellRenderer dtcr, SeaGlassContext treeContext, SeaGlassContext cellContext,
Graphics g, Rectangle clipBounds, Insets insets, Rectangle bounds, Rectangle rowBounds, TreePath path, int row, boolean isExpanded,
boolean hasBeenExpanded, boolean isLeaf) {
// Don't paint the renderer if editing this row.
boolean selected = tree.isRowSelected(row);
JTree.DropLocation dropLocation = tree.getDropLocation();
boolean isDrop = dropLocation != null && dropLocation.getChildIndex() == -1 && path == dropLocation.getPath();
int state = ENABLED;
if (selected || isDrop) {
state |= SELECTED;
}
if (tree.isFocusOwner() && row == getLeadSelectionRow()) {
state |= FOCUSED;
}
cellContext.setComponentState(state);
if (dtcr != null && (dtcr.getBorderSelectionColor() instanceof UIResource)) {
dtcr.setBorderSelectionColor(style.getColor(cellContext, ColorType.FOCUS));
}
SeaGlassLookAndFeel.updateSubregion(cellContext, g, rowBounds);
cellContext.getPainter().paintTreeCellBackground(cellContext, g, rowBounds.x, rowBounds.y, rowBounds.width, rowBounds.height);
cellContext.getPainter().paintTreeCellBorder(cellContext, g, rowBounds.x, rowBounds.y, rowBounds.width, rowBounds.height);
if (editingComponent != null && editingRow == row) {
return;
}
int leadIndex;
if (tree.hasFocus()) {
leadIndex = getLeadSelectionRow();
} else {
leadIndex = -1;
}
Component component = renderer.getTreeCellRendererComponent(tree, path.getLastPathComponent(), selected, isExpanded, isLeaf, row,
(leadIndex == row));
rendererPane.paintComponent(g, component, tree, bounds.x, bounds.y, bounds.width, bounds.height, true);
}
private int findCenteredX(int x, int iconWidth) {
return tree.getComponentOrientation().isLeftToRight() ? x - (int) Math.ceil(iconWidth / 2.0) : x
- (int) Math.floor(iconWidth / 2.0);
}
/**
* @inheritDoc
*/
@Override
protected void paintExpandControl(Graphics g, Rectangle clipBounds, Insets insets, Rectangle bounds, TreePath path, int row,
boolean isExpanded, boolean hasBeenExpanded, boolean isLeaf) {
// modify the paintContext's state to match the state for the row
// this is a hack in that it requires knowledge of the subsequent
// method calls. The point is, the context used in drawCentered
// should reflect the state of the row, not of the tree.
boolean isSelected = tree.getSelectionModel().isPathSelected(path);
int state = paintContext.getComponentState();
if (isSelected) {
paintContext.setComponentState(state | SynthConstants.SELECTED);
}
super.paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf);
paintContext.setComponentState(state);
}
/**
* @inheritDoc
*/
@Override
protected void drawCentered(Component c, Graphics graphics, Icon icon, int x, int y) {
int w = SynthIcon.getIconWidth(icon, paintContext);
int h = SynthIcon.getIconHeight(icon, paintContext);
SynthIcon.paintIcon(icon, paintContext, graphics, findCenteredX(x, w), y - h / 2, w, h);
}
/**
* @inheritDoc
*/
@Override
public void propertyChange(PropertyChangeEvent event) {
if (SeaGlassLookAndFeel.shouldUpdateStyle(event)) {
updateStyle((JTree) event.getSource());
}
if ("dropLocation" == event.getPropertyName()) {
JTree.DropLocation oldValue = (JTree.DropLocation) event.getOldValue();
repaintDropLocation(oldValue);
repaintDropLocation(tree.getDropLocation());
}
}
/**
* @inheritDoc
*/
@Override
protected void paintDropLine(Graphics g) {
JTree.DropLocation loc = tree.getDropLocation();
if (!isDropLine(loc)) {
return;
}
Color c = (Color) style.get(paintContext, "Tree.dropLineColor");
if (c != null) {
g.setColor(c);
Rectangle rect = getDropLineRect(loc);
g.fillRect(rect.x, rect.y, rect.width, rect.height);
}
}
private void repaintDropLocation(JTree.DropLocation loc) {
if (loc == null) {
return;
}
Rectangle r;
if (isDropLine(loc)) {
r = getDropLineRect(loc);
} else {
r = tree.getPathBounds(loc.getPath());
if (r != null) {
r.x = 0;
r.width = tree.getWidth();
}
}
if (r != null) {
tree.repaint(r);
}
}
/**
* @inheritDoc
*/
@Override
protected int getRowX(int row, int depth) {
return super.getRowX(row, depth) + padding;
}
private class SynthTreeCellRenderer extends DefaultTreeCellRenderer implements UIResource {
SynthTreeCellRenderer() {
}
@Override
public String getName() {
return "Tree.cellRenderer";
}
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
if (!useTreeColors && (sel || hasFocus)) {
SeaGlassLookAndFeel.setSelectedUI((SynthLabelUI) SeaGlassLookAndFeel.getUIOfType(getUI(), SynthLabelUI.class), sel, hasFocus,
tree.isEnabled(), false);
} else {
SeaGlassLookAndFeel.resetSelectedUI();
}
return super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
}
@Override
public void paint(Graphics g) {
paintComponent(g);
if (hasFocus) {
SeaGlassContext context = getContext(tree, Region.TREE_CELL);
if (context.getStyle() == null) {
assert false : "SynthTreeCellRenderer is being used " + "outside of UI that created it";
return;
}
int imageOffset = 0;
Icon currentI = getIcon();
if (currentI != null && getText() != null) {
imageOffset = currentI.getIconWidth() + Math.max(0, getIconTextGap() - 1);
}
if (selected) {
context.setComponentState(ENABLED | SELECTED);
} else {
context.setComponentState(ENABLED);
}
if (getComponentOrientation().isLeftToRight()) {
context.getPainter().paintTreeCellFocus(context, g, imageOffset, 0, getWidth() - imageOffset, getHeight());
} else {
context.getPainter().paintTreeCellFocus(context, g, 0, 0, getWidth() - imageOffset, getHeight());
}
context.dispose();
}
SeaGlassLookAndFeel.resetSelectedUI();
}
}
private static class SynthTreeCellEditor extends DefaultTreeCellEditor {
public SynthTreeCellEditor(JTree tree, DefaultTreeCellRenderer renderer) {
super(tree, renderer);
setBorderSelectionColor(null);
}
@Override
protected TreeCellEditor createTreeCellEditor() {
JTextField tf = new JTextField() {
@Override
public String getName() {
return "Tree.cellEditor";
}
};
DefaultCellEditor editor = new DefaultCellEditor(tf);
// One click to edit.
editor.setClickCountToStart(1);
return editor;
}
}
//
// BasicTreeUI directly uses expandIcon outside of the Synth methods.
// To get the correct context we return an instance of this that fetches
// the SynthContext as needed.
//
private class ExpandedIconWrapper extends SeaGlassIcon {
public ExpandedIconWrapper() {
super("Tree", "expandedIconPainter", 7, 7);
}
public void paintIcon(SynthContext context, Graphics g, int x, int y, int w, int h) {
if (context == null) {
context = getContext(tree);
SynthIcon.paintIcon(expandedIcon, context, g, x, y, w, h);
((SeaGlassContext)context).dispose();
} else {
SynthIcon.paintIcon(expandedIcon, context, g, x, y, w, h);
}
}
public int getIconWidth(SynthContext context) {
int width;
if (context == null) {
context = getContext(tree);
width = SynthIcon.getIconWidth(expandedIcon, context);
((SeaGlassContext)context).dispose();
} else {
width = SynthIcon.getIconWidth(expandedIcon, context);
}
return width;
}
public int getIconHeight(SynthContext context) {
int height;
if (context == null) {
context = getContext(tree);
height = SynthIcon.getIconHeight(expandedIcon, context);
((SeaGlassContext)context).dispose();
} else {
height = SynthIcon.getIconHeight(expandedIcon, context);
}
return height;
}
}
}