/*
* $Id$
*
* Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
package org.jdesktop.swingx.renderer;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.Calendar;
import java.util.Date;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.AbstractListModel;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.DefaultListCellRenderer;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.ListModel;
import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.Border;
import javax.swing.filechooser.FileSystemView;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.table.TableCellRenderer;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
import org.jdesktop.swingx.InteractiveTestCase;
import org.jdesktop.swingx.JXButton;
import org.jdesktop.swingx.JXFrame;
import org.jdesktop.swingx.JXList;
import org.jdesktop.swingx.JXPanel;
import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.JXTree;
import org.jdesktop.swingx.JXTreeTable;
import org.jdesktop.swingx.border.DropShadowBorder;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.HighlightPredicate;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.decorator.HighlighterFactory;
import org.jdesktop.swingx.decorator.PainterHighlighter;
import org.jdesktop.swingx.decorator.PatternPredicate;
import org.jdesktop.swingx.painter.BusyPainter;
import org.jdesktop.swingx.painter.ImagePainter;
import org.jdesktop.swingx.painter.MattePainter;
import org.jdesktop.swingx.rollover.RolloverProducer;
import org.jdesktop.swingx.rollover.RolloverRenderer;
import org.jdesktop.swingx.test.ComponentTreeTableModel;
import org.jdesktop.swingx.test.XTestUtils;
import org.jdesktop.swingx.treetable.FileSystemModel;
import org.jdesktop.swingx.treetable.TreeTableModel;
import org.jdesktop.swingx.util.PaintUtils;
import org.jdesktop.test.AncientSwingTeam;
import com.sun.java.swing.plaf.motif.MotifLookAndFeel;
/**
* Test around known issues of SwingX renderers. <p>
*
* Ideally, there would be at least one failing test method per open
* Issue in the issue tracker. Plus additional failing test methods for
* not fully specified or not yet decided upon features/behaviour.
*
* @author Jeanette Winzenburg
*/
public class RendererIssues extends InteractiveTestCase {
private static final Logger LOG = Logger.getLogger(RendererIssues.class
.getName());
public static void main(String[] args) {
RendererIssues test = new RendererIssues();
setLAF("Nimb");
try {
// test.runInteractiveTests();
test.runInteractiveTests("interactive.*Alpha.*");
// test.runInteractiveTests(".*XLabel.*");
// test.runInteractiveTests(".*Color.*");
// test.runInteractiveTests("interactive.*ColumnControl.*");
// test.runInteractiveTests("interactive.*ToolTip.*");
// test.runInteractiveTests("interactive.*TreeRenderer.*");
// test.runInteractiveTests("interactive.*Opacity.*");
// test.runInteractive("RendererCheckBox");
// test.runInteractiveTests("interactive.*Hyperlink.*");
} catch (Exception e) {
System.err.println("exception when executing interactive tests:");
e.printStackTrace();
}
}
/**
* Issue ??-swingx: Boolean renderer background is slightly darker if
* background color is part-transparent.
*
* Not only checkbox as renderer - there seem to be subtle differences
* when painting buttons, labels, ... plus it's laf dependent.
*
*/
public void interactiveAlphaBackground() {
Color color = PaintUtils.setAlpha(Color.ORANGE, 60);
JCheckBox check = new JCheckBox("what's my color? - as is");
// check.setOpaque(true);
// check.setContentAreaFilled(true);
check.setBackground(color);
JLabel label = new JLabel("label: and mine? (forced to opaque)");
label.setOpaque(true);
label.setBackground(color );
JButton button = new JButton("the new kid on the block - as-is");
button.setBackground(color);
JRadioButton radio = new JRadioButton("radio, raadio .. as-is");
radio.setBackground(color);
JTextField field = new JTextField(40);
field.setBackground(color);
JComponent box = Box.createVerticalBox();
box.setOpaque(true);
box.setBackground(Color.WHITE);
box.add(check);
box.add(radio);
box.add(label);
box.add(button);
box.add(field);
JXFrame frame = wrapInFrame(box, "alpha in plain ..", true);
show(frame, 400, 200);
}
/**
* Issue ??-swingx: Boolean renderer background is slightly darker if
* background color is part-transparent.
*
*/
public void interactiveCheckBoxAlpha() {
JXTable table = new JXTable(new org.jdesktop.test.AncientSwingTeam());
table.setBackground(PaintUtils.setAlpha(Color.YELLOW, 100));
table.setOpaque(false);
table.addHighlighter(new RowHighlighter(new HighlightPredicate() {
public boolean isHighlighted(Component renderer, ComponentAdapter adapter) {
return ((Boolean) adapter.getValue(4)).booleanValue();
}
}));
showWithScrollingInFrame(table, "boolean renderer and alpha background");
}
static class RowHighlighter extends ColorHighlighter {
Font BOLD_FONT;
RowHighlighter(HighlightPredicate predicate) {
super(predicate, PaintUtils.setAlpha(Color.ORANGE, 60), Color.RED);
setSelectedForeground(getForeground());
}
@Override
protected Component doHighlight(Component renderer, ComponentAdapter adapter) {
if (BOLD_FONT == null) {
BOLD_FONT = renderer.getFont().deriveFont(Font.BOLD);
}
renderer.setFont(BOLD_FONT);
return super.doHighlight(renderer, adapter);
}
}
/**
* example to configure treeTable hierarchical column with
* custom icon and content mapping. The nodes are actually of type File.
*
* Problem:
* painting on resizing tree column sluggish, especially if the PatternHighlighter
* is on. Reason seems to be the FileSystemView - prepare time increases with
* number of accesses.
*/
public void interactiveTreeTableCustomIconsPerformance() {
// modify the file model to return the file itself for the hierarchical column
TreeTableModel model = new FileSystemModel() {
@Override
public Object getValueAt(Object node, int column) {
if (column == 0) {
return node;
}
return super.getValueAt(node, column);
}
};
final JXTreeTable table = new JXTreeTable(model);
table.setAutoResizeMode(JXTable.AUTO_RESIZE_OFF);
StringValue sv = new StringValue() {
public String getString(Object value) {
if (value instanceof File) {
return FileSystemView.getFileSystemView().getSystemDisplayName((File) value)
+ " Type: "
+ FileSystemView.getFileSystemView().getSystemTypeDescription((File) value)
;
}
return StringValues.TO_STRING.getString(value);
}
};
IconValue iv = new IconValue() {
public Icon getIcon(Object value) {
if (value instanceof File) {
return FileSystemView.getFileSystemView().getSystemIcon((File) value);
}
return null;
}};
final DefaultTreeRenderer treeRenderer = new DefaultTreeRenderer(iv, sv);
table.setTreeCellRenderer(treeRenderer);
// string based. Note: this example is locale dependent
String folderDescription = ".*ordner.*";
PatternPredicate predicate = new PatternPredicate(folderDescription, 0, -1);
final Highlighter hl = new ColorHighlighter(predicate, null, Color.RED);
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, -1);
final Date lastYear = calendar.getTime();
// install value based highlighter
HighlightPredicate valueBased = new HighlightPredicate() {
public boolean isHighlighted(Component renderer,
ComponentAdapter adapter) {
if (!(adapter.getValue() instanceof File)) return false;
File file = (File) adapter.getValue();
Date date = new Date(file.lastModified());
return date.after(lastYear);
}
};
final ColorHighlighter back = new ColorHighlighter(valueBased, Color.YELLOW, null);
JXFrame frame =showWithScrollingInFrame(table, "TreeTable: performance bottleneck is FileSystemView");
Action toggleBack = new AbstractAction("toggleBackHighlighter") {
boolean hasBack;
public void actionPerformed(ActionEvent e) {
if (hasBack) {
table.removeHighlighter(back);
} else {
table.addHighlighter(back);
}
hasBack = !hasBack;
}
};
addAction(frame, toggleBack);
Action togglePattern = new AbstractAction("togglePatternHighlighter") {
boolean hasBack;
public void actionPerformed(ActionEvent e) {
if (hasBack) {
table.removeHighlighter(hl);
} else {
table.addHighlighter(hl);
}
hasBack = !hasBack;
}
};
addAction(frame, togglePattern);
// accessing the FileSystemView is slooooww
// increasingly costly over time, shows particularly
// when the patternHighlighter is on
// (probably because that increases the number
// of queries to the systemView
final JLabel timeL = new JLabel("stop-watch");
Action timer = new AbstractAction("start") {
public void actionPerformed(ActionEvent e) {
timeL.setText("started");
long time = System.currentTimeMillis();
for (int i = 0; i < 2000; i++) {
table.prepareRenderer(table.getCellRenderer(0, 0), 0, 0);
}
timeL.setText("stopped: " + (System.currentTimeMillis() - time));
}
};
addAction(frame, timer);
addStatusComponent(frame, timeL);
addStatusMessage(frame, "node is File - string/value based highlighters same");
}
/**
* Playing with rollover for visual clue if cell editable.
* The old problem: cursor shoudn't be altered by the rollover
* controller but by the rollover renderer.
*/
public void interactiveRolloverEffects() {
JXTable table = new JXTable(new AncientSwingTeam());
table.setDefaultRenderer(Boolean.class, new DefaultTableRenderer(new RolloverCheckBox()));
showWithScrollingInFrame(table, "checkbox rollover effect");
}
public static class RolloverCheckBox extends CheckBoxProvider
implements RolloverRenderer {
boolean wasEditable;
@Override
protected void configureState(CellContext context) {
super.configureState(context);
if (context.getComponent() != null) {
Point p = (Point) context.getComponent()
.getClientProperty(RolloverProducer.ROLLOVER_KEY);
if (/*hasFocus || */(p != null && (p.x >= 0) &&
(p.x == context.getColumn()) && (p.y == context.getRow()))) {
rendererComponent.getModel().setRollover(true);
} else {
rendererComponent.getModel().setRollover(false);
}
}
}
@Override
protected void format(CellContext context) {
// TODO Auto-generated method stub
super.format(context);
wasEditable = context.isEditable();
}
public void doClick() {
}
public boolean isEnabled() {
return wasEditable;
}
}
public void interactiveToolTipList() {
final JXTree table = new JXTree(new ComponentTreeTableModel(new JXFrame()));
table.expandAll();
// quick model for long values
ListModel model = new AbstractListModel() {
public Object getElementAt(int index) {
return table.getPathForRow(index).getLastPathComponent();
}
public int getSize() {
return table.getRowCount();
}
};
JXList list = new JXList(model) {
@Override
public String getToolTipText(MouseEvent event) {
int row = locationToIndex(event.getPoint());
Rectangle r = getCellBounds(row, row);
if (r == null)
return super.getToolTipText(event);
ListCellRenderer renderer = getCellRenderer();
Component comp = renderer.getListCellRendererComponent(this, getElementAt(row), row, isSelectedIndex(row), false);
if (comp.getPreferredSize().width <= getVisibleRect().width) return null;
renderer = ((DelegatingRenderer) renderer).getDelegateRenderer();
if (renderer instanceof StringValue) {
return ((StringValue) renderer).getString(getElementAt(row));
}
return null;
}
@Override
public Point getToolTipLocation(MouseEvent event) {
int row = locationToIndex(event.getPoint());
Rectangle r = getCellBounds(row, row);
if (r != null) {
if (!getComponentOrientation().isLeftToRight()) {
r.translate(r.width, 0);
}
return r.getLocation();
}
return super.getToolTipLocation(event);
}
};
JXFrame frame = wrapWithScrollingInFrame(list, "list tooltip");
addComponentOrientationToggle(frame);
show(frame, 300, 300);
}
public void interactiveToolTipTable() {
JXTreeTable treeTable = new JXTreeTable(new ComponentTreeTableModel(new JXFrame()));
treeTable.expandAll();
JXTable table = new JXTable(treeTable.getModel()) {
@Override
public String getToolTipText(MouseEvent event) {
int column = columnAtPoint(event.getPoint());
int row = rowAtPoint(event.getPoint());
TableCellRenderer renderer = getCellRenderer(row, column);
Component comp = prepareRenderer(renderer, row, column);
if (comp.getPreferredSize().width <= getColumn(column).getWidth()) return null;
if (renderer instanceof StringValue) {
return ((StringValue) renderer).getString(getValueAt(row, column));
}
return null;
}
@Override
public Point getToolTipLocation(MouseEvent event) {
int column = columnAtPoint(event.getPoint());
int row = rowAtPoint(event.getPoint());
Rectangle cellRect = getCellRect(row, column, false);
if (!getComponentOrientation().isLeftToRight()) {
cellRect.translate(cellRect.width, 0);
}
// PENDING JW: otherwise we get a small (borders only) tooltip for null
// core issue? Yeh, the logic in tooltipManager is crooked.
// but this here is ehem ... rather arbitrary, not working if value
// not null without tooltip.
return getValueAt(row, column) == null ? null : cellRect.getLocation();
// working - might be costly?
// return getToolTipText(event) == null ? null : cellRect.getLocation();
// return null;
}
};
table.setColumnControlVisible(true);
JXFrame frame = wrapWithScrollingInFrame(table, "tooltip");
addComponentOrientationToggle(frame);
show(frame);
}
public void interactiveToolTipTree() {
ComponentTreeTableModel model = new ComponentTreeTableModel(new JXFrame());
JXTree tree = new JXTree(model) {
@Override
public String getToolTipText(MouseEvent event) {
int row = getRowForLocation(event.getX(), event.getY());
if (row < 0) return null;
TreeCellRenderer renderer = getCellRenderer();
TreePath path = getPathForRow(row);
Object lastPath = path.getLastPathComponent();
Component comp = renderer.getTreeCellRendererComponent(this,
lastPath, isRowSelected(row), isExpanded(row),
getModel().isLeaf(lastPath), row, false);
int width = getVisibleRect().width;
if (comp.getPreferredSize().width <= width ) return null;
if (renderer instanceof JXTree.DelegatingRenderer) {
renderer = ((JXTree.DelegatingRenderer) renderer).getDelegateRenderer();
if (renderer instanceof StringValue) {
return ((StringValue) renderer).getString(lastPath);
}
}
return null;
}
@SuppressWarnings("unused")
private int getVisibleWidth() {
int width = getVisibleRect().width;
int indent = (((BasicTreeUI)getUI()).getLeftChildIndent() + ((BasicTreeUI)getUI()).getRightChildIndent());
return width;
}
@Override
public Point getToolTipLocation(MouseEvent event) {
return null;
}
};
tree.expandAll();
tree.setCellRenderer(new DefaultTreeRenderer());
// I'm registered to do tool tips so we can draw tips for the renderers
ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
toolTipManager.registerComponent(tree);
JXFrame frame = showWithScrollingInFrame(tree, "tooltip");
addComponentOrientationToggle(frame);
show(frame, 400, 400);
}
/**
* PENDING JW: really fancify or remove ;-)
*/
public void interactiveTreeFancyButton() {
JXTree tree = new JXTree();
tree.setRowHeight(30);
MattePainter painter = new MattePainter(Color.YELLOW);
Highlighter hl = new PainterHighlighter(HighlightPredicate.ROLLOVER_ROW, painter);
tree.addHighlighter(hl);
ComponentProvider<?> provider = new NormalButtonProvider(StringValues.TO_STRING, JLabel.LEADING);
tree.setCellRenderer(new DefaultTreeRenderer(provider));
tree.setRolloverEnabled(true);
showWithScrollingInFrame(tree, "Fancy..");
}
public void interactiveFancyButton() {
JXButton button = new JXButton("Dummy .... but lonnnnnnngg");
button.setBorder(BorderFactory.createCompoundBorder(new DropShadowBorder(), button.getBorder()));
JXPanel panel = new JXPanel();
panel.add(button);
showInFrame(panel, "Fancy..");
}
public static class NormalButtonProvider extends CheckBoxProvider
implements RolloverRenderer {
private Border border;
/**
* @param toString
* @param leading
*/
public NormalButtonProvider(StringValue toString, int leading) {
super(toString, leading);
setBorderPainted(true);
}
@Override
protected void configureState(CellContext context) {
super.configureState(context);
rendererComponent.setBorder(border);
Point p = (Point) context.getComponent().getClientProperty(
RolloverProducer.ROLLOVER_KEY);
if (/* hasFocus || */(p != null && (p.x >= 0)
&& (p.x == context.getColumn()) && (p.y == context.getRow()))) {
rendererComponent.getModel().setRollover(true);
} else {
rendererComponent.getModel().setRollover(false);
}
}
@Override
protected AbstractButton createRendererComponent() {
JXButton button = new JXButton();
border = BorderFactory.createCompoundBorder(
new DropShadowBorder(),
button.getBorder());
return button;
}
public void doClick() {
// TODO Auto-generated method stub
}
public boolean isEnabled() {
// TODO Auto-generated method stub
return true;
}
}
//--------------- unit tests
/**
* Issue #794-swingx: tooltip must be reset.
*
* Here: TreeTableCellRenderer (the tree used for rendering the hierarchical
* column)
*
*/
public void testToolTipResetTreeTableTreeRenderer() {
JXTreeTable treeTable = new JXTreeTable(new ComponentTreeTableModel(new JXPanel()));
JComponent label = (JComponent) treeTable.prepareRenderer(treeTable.getCellRenderer(0, 0), 0, 0);
String tip = "some tip";
label.setToolTipText(tip);
assertEquals("sanity: tooltip must be set", tip, label.getToolTipText());
// prepare again
label = (JComponent) treeTable.prepareRenderer(treeTable.getCellRenderer(0, 0), 0, 0);
assertEquals("tooltip must be reset in each prepare", null, label.getToolTipText());
}
/**
* Issue #774-swingx: support per node-type icons.
*
* postponed to 0.9.x - will break all interface implementors.
*
*/
public void testNodeTypeIcons() {
TreeCellContext context = new TreeCellContext();
context.installContext(null, "dummy", -1, -1, false, false, false, true);
final Icon custom = XTestUtils.loadDefaultIcon();
IconValue iv = new IconValue() {
public Icon getIcon(Object value) {
// TODO Auto-generated method stub
return custom;
}
};
WrappingProvider provider = new WrappingProvider(iv);
WrappingIconPanel comp = provider.getRendererComponent(context);
assertEquals(custom, comp.getIcon());
fail("feature request: per node type iconValue");
}
/**
* base interaction with list: focused, not-selected uses UI border.
* Moved from ListRendererTest: failes on the new server (what's the default LF there?)
* TODO: fix and reinstate the test
* @throws UnsupportedLookAndFeelException
*/
public void testListFocusBorder() throws UnsupportedLookAndFeelException {
LookAndFeel lf = UIManager.getLookAndFeel();
try {
UIManager.setLookAndFeel(new MotifLookAndFeel());
JList list = new JList(new Object[] {1, 2, 3});
ListCellRenderer coreListRenderer = new DefaultListCellRenderer();
ListCellRenderer xListRenderer = new DefaultListRenderer();
// access ui colors
Border focusBorder = UIManager.getBorder("List.focusCellHighlightBorder");
// sanity
assertNotNull(focusBorder);
// JW: this looks suspicious ...
// RAH: line below makes hudson fail the test tho it runs fine locally ...
assertNotSame(focusBorder, UIManager.getBorder("Table.focusCellHighlightBorder"));
// need to prepare directly - focus is true only if list is focusowner
JComponent coreComponent = (JComponent) coreListRenderer.getListCellRendererComponent(list,
null, 0, false, true);
// sanity: known standard behaviour
assertEquals(focusBorder, coreComponent.getBorder());
// prepare extended
JComponent xComponent = (JComponent) xListRenderer.getListCellRendererComponent(list,
null, 0, false, true);
// assert behaviour same as standard
assertEquals(coreComponent.getBorder(), xComponent.getBorder());
} finally {
UIManager.setLookAndFeel(lf);
}
}
/**
* test if renderer properties are updated on LF change. <p>
* Note: this can be done examplary only. Here: we use the
* font of a rendererComponent returned by a HyperlinkProvider for
* comparison. There's nothing to test if the font are equal
* in System and crossplattform LF. <p>
*
* There are spurious problems when toggling UI (since when?)
* with LinkRenderer
* "no ComponentUI class for: org.jdesktop.swingx.LinkRenderer$1"
* that's the inner class JXHyperlink which overrides updateUI.
*
* PENDING: this was moved from tableUnitTest - had been passing with
* LinkRenderer but with HyperlinkProvider
* now is failing (on server with defaultToSystem == false, locally win os
* with true), probably due to slightly different setup now
* in renderer defaultVisuals? It resets the font to table's which
* LinkRenderer didn't. Think whether to change the provider go back
* to hyperlink font?
*/
public void testUpdateRendererOnLFChange() {
boolean defaultToSystemLF = true;
setSystemLF(defaultToSystemLF);
TableCellRenderer comparison = new DefaultTableRenderer(new HyperlinkProvider());
TableCellRenderer linkRenderer = new DefaultTableRenderer(new HyperlinkProvider());
JXTable table = new JXTable(2, 3);
Component comparisonComponent = comparison.getTableCellRendererComponent(table, null, false, false, 0, 0);
Font comparisonFont = comparisonComponent.getFont();
table.getColumnModel().getColumn(0).setCellRenderer(linkRenderer);
setSystemLF(!defaultToSystemLF);
SwingUtilities.updateComponentTreeUI(comparisonComponent);
if (comparisonFont.equals(comparisonComponent.getFont())) {
LOG.info("cannot run test - equal font " + comparisonFont);
return;
}
SwingUtilities.updateComponentTreeUI(table);
Component rendererComp = table.prepareRenderer(table.getCellRenderer(0, 0), 0, 0);
assertEquals("renderer font must be updated",
comparisonComponent.getFont(), rendererComp.getFont());
}
/**
* RendererLabel NPE with null Graphics. While expected,
* the exact location is not.
* NPE in JComponent.paintComponent finally block
*
*/
public void testLabelNPEPaintComponentOpaque() {
JRendererLabel label = new JRendererLabel();
label.setOpaque(true);
label.paintComponent(null);
}
/**
* RendererLabel NPE with null Graphics. While expected,
* the exact location is not.
* NPE in JComponent.paintComponent finally block
*
*/
public void testLabelNPEPaintComponent() {
JRendererLabel label = new JRendererLabel();
label.setOpaque(false);
label.paintComponent(null);
}
}