Folks,
I am trying to create a gradient JTree control. The following code mostly works except that the background for the tree cell is not transparent. I would appreciate it if someone call tell me what is it that I am not doing right.
Thank you in advance for your help.
Regards,
Peter
package TestPackage;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import java.awt.*;
public class Test {
public Test() {
JFrame frame = new JFrame();
JPanel framePanel = new JPanel();
framePanel.setLayout(new BorderLayout());
frame.setContentPane(framePanel);
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode("Item");
DefaultMutableTreeNode childNode = new DefaultMutableTreeNode("Child");
rootNode.add(childNode);
GradientTree tree = new GradientTree(rootNode);
// JTree tree = new JTree(rootNode);
// tree.setBackground(Color.blue);
tree.setCellRenderer(new MyRenderer());
JScrollPane scroll = new JScrollPane(tree);
scroll.setOpaque(false);
framePanel.add(scroll, BorderLayout.CENTER);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Test();
}
});
}
#SuppressWarnings("serial")
public static class GradientTree extends JTree {
public GradientTree(DefaultMutableTreeNode node) {
super(node);
}
#Override
protected void paintComponent(Graphics g) {
int h = getHeight();
int w = getWidth();
GradientPaint gradientPaint = new GradientPaint(0, 0, Color.LIGHT_GRAY, 0, h, Color.WHITE);
Graphics2D g2D = (Graphics2D) g;
g2D.setPaint(gradientPaint);
g2D.fillRect(0, 0, w, h);
this.setOpaque(false);
super.paintComponent(g);
this.setOpaque(true);
}
}
#SuppressWarnings({"serial" })
private class MyRenderer extends DefaultTreeCellRenderer {
public MyRenderer() {
this.setOpaque(false);
this.setForeground(Color.RED);
}
public Component getTreeCellRendererComponent(
JTree tree,
Object value,
boolean sel,
boolean expanded,
boolean leaf,
int row,
boolean hasFocus) {
super.getTreeCellRendererComponent(
tree, value, sel,
expanded, leaf, row,
hasFocus);
return this;
}
}
}
This is a real pain. The DefaultTreeCellRenderer will ignore the opaque value and fill it's contents anyway. However, there is a flag you can try. I've done it in the past, but don't have time to test it...
Try UIManager.put("Tree.rendererFillBackground", false). Try and do this before anything is renderer, but after any look and feel settings have been applied.
UPDATED
It is very important to set this property BEFORE you create any trees
Without | With...
public class TestTreeRenderer {
public static void main(String[] args) {
new TestTreeRenderer();
}
public TestTreeRenderer() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TreePane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TreePane extends JPanel {
private JTree tree;
public TreePane() {
// THIS IS VERY IMPORTANT
// You must set this BEFORE creating ANY trees!!
UIManager.put("Tree.rendererFillBackground", false);
setLayout(new BorderLayout());
tree = new JTree();
tree.setBackground(Color.BLUE);
System.out.println("Loading files...");
File root = new File("/etc");
DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(root.getName());
for (File file : root.listFiles()) {
rootNode.add(new DefaultMutableTreeNode(file.getName()));
}
System.out.println("Loading model");
DefaultTreeModel model = new DefaultTreeModel(rootNode);
tree.setModel(model);
add(new JScrollPane(tree));
}
}
}
Answer
(expanding on #Mad's answer, the longish analysis of the underlying problems is at the end):
If you want the global property to be effective in a defaultTreeCellRenderer set manually to the tree, that renderer has to call updateUI again , f.i.
UIManager.put("Tree.rendererFillBackground", false);
...
TreeCellRenderer r = new DefaultTreeCellRenderer() {
{
updateUI();
}
};
tree.setCellRenderer(r);
If you do not want to change the global setting and have the transparent renderers only some tree instances - the options are
either implement a TreeCellRenderer from scratch and leaving out all the dirtiness (like overriding paint and doing some unexpected hard-coded tricksery ... doooh!)
tricks the renderer by temporarily setting the ui property in updateUI
Tricksing code:
TreeCellRenderer r = new DefaultTreeCellRenderer() {
{
updateUI();
}
#Override
public void updateUI() {
Object old = UIManager.get("Tree.rendererFillBackground");
try {
UIManager.put("Tree.rendererFillBackground", false);
super.updateUI();
} finally {
UIManager.put("Tree.rendererFillBackground", old);
}
}
};
Analysis
starting from my comment:
Weirdly, the mere act of setting CellRenderer (vs. letting the ui install its favourits) makes the flag ineffective
This puzzle is resolved:
DefaultTreeCellRenderer has the intention to set its fillBackground field from the setting in the UIManager - but fails doing so on instantiation. The reason is a - all too common error ;-) - in actually doing so in super's instantiation, due to calling a overridden method in super's constructor:
// this is implemented in DefaultTreeCellRenderer
// but called in JLabel constructor
public void updateUI() {
....
// we are in JLabel, that is fillBackground not yet known
fillBackground = DefaultLookup.getBoolean(this, ui, "Tree.rendererFillBackground", true);
...
}
then later in the instantiation process, the field value is hardcoded:
private boolean fillBackground = true;
The net result is (assuming that we force access to the field, f.i. via reflection), the following passes always, irrespective of the setting in the UIManager.
DefaultTreeCellRenderer renderer = new DefaultTreeRenderer();
assertTrue(renderer.fillBackground);
With that the unusual thingy is: why does the setting in the UIManager has an effect when letting the ui install its default? Here the reason is that the renderers updateUI is called twice: once on instantiation and once in the tree's updateUI:
public void updateUI() {
setUI((TreeUI)UIManager.getUI(this));
// JW: at this point the renderer has its fillbackground hard-coded to true
SwingUtilities.updateRendererOrEditorUI(getCellRenderer());
// JW: now it's updateUI has been called again, and correctly set to the
// UIManager's value
SwingUtilities.updateRendererOrEditorUI(getCellEditor());
}
BTW: this instantiation mess seems to be introduced in jdk7 ... most probably (didn't check, though) the default settings of the renderer colors not working as well.
How about extending the DefaultTreeCellRenderer like this:
public class MyRenderer extends DefaultTreeCellRenderer {
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean isSelected, boolean expanded, boolean leaf, int row,
boolean hasFocus) {
JComponent c = (JComponent) super.getTreeCellRendererComponent(tree, value, isSelected, expanded, leaf, row, hasFocus);
c.setOpaque(true);
return c;
}
}
Setting c.setOpaque(true); seems to solve it.
I really hesitate to advance this hypothesis in the presence of these Swing experts... but could it be that more recent JDKs have actually rectified this problem?
I have code like this in my app and it seems to work fine... the JTree's background shines through perfectly... NB Jython, but should be understandable:
def getTreeCellRendererComponent( self, tree, value, selected, expanded, leaf, row, has_focus ):
super_comp = self.super__getTreeCellRendererComponent( tree, value, selected, expanded, leaf, row, has_focus )
super_comp.opaque = not selected
...
Java version is 1.7.0_079
Related
I have a JFileChooser. I am trying to add a zoom feature to the files JList.
I would like to change the scale factor of the file name and of the file icon, for each element of the list.
How could we achieve this ?
Should I make a custom renderer like here [JList custom renderer example] (http://www.codejava.net/java-se/swing/jlist-custom-renderer-example)
or change the list Model ?
Well, I found out some ugly lazy hacks to do it.
It might not be just what you want, but it's a good starting point (and fairly simple):
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.plaf.basic.BasicListUI;
public class TJFileChooserDemo {
//Obtains the (first) JList which is found inside the component/container:
public static JList getFirstJList(final Component component) {
if (component instanceof JList)
return (JList) component;
if (component instanceof Container)
for (int i=0; i<((Container)component).getComponentCount(); ++i) {
final JList list = getFirstJList(((Container)component).getComponent(i));
if (list != null)
return list;
}
return null;
//As you can see, it's a bit lazy hack, which has to run for every JFileChooser once at start-up.
}
private static final double SCALE_STEP_SIZE = 0.125; //Smaller values of this makes zooming slower. Greater values makes zooming faster.
private static double scaleFactor = 1;
public static class TJListCellRenderer extends DefaultListCellRenderer {
public TJListCellRenderer() {
//Ensure every pixel is painted starting from the top-left corner of the label:
super.setVerticalAlignment(JLabel.TOP);
super.setHorizontalAlignment(JLabel.LEFT);
//We need to do this, because the scaling in paintComponent() is also relative to the top-left corner.
}
#Override
public void paintComponent(final Graphics g) {
//setRenderingHints here? Probably for ANTIALIAS...
((Graphics2D)g).scale(scaleFactor, scaleFactor); //Let's scale everything that is painted afterwards:
super.paintComponent(g); //Let's paint the (scaled) JLabel!
}
#Override
public Dimension getPreferredSize() {
final Dimension superPrefDim = super.getPreferredSize(); //Handles automatically insets, icon size, text font, etc.
final double w = superPrefDim.width * scaleFactor, //And we just scale the preferred size.
h = superPrefDim.height * scaleFactor; //And we just scale the preferred size.
return new Dimension((int)w + 5, (int)h + 5); //Add 5 extra pixels to spare.
}
#Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
// System.out.println(value.getClass()); //Something ugly...
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
}
public static class TJListUI extends BasicListUI {
#Override
public void updateLayoutState() {
super.updateLayoutState(); //Just make the following method public.
/*Note: this is not really needed here:
The method could remain protected, but in the case you want this
code to be a bit more reusable, then you shall make it public.*/
}
}
public static void main(final String[] args) {
final JFileChooser jfc = new JFileChooser();
jfc.setDialogType(JFileChooser.OPEN_DIALOG);
final TJListUI ui = new TJListUI();
final JList list = getFirstJList(jfc);
list.setUI(ui);
list.setCellRenderer(new TJListCellRenderer());
final JButton buttonZoomIn = new JButton("Zoom in"),
buttonZoomOut = new JButton("Zoom out"),
buttonResetZoom = new JButton("Reset zoom");
buttonZoomIn.addActionListener(e -> {
scaleFactor = scaleFactor + SCALE_STEP_SIZE;
ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
list.revalidate(); //Update the JScrollPane.
list.repaint(); //Repaint the list.
});
buttonZoomOut.addActionListener(e -> {
scaleFactor = Math.max(scaleFactor - SCALE_STEP_SIZE, SCALE_STEP_SIZE); //Do not allow underflow.
ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
list.revalidate(); //Update the JScrollPane.
list.repaint(); //Repaint the list.
});
buttonResetZoom.addActionListener(e -> {
scaleFactor = 1;
ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
list.revalidate(); //Update the JScrollPane.
list.repaint(); //Repaint the list.
});
final JPanel buttons = new JPanel(); //FlowLayout.
buttons.add(buttonZoomIn);
buttons.add(buttonZoomOut);
buttons.add(buttonResetZoom);
final JPanel panel = new JPanel(new BorderLayout());
panel.add(buttons, BorderLayout.PAGE_START);
panel.add(jfc, BorderLayout.CENTER);
final JFrame frame = new JFrame("JFileChooser's JList cell sizes demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
Alternatively you can check my answer here about individually resizable cells of a JList.
You can also probably add the JFileChooser's buttons for zooming in/out as an accessory. Read this simple example for how to do it.
Test this code, and I am waiting for comments...
In the end, I realized scaling the text wasn't needed.
To obtain the image files thumbnail, I used the code in making JFileChooser show image thumbnails - check BoffinbraiN answer.
Then for scaling :
1) add an ActionListener to the buttons of ThumbnailFileChooser.
public class ZoomListener implements ActionListener {
private boolean zoomIn = false;
private IconScaleManager iconScaleManager = null;
public ZoomListener(boolean zoom, IconScaleManager renderer) {
zoomIn = zoom;
iconScaleManager = renderer;
}
#Override
public void actionPerformed(ActionEvent e) {
iconScaleManager.scaleButton(zoomIn);
}
}
2) ActionListener::actionPerformed() calls a scale method of a ScaleManager.
#Override
public void actionPerformed(ActionEvent e) {
iconScaleManager.scaleButton(zoomIn);
}
3) The ScaleManager method changes and update the cells of the ThumbnailFileChooser's Jlist (the list is an attribute of the ScaleManager)
public class IconScaleManager {
static final int[] iconScales = new int[]{ 16, 32, 64, 128, 256, 512, 1024, 2048 };
private int scaleIndex = 4;
private JList fileList = null;
public IconScaleManager(JList list) {
fileList = list;
setFixedCellDimension();
}
public void scaleButton(boolean zoomIn) {
if (zoomIn && scaleIndex < iconScales.length - 1) {
scaleIndex++;
setFixedCellDimension();
} else if (!zoomIn && 0 < scaleIndex) {
scaleIndex--;
setFixedCellDimension();
}
}
private void setFixedCellDimension() {
fileList.setFixedCellWidth(iconScales[scaleIndex]);
fileList.setFixedCellHeight(iconScales[scaleIndex]);
}
}
Thank you #thanopi57 for your help. I didn't really use what you provided, but I appreciate your support.
Also, I will have to make sure that it works, because there might not be a JList for all JFileChooser
I'm using a look and feel which does row striping of tables by default. When I put in a JXTreeTable, I noticed that for some reason it didn't get the row striping automatically.
So I put in a workaround using Highlighter, but it looks like I get a repainting glitch:
It seems like JXTreeTable is specifically repainting only the boundaries of the text instead of the whole cell. I have been trying to catch this in the debugger to figure out why, but every time I switch between programs, the whole window repaints, so it's nearly impossible to catch this kind of thing.
JTable and JTree both behave sanely. This look and feel is one which paints the whole row of a JTree (like Quaqua and Synth), so maybe that has something to do with it too. Perhaps JXTreeTable has some kind of assumption that the look and feel won't paint the rows of the tree? If so, is there a way to work around that? It won't just be this look and feel which has the issue.
The code:
import org.jdesktop.swingx.JXTreeTable;
import org.jdesktop.swingx.decorator.AbstractHighlighter;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode;
import org.jdesktop.swingx.treetable.DefaultTreeTableModel;
import org.jdesktop.swingx.treetable.TreeTableModel;
import org.trypticon.haqua.HaquaLookAndFeel;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import java.awt.BorderLayout;
import java.awt.Component;
import java.util.Arrays;
public class TreeTableDemo2 implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new TreeTableDemo2());
}
#Override
public void run() {
try {
UIManager.setLookAndFeel(new HaquaLookAndFeel());
} catch (Exception e) {
throw new RuntimeException(e);
}
JFrame frame = new JFrame("Tree Table Demo");
frame.setLayout(new BorderLayout());
frame.add(createPanel(), BorderLayout.CENTER);
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
public JPanel createPanel() {
JPanel panel = new JPanel(new BorderLayout());
TreeTableModel treeTableModel = new DummyTreeTableModel();
JXTreeTable treeTable = new FixedTreeTable(treeTableModel);
JScrollPane treeTableScroll = new JScrollPane(treeTable);
panel.add(treeTableScroll, BorderLayout.CENTER);
return panel;
}
private static class FixedTreeTable extends JXTreeTable {
private static final Highlighter oddRowHighlighter = new AbstractHighlighter() {
#Override
protected Component doHighlight(Component component, ComponentAdapter componentAdapter) {
if (componentAdapter.row % 2 != 0 &&
!componentAdapter.isSelected()) {
component.setBackground(UIManager.getColor("Table.alternateRowColor"));
}
return component;
}
};
public FixedTreeTable(TreeTableModel treeModel) {
super(treeModel);
// This hack makes it paint correctly after releasing the mouse, which is not quite good enough.
// getSelectionModel().addListSelectionListener(new ListSelectionListener() {
// #Override
// public void valueChanged(ListSelectionEvent e) {
// Rectangle repaintRange = getCellRect(e.getFirstIndex(), 0, true);
// repaintRange.add(getCellRect(e.getLastIndex(), 0, true));
// repaint(repaintRange);
// }
// });
}
#Override
public void updateUI() {
removeHighlighter(oddRowHighlighter);
super.updateUI();
// JTable does this striping automatically but JXTable's default renderer
// seems to ignore it, so JXTreeTable inherits this broken behaviour.
if (UIManager.get("Table.alternateRowColor") != null) {
addHighlighter(oddRowHighlighter);
}
}
}
private static class DummyTreeTableNode extends DefaultMutableTreeTableNode {
private final Object[] values;
private DummyTreeTableNode(String name) {
super(name);
values = new Object[5];
values[0] = name;
}
private DummyTreeTableNode(Object... values) {
super(values[0]);
this.values = values;
}
#Override
public Object getValueAt(int column) {
return values[column];
}
}
private static class DummyTreeTableModel extends DefaultTreeTableModel {
private static DefaultMutableTreeTableNode rootNode = new DefaultMutableTreeTableNode();
static {
DefaultMutableTreeTableNode blue = new DefaultMutableTreeTableNode("Blue");
blue.add(new DummyTreeTableNode("Orionis C", 33000, 30000.0, 18.0, 5.90));
rootNode.add(blue);
DefaultMutableTreeTableNode bluish = new DefaultMutableTreeTableNode("Bluish");
bluish.add(new DummyTreeTableNode("Becrux", 30000, 16000.0, 16.0, 5.70));
bluish.add(new DummyTreeTableNode("Spica", 22000, 8300.0, 10.5, 5.10));
bluish.add(new DummyTreeTableNode("Achernar", 15000, 750.0, 5.40, 3.70));
bluish.add(new DummyTreeTableNode("Rigel", 12500, 130.0, 3.50, 2.70));
rootNode.add(bluish);
DefaultMutableTreeTableNode blueWhite = new DefaultMutableTreeTableNode("Blue-White");
blueWhite.add(new DummyTreeTableNode("Sirius A", 9500, 63.0, 2.60, 2.30));
blueWhite.add(new DummyTreeTableNode("Fomalhaut", 9000, 40.0, 2.20, 2.00));
blueWhite.add(new DummyTreeTableNode("Altair", 8700, 24.0, 1.90, 1.80));
rootNode.add(blueWhite);
DefaultMutableTreeTableNode white = new DefaultMutableTreeTableNode("White");
white.add(new DummyTreeTableNode("Polaris A", 7400, 9.0, 1.60, 1.50));
white.add(new DummyTreeTableNode("Eta Scorpii", 7100, 6.3, 1.50, 1.30));
white.add(new DummyTreeTableNode("Procyon A", 6400, 4.0, 1.35, 1.20));
rootNode.add(white);
DefaultMutableTreeTableNode yellowWhite = new DefaultMutableTreeTableNode("Yellow-White");
yellowWhite.add(new DummyTreeTableNode("Alpha Centauri A", 5900, 1.45, 1.08, 1.05));
yellowWhite.add(new DummyTreeTableNode("The Sun", 5800, 100.0, 1.00, 1.00));
yellowWhite.add(new DummyTreeTableNode("Mu Cassiopeiae", 5600, 0.70, 0.95, 0.91));
yellowWhite.add(new DummyTreeTableNode("Tau Ceti", 5300, 0.44, 0.85, 0.87));
rootNode.add(yellowWhite);
DefaultMutableTreeTableNode orange = new DefaultMutableTreeTableNode("Orange");
orange.add(new DummyTreeTableNode("Pollux", 5100, 0.36, 0.83, 0.83));
orange.add(new DummyTreeTableNode("Epsilon Eridani", 4830, 0.28, 0.78, 0.79));
orange.add(new DummyTreeTableNode("Alpha Centauri B", 4370, 0.18, 0.68, 0.74));
rootNode.add(orange);
DefaultMutableTreeTableNode red = new DefaultMutableTreeTableNode("Red");
red.add(new DummyTreeTableNode("Lalande 21185", 3400, 0.03, 0.33, 0.36));
red.add(new DummyTreeTableNode("Ross 128", 3200, 0.0005, 0.20, 0.21));
red.add(new DummyTreeTableNode("Wolf 359", 3000, 0.0002, 0.10, 0.12));
rootNode.add(red);
}
private static final Object[] columnNames = {
"Star", "Temperature (K)", "Luminosity", "Mass", "Radius"
};
public DummyTreeTableModel() {
super(rootNode, Arrays.asList(columnNames));
}
#Override
public Class<?> getColumnClass(int columnIndex) {
if (columnIndex == 0) {
return String.class;
} else {
return Double.class;
}
}
#Override
public boolean isCellEditable(Object node, int column) {
return false;
}
}
}
Further investigation round 1:
I finally managed to trap the condition in the debugger by watching a paint method in JXTreeTable. What I see is that it has something called ClippedTreeCellRenderer which has some fields which look like they correspond to the suspicious behaviour:
iconRect = {java.awt.Rectangle#2946}"java.awt.Rectangle[x=20,y=92,width=16,height=16]"
textRect = {java.awt.Rectangle#2947}"java.awt.Rectangle[x=20,y=-17,width=62,height=15]"
itemRect = {java.awt.Rectangle#2948}"java.awt.Rectangle[x=20,y=36,width=103,height=18]"
I haven't yet confirmed that it is definitely using this value to paint the rectangle, but textRect is exactly the size of the small window it does repaint. So now the question is, where on earth is JXTreeTable pulling these values from and why is it using them?
My instinct tells me that JXTreeTable's renderer is somehow using the tree cell renderer to render the cell directly instead of just telling the tree itself to paint. The logic for painting the row backgrounds and the expand/collapse icons is in the tree, not in the cells, so if it's doing that, it would make sense that it isn't consistently painting the tree.
Further investigation round 2:
I think I was on the wrong path entirely. It looks like the whole row is being painted, but tree.isPathSelected(path) returns false for the newly-selected blue rows and returns true for the just-deselected row.
I can confirm by breakpoints in DefaultTreeSelectionModel that the tree selection is only updated after you let go of the mouse, which is why it eventually returns to being rendered correctly.
I'll have to dig further into JXTreeTable to see how it's keeping those in sync.
I found the bug. It looks like some well-meaning code in JXTreeTable.java which is trying to reduce updates:
/**
* Class responsible for calling updateSelectedPathsFromSelectedRows
* when the selection of the list changse.
*/
class ListSelectionHandler implements ListSelectionListener {
#Override
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting()) {
updateSelectedPathsFromSelectedRows();
}
}
}
If you remove that if check, everything works correctly. I'll just have to make some local changes to SwingX I guess, since the project is essentially dead. :(
I`ve got a mysterious problem with my custom JTable and a custom TableRenderer.
In 95%-99,99% it works perfectly, but sometimes the renderer just stops doing his job, and leaves a portion of the table (which is inside a JScrollPane) blank.
The problem case looks like that:
In all other cases, and after a slight resize of the window, the Table look like that:
Now both columns has a TextAreaCellRenderer associated to, which works as follows:
public class TextAreaCellRenderer extends JTextArea implements TableCellRenderer {
private final Color evenColor = new Color(252, 248, 202);
public TextAreaCellRenderer() {
super();
setLineWrap(true);
setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
}
#Override
public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) {
if (isSelected) {
setForeground(table.getSelectionForeground());
setBackground(table.getSelectionBackground());
} else {
setForeground(table.getForeground());
setBackground(table.getBackground());
setBackground((row % 2 == 0) ? evenColor : getBackground());
}
setWrapStyleWord(true);
setFont(table.getFont());
setText((value == null) ? "" : value.toString());
return this;
}
}
I also have to override the doLayout method of the JTable to be able to calculate the hight of a cell depending on the content. The custom table looks like that:
public class MediaMetaDataTable extends JTable {
#Override
public void doLayout() {
TableColumn col = getColumnModel().getColumn(1);
for (int row = 0; row < getRowCount(); row++) {
Component c = prepareRenderer(col.getCellRenderer(), row, 1);
if (c instanceof JTextArea) {
JTextArea a = (JTextArea) c;
int h = getPreferredHeight(a) + getIntercellSpacing().height;
if (getRowHeight(row) != h) {
setRowHeight(row, h);
}
}
}
super.doLayout();
}
private int getPreferredHeight(final JTextComponent c) {
Insets insets = c.getInsets();
View view = c.getUI().getRootView(c).getView(0);
int preferredHeight = (int) view.getPreferredSpan(View.Y_AXIS);
return preferredHeight + insets.top + insets.bottom;
}
}
The table is instantiated once with the following parameters:
metaTable = new MediaMetaDataTable();
metaTable.setModel(new MediaMetaDataTableModel());
metaTable.setEnabled(false);
metaTable.setShowGrid(false);
metaTable.setTableHeader(null);
metaTable.getColumnModel().getColumn(0).setCellRenderer(new TextAreaCellRenderer());
metaTable.getColumnModel().getColumn(1).setCellRenderer(new TextAreaCellRenderer());
metaTable.setPreferredScrollableViewportSize(new Dimension(-1, -1));
metaTable.setShowHorizontalLines(false);
metaTable.setShowVerticalLines(false);
Each time the data to show changes i update table by replacing the underlying models data:
List<MediaMetaData> metaInformation = mediaSearchHit.getMetaInformation();
if (metaInformation != null) {
((MediaMetaDataTableModel) metaTable.getModel()).replaceMetaInfos(metaInformation);
}
On update the model itself fires a table data changed event:
public class MediaMetaDataTableModel extends AbstractTableModel {
private List<MediaMetaData> metaInfos = new LinkedList<MediaMetaData>();
public static final int COL_INDEX_NAME = 0;
public static final int COL_INDEX_VALUE = 1;
public void replaceMetaInfos(final List<MediaMetaData> metaInfos) {
this.metaInfos = null;
this.metaInfos = metaInfos;
fireTableDataChanged();
}
...
Now does anybody has a idea, what causes the described rendering problem?
Thanks for any advices.
I also have to override the doLayout method of the JTable to be able
to calculate the hight of a cell depending on the content.
To achieve this goal there's no need to override doLayout() method. I think the simplest way to do this is by adding the text area used to render the cell content into a JPanel with BorderLayout and set the row height based on the panel's preferred size. This way the layout manager will do the trick for you and all the cell's content will be visible:
#Override
public Component getTableCellRendererComponent(...) {
...
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.add(this);
table.setRowHeight(row, contentPane.getPreferredSize().height); // sets row's height
return contentPane;
}
As #mKorbel pointed out, there's no need to make the renderer extend from JTextArea: a single variable will work. Keeping this in mind take a look to this implementation based on your work:
class TextAreaRenderer implements TableCellRenderer {
private JTextArea renderer;
private final Color evenColor = new Color(252, 248, 202);
public TextAreaRenderer() {
renderer = new JTextArea();
renderer.setLineWrap(true);
renderer.setWrapStyleWord(true);
renderer.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
}
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
if (isSelected) {
renderer.setForeground(table.getSelectionForeground());
renderer.setBackground(table.getSelectionBackground());
} else {
renderer.setForeground(table.getForeground());
renderer.setBackground((row % 2 == 0) ? evenColor : table.getBackground());
}
renderer.setFont(table.getFont());
renderer.setText((value == null) ? "" : value.toString());
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.add(renderer);
table.setRowHeight(row, contentPane.getPreferredSize().height); // sets row's height
return contentPane;
}
}
Screenshot
If I had to guess I would say it could be a concurency problem. Are you doing everything in the GUI-Thread? If yes, it can't be a concurency problem. Otherwhise try to call everything with Thread.InvokeLater() in an inital debug step, if you don't encounter the error anymore after a long time of testing, you know the cause of the problem.
In a second step you would then check exactly where it is necessary to make the calls with invokelater() and where not (because you shouldn't do that all the time, because it leads to very poor performance.
As I said, just a wild guess... It can of youre just be another bug. Are you using Java7? There are millions of Bugs in Swing with java 7 code (just all the code that from oracle came).
Please have a look at the SSCCE. How can I make the non-selected tree nodes' background transparent. At the moment the background of non-selected nodes is white. My cell renderer, however, should paint it non-opaque if it is not selected (and green when selected...what it does). In the end I want non-selected nodes to be just text without background, since the area which is red in the SSCCE has a gradient fill in my application.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
public class SimpleTree extends JFrame
{
public static void main(final String[] args)
{
new SimpleTree();
}
public SimpleTree()
{
super("Creating a Simple JTree");
final Container content = this.getContentPane();
content.setBackground(Color.RED);
final Object[] hierarchy = { "javax.swing", "javax.swing.border", "javax.swing.colorchooser", "javax.swing.event", "javax.swing.filechooser", new Object[] { "javax.swing.plaf", "javax.swing.plaf.basic", "javax.swing.plaf.metal", "javax.swing.plaf.multi" }, "javax.swing.table",
new Object[] { "javax.swing.text", new Object[] { "javax.swing.text.html", "javax.swing.text.html.parser" }, "javax.swing.text.rtf" }, "javax.swing.tree", "javax.swing.undo" };
final DefaultMutableTreeNode root = this.processHierarchy(hierarchy);
final JTree tree = new JTree(root);
tree.setOpaque(false);
tree.setCellRenderer(new MyCellRenderer());
final JScrollPane scroller = new JScrollPane(tree);
scroller.getViewport().setOpaque(false);
scroller.setOpaque(false);
content.add(scroller, BorderLayout.CENTER);
this.setSize(275, 300);
this.setVisible(true);
}
/**
* Small routine that will make node out of the first entry in the array,
* then make nodes out of subsequent entries and make them child nodes of
* the first one. The process is repeated recursively for entries that are
* arrays.
*/
private DefaultMutableTreeNode processHierarchy(final Object[] hierarchy)
{
final DefaultMutableTreeNode node = new DefaultMutableTreeNode(hierarchy[0]);
DefaultMutableTreeNode child;
for (int i = 1; i < hierarchy.length; i++)
{
final Object nodeSpecifier = hierarchy[i];
if (nodeSpecifier instanceof Object[]) // Ie node with children
child = this.processHierarchy((Object[]) nodeSpecifier);
else
child = new DefaultMutableTreeNode(nodeSpecifier); // Ie Leaf
node.add(child);
}
return (node);
}
public class MyCellRenderer extends DefaultTreeCellRenderer
{
#Override
public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean sel, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus)
{
final Component ret = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
final DefaultMutableTreeNode node = ((DefaultMutableTreeNode) (value));
this.setText(value.toString());
if (sel)
{
this.setOpaque(true);
this.setBackground(Color.GREEN);
}
else
{
this.setOpaque(false);
this.setBackground(null);
}
return ret;
}
}
}
You should override getBackgroundNonSelectionColor,getBackgroundSelectionColor and getBackground of DefaultTreeCellRenderer and return appropriate values like so:
public class MyCellRenderer extends DefaultTreeCellRenderer {
#Override
public Color getBackgroundNonSelectionColor() {
return (null);
}
#Override
public Color getBackgroundSelectionColor() {
return Color.GREEN;
}
#Override
public Color getBackground() {
return (null);
}
#Override
public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean sel, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) {
final Component ret = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
final DefaultMutableTreeNode node = ((DefaultMutableTreeNode) (value));
this.setText(value.toString());
return ret;
}
}
which will produce:
Other suggestions:
Create and manipulate Swing components on Event Dispatch Thread.
Dont extend JFrame unnecessarily rather create an instance and use that.
Dont call setSize on JFrame rather use a correct LayoutManager and/or override getPreferredSize() and call pack() on JFrame before setting it visible but after adding all components.
Remember to call JFrame#setDefaultCloseOperation with either DISPOSE_ON_CLOSE or EXIT_ON_CLOSE (DISPOSE_XXX is usually preferred unless using Timers as this will allow main(String[] args) to continue its execution after Gui has been closed).
To avoid background refilling, just put UIManager.put("Tree.rendererFillBackground", false); before new SimpleTree(); or after super("Creating a Simple JTree");.
I have a created a following renderer which renders the JTree with checkboxes and I want to add different color and icon to different nodes. How do I do it? Please help me. Thank you in advance.
class CheckTreeCellRenderer extends JPanel implements TreeCellRenderer {
private CheckTreeSelectionModel selectionModel;
private TreeCellRenderer delegate;
private TristateCheckBox checkBox = new TristateCheckBox("",null,true);
public static final State NOT_SELECTED = new State();
public static final State SELECTED = new State();
public static final State DONT_CARE = new State();
public CheckTreeCellRenderer(TreeCellRenderer delegate, CheckTreeSelectionModel selectionModel) {
this.delegate = delegate;
this.selectionModel = selectionModel;
setLayout(new BorderLayout());
setOpaque(false);
checkBox.setState(Boolean.TRUE);
revalidate();
checkBox.setOpaque(false);
}
public Component getTreeCellRendererComponent
(JTree tree, Object value, boolean selected, boolean expanded,
boolean leaf, int row, boolean hasFocus) {
Component renderer = delegate.getTreeCellRendererComponent
(tree, value, selected, expanded, leaf, row, hasFocus);
TreePath path = tree.getPathForRow(row);
if(path!=null){
if(selectionModel.isPathSelected(path, true)) {
checkBox.setState(Boolean.TRUE);
}
else {
checkBox.setState
(selectionModel.isPartiallySelected(path) ? null : Boolean.FALSE);
}
}
setBackground(Color.pink);
removeAll();
add(checkBox, BorderLayout.WEST);
add(renderer, BorderLayout.CENTER);
return this;
}
}
The best place to learn about TreeCellRenderers is from the tutorial (at the bottom of the page).
Instead of adding renderer to BorderLayout.CENTER, you can just add a different icon of whatever color you like.
In order for your setBackground(Color.PINK) to have any visible effect, you should change the setOpaque(false) to setOpaque(true) in your constructor. That said, I second #John's suggestion that you read up on renderers in the Sun tutorials.