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. :(
Related
I have a problem in which I hope SwingWorker can help me, but I am not quite sure how to integrate it in my program.
The problem:
In a CardLayout I have a button on Card1 that opens Card2.
Card2 has a JList with a custom renderer(extending JLabel) which will display on average 1 to 6 images which are:
PNGs
around 500kb in size
loaded via imageIO with the change of cards
the renderer applies heavy operations such as image scaling or blurring and than sets the image as JLabel icon.
This can almost take up to a second if around 6 images have to be rendered, which is does not happen frequently but even that occasional split second of unresponsiveness feels bad.
Now I thought a SwingWorker might help here, but I'm thoroughly confused as to how I would have to integrate it.
Assuming we had this Code snippet
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class Example {
private JPanel mainPanel = new JPanel();
private JList<Product> list = new JList();
private JScrollPane scroll = new JScrollPane();
private Map<String, Color> colorMap = new HashMap<>();
public Example() {
colorMap.put("red", Color.red);
colorMap.put("blue", Color.blue);
colorMap.put("cyan", Color.cyan);
colorMap.put("green", Color.green);
colorMap.put("yellow", Color.yellow);
mainPanel.setBackground(new Color(129, 133, 142));
scroll.setViewportView(list);
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setPreferredSize(new Dimension(80,200));
list.setCellRenderer(new CustomRenderer());
DefaultListModel model = new DefaultListModel();
model.addElement(new Product("red"));
model.addElement(new Product("yellow"));
model.addElement(new Product("blue"));
model.addElement(new Product("red"));
model.addElement(new Product("cyan"));
list.setModel(model);
mainPanel.add(scroll);
}
public static void main(String[] args) throws IOException {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame("WorkerTest");
frame.setContentPane(new Example().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(300, 300);
frame.setMinimumSize(new Dimension(160, 255));
frame.setVisible(true);
}
});
}
class CustomRenderer extends JLabel implements ListCellRenderer<Product> {
private Product product;
public CustomRenderer() {
setOpaque(false);
}
#Override
public Component getListCellRendererComponent(JList<? extends Product> list, Product product, int index, boolean isSelected, boolean cellHasFocus) {
this.product = product;
/**
* in the actual code image is png with alpha channel respectively named to the productID of the JList object
*
* String id = product.getId();
* image = ImageIO.read(getClass().getResource("../../resources/images/" + id + ".png"));
*/
BufferedImage image1 = new BufferedImage(80, 50, BufferedImage.TYPE_INT_RGB);
BufferedImage image2 = new BufferedImage( 80, 75, BufferedImage.TYPE_INT_RGB);
Graphics g = image2.getGraphics();
/**
* this is only an example, in the actual code I might also apply gaussian blurs or rescale several time
*/
g.drawImage(image1,0,0,null);
setIcon(new ImageIcon(image2));
return this;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(colorMap.get(product.getColor()));
g.fillRect(0,0,80,75);
}
}
class Product {
String productID;
String color;
public Product(String color) {
this.color = color;
}
public String getColor() {
return color;
}
public String getProductID() {
return productID;
}
}
}
would I have to call a SwingWorker from every getListCellRendererComponent call
to take over the image operations ?
Is SwingWorker even the right tool for this problem?
any help as to how I can make this part of my GUI faster would be greatly appreciated.
EDIT:
Hovercraft Full Of Eels mentioned that preloading the images could help and that loading the images from the renderer is fundamentally wrong.
This leads me to another Question:
I have a list(let's call it list1) with around 3000 objects each object has a 8kb jpg thumbnail which is load via object ID (also during the rendering)
The list displays around 6 to 12 of these thumbnail at the same time (due to the List's Dimension)
when the user selects an object he can press a button to display Card2 from the Cardlayout mentioned in the original question and it's list(list2) with the Object
and all it's related Object in non thumbnail view (500kb png + heavy image operation). Now I think it would be feasible to preload the non thumbnail image of the Object and it's relations selected in the first list which would be around 1-6 images. If I understood correctly what Hovercraft Full Of Eels said, then I could use a SwingWorker to load these Images after the selection of an Object from list1.
But what about the around 3000 images from list1, the program seemingly is not slowed down or becomes unresponsive because they are rather small in size and there are no heavy operations on the thumbnails, but they are still load form the list1's renderer. Would it make sense to preload the several thousand thumbnails ?
btw. feel free to tell me if this kind of question edit is not wished for and if it should be made into a question of itself.
One approach might be the following:
Whenever a cell renderer component for a certain element (Product) is requested, you check whether the matching image is already loaded. If not, you start a Swing worker that does the work of loading and processing the image in the background. When the worker is done, the image is placed into a cache for later lookup. In the meantime, you let the renderer just say "Loading..." or something.
A very quick implementation is here:
And as an MCVE:
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.SwingWorker;
public class LazyImageLoadingCellRendererTest
{
private JPanel mainPanel = new JPanel();
private JList<Product> list = new JList<Product>();
private JScrollPane scroll = new JScrollPane();
public LazyImageLoadingCellRendererTest()
{
mainPanel.setBackground(new Color(129, 133, 142));
scroll.setViewportView(list);
scroll.setHorizontalScrollBarPolicy(
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setPreferredSize(new Dimension(80, 200));
list.setCellRenderer(new LazyImageLoadingCellRenderer<Product>(list,
LazyImageLoadingCellRendererTest::loadAndProcessImage));
DefaultListModel<Product> model = new DefaultListModel<Product>();
for (int i=0; i<1000; i++)
{
model.addElement(new Product("id" + i));
}
list.setModel(model);
mainPanel.add(scroll);
}
public static void main(String[] args) throws IOException
{
EventQueue.invokeLater(new Runnable()
{
#Override
public void run()
{
JFrame frame = new JFrame("WorkerTest");
frame.setContentPane(
new LazyImageLoadingCellRendererTest().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(300, 300);
frame.setMinimumSize(new Dimension(160, 255));
frame.setVisible(true);
}
});
}
private static final Random random = new Random(0);
private static BufferedImage loadAndProcessImage(Product product)
{
String id = product.getProductID();
int w = 100;
int h = 20;
BufferedImage image =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.GREEN);
g.fillRect(0, 0, w, h);
g.setColor(Color.BLACK);
g.drawString(id, 10, 16);
g.dispose();
long delay = 500 + random.nextInt(3000);
try
{
System.out.println("Load time of " + delay + " ms for " + id);
Thread.sleep(delay);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return image;
}
class Product
{
String productID;
public Product(String productID)
{
this.productID = productID;
}
public String getProductID()
{
return productID;
}
}
}
class LazyImageLoadingCellRenderer<T> extends JLabel
implements ListCellRenderer<T>
{
private final JList<?> owner;
private final Function<? super T, ? extends BufferedImage> imageLookup;
private final Set<T> pendingImages;
private final Map<T, BufferedImage> loadedImages;
public LazyImageLoadingCellRenderer(JList<?> owner,
Function<? super T, ? extends BufferedImage> imageLookup)
{
this.owner = Objects.requireNonNull(
owner, "The owner may not be null");
this.imageLookup = Objects.requireNonNull(imageLookup,
"The imageLookup may not be null");
this.loadedImages = new ConcurrentHashMap<T, BufferedImage>();
this.pendingImages =
Collections.newSetFromMap(new ConcurrentHashMap<T, Boolean>());
setOpaque(false);
}
class ImageLoadingWorker extends SwingWorker<BufferedImage, Void>
{
private final T element;
ImageLoadingWorker(T element)
{
this.element = element;
pendingImages.add(element);
}
#Override
protected BufferedImage doInBackground() throws Exception
{
try
{
BufferedImage image = imageLookup.apply(element);
loadedImages.put(element, image);
pendingImages.remove(element);
return image;
}
catch (Exception e)
{
e.printStackTrace();
return null;
}
}
#Override
protected void done()
{
owner.repaint();
}
}
#Override
public Component getListCellRendererComponent(JList<? extends T> list,
T value, int index, boolean isSelected, boolean cellHasFocus)
{
BufferedImage image = loadedImages.get(value);
if (image == null)
{
if (!pendingImages.contains(value))
{
//System.out.println("Execute for " + value);
ImageLoadingWorker worker = new ImageLoadingWorker(value);
worker.execute();
}
setText("Loading...");
setIcon(null);
}
else
{
setText(null);
setIcon(new ImageIcon(image));
}
return this;
}
}
Note:
This is really just a quick example showing the general approach. Of course, this could be improved in many ways. Although the actual loading process is already pulled out into a Function (thus making it generically applicable for any sort of image, regardless of where it comes from), one major caveat is that: It will try to load all images. A nice extension would be to add some smartness here, and make sure that it only loads the images for which the cells are currently visible. For example, when you have a list of 1000 elements, and want to see the last 10 elements, then you should not have to wait for 990 elements to be loaded. The last elements should be priorized higher and loaded first. However, for this, a slightly larger infrastructure (mainly: an own task queue and some stronger connection to the list and its scroll pane) may be necessary. (I'll possibly tackle this one day, because it might be a nice and interesting thing to have, but until then, the example above might do it...)
would I have to call a SwingWorker from every getListCellRendererComponent call to take over the image operations ?
No, you would in fact never call a background thread from within a key rendering method. In fact this appears to be the main problem with the code above -- you're reading in images from within a rendering method, significantly reducing the perceived responsiveness of your program.
Is SwingWorker even the right tool for this problem?
Perhaps, but not where you're thinking about using it. A SwingWorker will not speed anything up, but by performing long-running tasks in the background, it would avoid blocking the Swing event thread, freezing the GUI. Best would be to read the images, once, perhaps in a SwingWorker if not done during program initiation, and save them to a variable. Do not re-read the image every time you want to render it, if this can be avoided. And again, do not read in the image from within your rendering code as this will significantly reduce the perceived responsiveness of the program.
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
In a project I've been working on, I noticed that all the JList items in my JScrollPane are hidden until the JScrollPane/JList has been clicked. The weird part is it's not completely covered. There's this white box with a transparent border that spreads out over the whole thing, covering all but a few pixels on all edges.
Pictures:
As you can see, there is this white block in the middle - notice the pink "border":
Now, once I click that white box, it goes away:
I know the magenta looks horrible, but I'm using it for contrast.
Which leads me to my question: how do I get rid of that obnoxious white box?
Here is my code:
public static void listJars(File f)
{
JCheckBox firstBox = null;
DefaultListModel<JCheckBox> model = new DefaultListModel<>();
if(mainGUI.checkList != null)
{
//System.out.println("Already exists lol: " + mainGUI.checkList.getName());
mainGUI.pluginList.remove(mainGUI.checkList);
}
//mainGUI.pluginList.repaint();
File[] files = new File(f.getPath()).listFiles();
if (files != null)
{
for (File file : files)
{
if (file.getName().endsWith(".jar") || file.getName().endsWith("._jar"))
{
JCheckBox cb = new JCheckBox(file.getName());
if(firstBox == null)
{
firstBox = cb;
}
cb.setSelected(file.getName().endsWith(".jar"));
cb.setVisible(true);
cb.setText(file.getName());
model.addElement(cb);
cb.repaint();
}
}
}
JCheckBoxList jCheckBoxList = new JCheckBoxList(model, mainGUI.textField1.getText());
jCheckBoxList.setName("pluginCheckboxList");
jCheckBoxList.setSize(mainGUI.pluginList.getSize());
mainGUI.pluginList.add(jCheckBoxList);
mainGUI.checkList = jCheckBoxList;
jCheckBoxList.setVisible(true);
jCheckBoxList.setVisibleRowCount(10);
}
And ten there's my JCheckBoxList class.
package Components;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
#SuppressWarnings("serial")
public class JCheckBoxList extends JList<JCheckBox>
{
protected static Border noFocusBorder = new EmptyBorder(1, 1, 1, 1);
protected String lastPath;
public JCheckBoxList(final String lastPath)
{
this.lastPath = lastPath;
setCellRenderer(new CellRenderer());
setBackground(Color.magenta);
addMouseListener(new MouseAdapter()
{
public void mousePressed(MouseEvent e)
{
int index = locationToIndex(e.getPoint());
if (index != -1)
{
JCheckBox checkBox = getModel().getElementAt(index);
checkBox.setSelected(!checkBox.isSelected());
repaint();
final String oldname = checkBox.getText();
if (!checkBox.isSelected())
{
checkBox.setName(checkBox.getText().substring(0, checkBox.getText().length() - 4) + "._jar");
}
else
{
checkBox.setName(checkBox.getText().substring(0, checkBox.getText().length() - 5) + ".jar");
}
System.out.println("Changed! Sel: " + checkBox.isSelected() + ", Name: " + checkBox.getName());
checkBox.setText(checkBox.getName());
String base = new File(lastPath).getParent() + "/plugins/";
boolean rename = new File(base + oldname).renameTo(new File(base + checkBox.getText()));
}
}
});
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
}
public JCheckBoxList(ListModel<JCheckBox> model, String lastPath)
{
this(lastPath);
setModel(model);
}
protected class CellRenderer implements ListCellRenderer<JCheckBox>
{
public Component getListCellRendererComponent(
JList<? extends JCheckBox> list, JCheckBox value, int index,
boolean isSelected, boolean cellHasFocus)
{
//Drawing checkbox, change the appearance here
value.setBackground(isSelected ? getSelectionBackground()
: getBackground());
value.setForeground(isSelected ? getSelectionForeground()
: getForeground());
value.setEnabled(isEnabled());
value.setFont(getFont());
value.setFocusPainted(false);
value.setBorderPainted(true);
value.setBorder(BorderFactory.createEmptyBorder(0, 10, 5, 0));
return value;
}
}
}
And then there's my scroll pane, which has these settings (using the Intelliji IDEA UI designer):
Any ideas?
mainGUI.pluginList.add(jCheckBoxList);
mainGUI.checkList = jCheckBoxList;
jCheckBoxList.setVisible(true);
jCheckBoxList.setVisibleRowCount(10);
Looks to me like you are dynamically adding components to a visible GUI.
When you do this the basic code is:
panel.add(...);
panel.revalidate();
panel.repaint();
You should set the visibleRowCount() before the above code is executed.
Also:
Swing components are visible by default so you don't need the setVisible(true).
You may want to consider using a one column JTable since it already supports a checkbox renderer and editor.
Edit:
The solution I gave you above is the general solution. A scroll pane is different, you should only ever add a component to the viewport.
Based on your incorrect solution you should be using:
//mainGUI.pluginList.add(jCheckBoxList);
mainGUI.pluginList.setViewportView(jCheckBoxList);
The problem with posting only a few random lines of code is that we don't know the full context of the code. I did not realize "pluginList" was actually a scrollpane. Usually the variable name will have scroll or scrollpane in the name.
mainGUI.pluginList.setViewportView(mainGUI.checkList); // pluginList is the JScrollPane.
Do that, and it fixes everything! Put it in with my listJars method.
Its been a while since i built a desktop JAVA application.. after lots of documentation and doing implementation tests, i still have not found an image grid solution.
Either Java lacks such a ready-to-use component (?!) or you tell me to brush up my google-fu. :)
I have a very simple technical premises: a JDialog that allows the user to pick an image. Input is a Map<Integer, String> list that holds filenames. Output is the Integer key the user chose. GUI also is simple: user chooses 1 image using mouse or keyboard, and dialog closes. All images are 80x80px and loaded from filename, not a resource.
I tried several approaches so far this morning:
Search for components/widgets that show scrollable imagegrid that can flow to the left. (no dice)
Search for components/widgets that show scrollable imagegrid (no dice)
Search for any components/widgets/gui-libs (no dice .. do these even exist?!)
Try and implement myJList.setModel(), but i cant get it to just take my Map<> and show thumbnails. (overcomplicates!)
Try and implement myJPanel.setlayout(new FlowLayout(..)) with several myJPanel.add(new JButton(..)) which just creates a bunch of JButton on a JPanel, which each need a event handler. I wonder how scrolling and keyboard input is going to work out, and how i'm supposed to keep/reference my Map<> key values. (overcomplicates?)
In lieu of your answer, i am now working on the latter, which should work but i cant believe everyone needs to reinvent the same GUI wheel here. How to have the user select an image from my Map<Integer, String>? Are there JAVA libraries/widgets/components that i should look to avoid this?
I hope this isn't being modded down, i have no working implementation with error to show you guys.. this question is about how/where to find the components or what approaches would be better. Its 2014 and i cant believe that JAVA still requires me to build my own "GUI component" just to see some images.. not even Delphi or Mono does that.
If all you want is a grid of images, and having them selectable, consider using a JList, filling it with appropriate ImageIcons, and giving it a ListSelectionListener. In the Listener you can close the enclosing dialog when a selection has been made.
You state:
Try and implement myJList.setModel(), but i cant get it to just take my Map<> and show thumbnails. (overcomplicates!)
You need to use your Map to populate your ListModel, and set that Model to the JList's model.
For example:
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dialog.ModalityType;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
#SuppressWarnings("serial")
public class ImageGridPanel extends JPanel {
public static final String PATH = "http://images-2.drive.com.au/2011/";
public static final String[] CARS = {
"04/15/2308961/giulietta_1024-80x80.jpg",
"11/18/2781958/audi-a1-sportback_600-80x80.jpg",
"12/23/2856762/fiat-500-80x80.jpg",
"01/12/2129944/Honda-Civic-Sedan-concept-1_600-80x80.jpg",
"12/23/2856581/mini-roadster-80x80.jpg",
"12/23/2856571/hyundai-veloster-80x80.jpg",
"12/23/2856771/hyundai-i30-80x80.jpg",
"12/23/2856580/mini-coupe-80x80.jpg" };
private DefaultListModel<Car> carModel = new DefaultListModel<>();
final JTextField textField = new JTextField(20);
public ImageGridPanel() {
for (String carPath : CARS) {
String path = PATH + carPath;
try {
URL imgUrl = new URL(path);
BufferedImage img = ImageIO.read(imgUrl);
ImageIcon icon = new ImageIcon(img);
String name = carPath.substring(carPath.lastIndexOf("/"));
name = name.substring(1, name.lastIndexOf("-"));
carModel.addElement(new Car(name, icon));
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
ShowGridAction showAction = new ShowGridAction("Car Grid", carModel);
JButton showGridBtn = new JButton(showAction);
add(showGridBtn);
add(textField);
}
private class ShowGridAction extends AbstractAction {
private CarGridPanel carGridPanel;
public ShowGridAction(String name, DefaultListModel<Car> carModel) {
super(name);
carGridPanel = new CarGridPanel(carModel);
}
public CarGridPanel getCarGridPanel() {
return carGridPanel;
}
#Override
public void actionPerformed(ActionEvent e) {
Window win = SwingUtilities.getWindowAncestor((Component) e.getSource());
JDialog dialog = new JDialog(win, "Cars", ModalityType.APPLICATION_MODAL);
dialog.add(carGridPanel);
dialog.pack();
dialog.setLocationRelativeTo(null);
int x = dialog.getLocation().x;
int y = dialog.getLocation().y - 150;
dialog.setLocation(x, y);
dialog.setVisible(true);
Car selectedCar = carGridPanel.getSelectedCar();
if (selectedCar != null) {
textField.setText(selectedCar.getName());
}
}
}
private static void createAndShowGui() {
JFrame frame = new JFrame("ImageGrid");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(new ImageGridPanel());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
class Car {
String name;
Icon icon;
public Car(String name, Icon icon) {
this.name = name;
this.icon = icon;
}
public String getName() {
return name;
}
public Icon getIcon() {
return icon;
}
}
#SuppressWarnings("serial")
class CarGridPanel extends JPanel {
private JList<Car> carList = new JList<>();
private Car selectedCar;
public CarGridPanel(ListModel<Car> model) {
carList.setModel(model);
carList.setLayoutOrientation(JList.HORIZONTAL_WRAP);
carList.setVisibleRowCount(2);
carList.setCellRenderer(new DefaultListCellRenderer() {
#Override
public Component getListCellRendererComponent(JList<?> list,
Object value, int index, boolean isSelected, boolean cellHasFocus) {
if (value != null) {
Car carValue = (Car) value;
value = carValue.getIcon();
} else {
value = "";
}
return super.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus);
}
});
setLayout(new BorderLayout());
add(new JScrollPane(carList));
carList.addListSelectionListener(new ListListener());
}
public Car getSelectedCar() {
return selectedCar;
}
private class ListListener implements ListSelectionListener {
#Override
public void valueChanged(ListSelectionEvent e) {
selectedCar = carList.getSelectedValue();
Window win = SwingUtilities.getWindowAncestor(CarGridPanel.this);
win.dispose();
}
}
}
No, Java doesn't have what you want.
Java is a general-purpose programming language, not a toolset, particularly not a specialized desktop GUI toolset. This is not a denigration of the language, just a statement of a purpose that it was not developed to fulfill.
If Delphi or Mono or anything has your particular widget, then I suggest you program in that, instead. This is not a denigration of you, just an observation that, if you do not want to put together the widget you want from lower-level components and code, then Java is not the right language/tool for you to use to do it.
As for not believing that Java "still requires" you to build your own component, I can only say that you don't get to choose which languages provide which features. I'm just as glad Java isn't littered with your component and the hundreds of others that people like you would come up with that they think Java should provide. It's big enough as it is.
I had some problems with freezing SWING GUIs when re-rendering a JTable with a custom cell renderer in Java. So I asked the question "Why does a JTable view update block the entire GUI?". The answers pointed to the fact, that a JList without modifying JTable and overwriting doLayout might be a better choice. So I implemented the example with a JList and ran into the same problem: while generating data, everything works fine and the progress bar moves. But when the view is updated, the program freezes and the progress bar stops moving.
Please note, that the sleep statement is there only to let the generation take a longer, more realistic time (reading thousands of data sets via JDBC and create objects out of them takes a lot time). One could remove it and increment the number of generated items. But you can clearly see, that the HTML rendering is quite slow. But I need this colors and the two lines (if not necessarily so many different colors).
So could you please tell me, where my mistake is? I think, that EDT and other work is separated through separate threads and I cannot see any mistke.
Update: I looked around at SO and found this question "https://stackoverflow.com/a/20813122/2429611". There is said:
The more interesting question would be how to avoid that UI blocking, but I don't think that's possible with just Swing, you'll have to implement some lazy loading, or rendering in batches.
This would mean, that I cannot solve my problem. Is this correct?
package example;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.AbstractListModel;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
public class ListExample extends AbstractListModel {
static List<DemoObject> internalList = new ArrayList<>();
#Override
public int getSize() {
return internalList.size();
}
#Override
public DemoObject getElementAt(int index) {
return internalList.get(index);
}
public void fireContentsChanged() {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
fireContentsChanged(this, 0, -1);
}
});
}
static class MyCellRenderer extends JLabel implements ListCellRenderer<ListExample.DemoObject> {
public MyCellRenderer() {
setOpaque(true);
}
#Override
public Component getListCellRendererComponent(JList<? extends ListExample.DemoObject> list,
ListExample.DemoObject value,
int index,
boolean isSelected,
boolean cellHasFocus) {
setText("<html>" + value.toString()
+ "<br/>"
+ "<span bgcolor=\"#ff0000\">Line 2; Color = " + value.c + "</span>");
Color background;
Color foreground;
// check if this cell represents the current DnD drop location
JList.DropLocation dropLocation = list.getDropLocation();
if (dropLocation != null
&& !dropLocation.isInsert()
&& dropLocation.getIndex() == index) {
background = Color.BLUE;
foreground = Color.WHITE;
// check if this cell is selected
} else if (isSelected) {
background = Color.RED;
foreground = Color.WHITE;
// unselected, and not the DnD drop location
} else {
background = value.c; //Color.WHITE;
foreground = Color.BLACK;
};
setBackground(background);
setForeground(foreground);
return this;
}
}
static class DemoObject {
String str;
Color c;
public DemoObject(String str, int color) {
this.str = str;
this.c = new Color(color);
}
#Override
public String toString() {
return str;
}
}
static JPanel overlay;
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame("Example");
frame.setLayout(new BorderLayout(4, 4));
// Add JTable
final ListExample model = new ListExample();
JList list = new JList(model);
list.setCellRenderer(new MyCellRenderer());
frame.add(new JScrollPane(list), BorderLayout.CENTER);
// Add button
Box hBox = Box.createHorizontalBox();
hBox.add(new JButton(new AbstractAction("Load data") {
#Override
public void actionPerformed(ActionEvent e) {
new Thread(new Runnable() {
#Override
public void run() {
overlay.setVisible(true);
internalList.clear();
System.out.println("Generating data ...");
SecureRandom sr = new SecureRandom();
for (int i = 0; i < 10000; i++) {
internalList.add(
new DemoObject(
"String: " + i + " (" + sr.nextFloat() + ")",
sr.nextInt(0xffffff)
)
);
// To create the illusion, that data are
// fetched via JDBC (which takes a little
// while), this sleep statement is embedded
// here. In a real world scenario, this wait
// time is caused by talking to the database
// via network
if (i%10 == 0) {
try {
Thread.sleep(1);
} catch (Exception e) {
}
}
}
System.out.println("Updating view ...");
model.fireContentsChanged();
overlay.setVisible(false);
System.out.println("Finished.");
}
}).start();
}
}));
hBox.add(Box.createHorizontalGlue());
frame.add(hBox, BorderLayout.NORTH);
// Create loading overlay
overlay = new JPanel(new FlowLayout(FlowLayout.CENTER)) {
#Override
protected void paintComponent(Graphics g) {
g.setColor(new Color(0, 0, 0, 125));
g.fillRect(0, 0, getWidth(), getHeight());
super.paintComponent(g);
}
};
overlay.setOpaque(false);
overlay.setBackground(new Color(0, 0, 0, 125));
JProgressBar bar = new JProgressBar();
bar.setIndeterminate(true);
overlay.add(bar);
frame.setGlassPane(overlay);
frame.getGlassPane().setVisible(false);
// Create frame
frame.setSize(600, 400);
frame.setVisible(true);
}
});
}
}
there are three problems (recreating, reseting the model, and custom Renderer stoped to works)
JList (JComboBox hasn't) has an issue by removing more than 999 items, you have to set a new model to JList
see important for ComboBoxModel extends AbstractListModel implements MutableComboBoxModel for setElementAt(to hold current selection)
usage of public void fireContentsChanged() { is wrong, don't see reason to use this way, again is about to replace current, reset the model
. e.g. with success atr runtime and by recrusive testing for/if event (fired)
setModel(new DefaultListModel(list.toArray()) {
protected void fireContentsChanged(Object obj, int i, int j) {
if (!isFired)
super.fireContentsChanged(obj, i, j);
}
});