Swing JTable row borders goes away after applying Nimbus LookAndFeel - java

I have a java 8 program in which - Parent is a JFrame that has Menu, few buttons, a text field and a JTable with fixed number of non-editable rows. Number of rows and data cannot be changed dynamically.
Menu has list of UIManager.getInstalledLookAndFeels()
Initially JTable rows border are visible
If LookAndFeel change to [Nimbus javax.swing.plaf.nimbus.NimbusLookAndFeel], and then try any other LookAndFeel, rows border goes away.
I am using SwingUtilities.updateComponentTreeUI(parentFrame) to apply LnF. LnF is applying on all components including JTable, but once Nimbus LnF applied and after that choosing any other LnF, rows border went off.
As a option repaint() is not making any difference.
In image
(1) is when program starts where row borders are visible
(2) when Nimbus LnF applied
(3) LnF changed to Metal but row borders are NOT visible
Please suggest.
Sample Code:
package com.sv.runcmd;
import com.sv.core.logger.MyLogger;
import com.sv.swingui.SwingUtils;
import com.sv.swingui.component.AppExitButton;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static com.sv.core.Constants.SP_DASH_SP;
import static com.sv.swingui.UIConstants.EMPTY_BORDER;
public class LnFExample extends JFrame {
public static void main(String[] args) {
new LnFExample().initComponents();
}
private static final String APP_TITLE = "LnF";
private DefaultTableModel model;
private JTable tblCommands;
private JMenuBar mbarSettings;
public LnFExample() {
super(APP_TITLE);
SwingUtilities.invokeLater(this::initComponents);
}
/**
* This method initializes the form.
*/
private void initComponents() {
Container parentContainer = getContentPane();
parentContainer.setLayout(new BorderLayout());
JButton btnExit = new AppExitButton(true);
createTable();
JPanel topPanel = new JPanel(new GridLayout(2, 1));
topPanel.add(btnExit);
topPanel.setBorder(EMPTY_BORDER);
JPanel lowerPanel = new JPanel(new BorderLayout());
JScrollPane jspCmds = new JScrollPane(tblCommands);
lowerPanel.add(jspCmds);
parentContainer.add(topPanel, BorderLayout.NORTH);
parentContainer.add(lowerPanel, BorderLayout.CENTER);
btnExit.addActionListener(evt -> exitForm());
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent evt) {
exitForm();
}
});
createAppMenu();
setPosition();
}
private final MyLogger logger = MyLogger.createLogger("rc.log");
private void createAppMenu() {
mbarSettings = new JMenuBar();
JMenu menuSettings = new JMenu("Settings");
menuSettings.add(getThemesMenu());
mbarSettings.add(menuSettings);
setJMenuBar(mbarSettings);
}
public UIManager.LookAndFeelInfo[] getAvailableLAFs() {
return UIManager.getInstalledLookAndFeels();
}
public JMenu getThemesMenu() {
JMenu menu = new JMenu("Theme");
int i = 'a';
int x = 0;
for (UIManager.LookAndFeelInfo l : getAvailableLAFs()) {
JMenuItem mi = new JMenuItem((char) i + SP_DASH_SP + l.getName());
if (i <= 'z') {
mi.setMnemonic(i);
}
int finalX = x;
mi.addActionListener(e -> applyTheme(finalX, l));
menu.add(mi);
i++;
x++;
}
return menu;
}
UIManager.LookAndFeelInfo themeToApply;
public void applyTheme(int idx, UIManager.LookAndFeelInfo lnf) {
themeToApply = lnf;
SwingUtilities.invokeLater(this::applyLnF);
}
public void applyLnF() {
try {
UIManager.setLookAndFeel(themeToApply.getClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
SwingUtilities.updateComponentTreeUI(this);
}
private void createTable() {
model = SwingUtils.getTableModel(new String[]{"Col1"});
createRows();
Border borderBlue = new LineBorder(Color.BLUE, 1);
tblCommands = new JTable(model);
}
private void createRows() {
for (int i = 1; i <= 10; i++) {
model.addRow(new String[]{"Row- " + i});
}
}
private void setPosition() {
// Setting to right most position
pack();
GraphicsConfiguration config = getGraphicsConfiguration();
Rectangle bounds = config.getBounds();
Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(config);
int x = bounds.x + bounds.width - insets.right - getWidth();
int y = bounds.y + insets.top + 10;
setLocation(x, y);
setVisible(true);
}
/**
* Exit the Application
*/
private void exitForm() {
setVisible(false);
dispose();
logger.dispose();
System.exit(0);
}
}

I also get the same results using JDK11 on Windows 10.
I suspect the issue is with the UIResource interface. Note, this is just a tagging interface, there are no actual methods to implement.
It is my understanding that this interface should be implemented on properties of Swing components. For example on the Font, Border, Color, Icon properties of the various component.
Then when you invoke SwingUtilities.updateComponentTreeUI(parentFrame) all the properties that implement the UIResource will be replaced with the respective property from the new LAF.
Check out UIManager Defaults. It will list all the properties of each Swing component.
You will see that for most LAF's the properties are in instance of FontUIResource, or ColorUIResource, etc.
However, for the Nimbus LAF many properties lack the "...UIResource".
So I would suggest this is a Nimbus issue and I have no idea how to fix it.
Edit:
hard to tell while debugging if this was a Metal LaF issue ... or a Nimbus issue
It is a Nimbus issue.
Download the code from the above link and then make the following change:
SwingUtilities.updateComponentTreeUI( rootPane );
System.out.println(table.getDefaultRenderer(Object.class));
Now toggle the LAF between non NImbus and you will see that the default table renderer for the non Nimbus LAF's contain UIResource in the class name which would indicate to me that they implement the UIResource interface.
Now toggle to the Nimbus LAF. The renderer does not have UIResource in the class name.
Now toggle back to any other LAF and the rendering is incorrect because the Nimbus renderer has not been replaced with the proper LAF renderer.
Note, this is not a LAF issue. It is designed to work this way. This allows you to create a custom renderer that can be used in all LAF (unless of course you tag the renderer with the UIResource interface).
For some reason the Nimbus developers appear to not have tagged the renderers with the UIResource interface so once they are set they are not changed with the rest of the LAF.
So another solution would be to create a "wrapper" renderer that simply wraps a default renderer and invoke its default renderering logic but would also implement the UIResource interface . Then you would need to replace each default Nimbus renderer with a wrapper renderer.

As discussed in comments and by #camickr, this is probably a bug with Nimbus or Metal LaF.
However I have made a workaround that you can use for now.
Essentially I override prepareRenderer of the JTable and check if the Metal LaF is being used ( and that it wasnt set on start up or it will paint 2 borders around the JTable) and if those conditions are met we simply set the border for each row to new MetalBorders.TableHeaderBorder():
#Override
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
JComponent component = (JComponent) super.prepareRenderer(renderer, row, column);
if (UIManager.getLookAndFeel().getName().equals("Metal") && !wasMetalOnStartup) {
component.setBorder(new MetalBorders.TableHeaderBorder());
}
return component;
}
TestApp.java:
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.util.UUID;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.metal.MetalBorders;
import javax.swing.plaf.metal.MetalLookAndFeel;
import javax.swing.plaf.metal.OceanTheme;
import javax.swing.plaf.nimbus.NimbusLookAndFeel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
public class TestApp {
private JTable table;
private boolean wasMetalOnStartup = false;
public TestApp() {
setNimbusLookAndFeel();
initComponents();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(TestApp::new);
}
private void initComponents() {
wasMetalOnStartup = UIManager.getLookAndFeel().getName().equals("Metal");
JFrame frame = new JFrame("TestApp");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
// setup chmahe LaF button
JButton refreshButton = new JButton("Change L&F");
refreshButton.addActionListener((ActionEvent e) -> {
try {
if (!UIManager.getLookAndFeel().getName().equals("Nimbus")) {
setNimbusLookAndFeel();
} else {
setMetalLookAndFeel();
}
SwingUtilities.updateComponentTreeUI(frame);
} catch (Exception ex) {
}
});
table = new JTable() {
private static final long serialVersionUID = 1L;
#Override
public boolean isCellEditable(int row, int column) {
return false;
}
#Override
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
JComponent component = (JComponent) super.prepareRenderer(renderer, row, column);
if (UIManager.getLookAndFeel().getName().equals("Metal") && !wasMetalOnStartup) {
component.setBorder(new MetalBorders.TableHeaderBorder());
}
return component;
}
};
// setup JTable and custom table model with intial data
Object[][] data = getRandomData();
String[] columnNames = {"Random Data"};
DefaultTableModel model = new DefaultTableModel(data, columnNames);
table.setModel(model);
table.getColumnModel().getColumn(0).setPreferredWidth(300);
table.setPreferredScrollableViewportSize(table.getPreferredSize());
// add components to the panel
JScrollPane pane = new JScrollPane(table);
panel.add(pane, BorderLayout.CENTER);
panel.add(refreshButton, BorderLayout.SOUTH);
frame.add(panel);
frame.pack();
frame.setVisible(true);
}
private void setNimbusLookAndFeel() {
try {
UIManager.setLookAndFeel(new NimbusLookAndFeel());
wasMetalOnStartup = false;
} catch (Exception ex) {
}
}
private void setMetalLookAndFeel() {
try {
MetalLookAndFeel.setCurrentTheme(new OceanTheme());
UIManager.setLookAndFeel(new MetalLookAndFeel());
wasMetalOnStartup = false;
} catch (Exception ex) {
}
}
private Object[][] getRandomData() {
Object[][] data = {{UUID.randomUUID()}, {UUID.randomUUID()}, {UUID.randomUUID()}, {UUID.randomUUID()}};
return data;
}
}

Got an email reply to bug I raised from Oracle regarding this issue and they also able to reproduce it as a bug.
JDK-8258567.
Link : http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8258567

Related

How to set Combobox value to one of the choices in the list

I need to change the value of the current JComboBox value.
I got this in my Settings gui class used by Main.class:
package com.tominocz.cookieclicker;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.LineBorder;
#SuppressWarnings({ "serial", "rawtypes", "unchecked" })
public class Settings extends JFrame {
private String[] choiceList = { "Default (Arial)", "Comic Sans MS" };
public class ChoiceComboListener implements ActionListener {
public void actionPerformed(ActionEvent ev) {
JComboBox cb = (JComboBox) ev.getSource();
String currentComboSelection = (String) cb.getSelectedItem();
if (currentComboSelection.equals(choiceList[0])) {
Main.SelectedFont = "Arial";
Main.refreshGame();
Main.opt.setSize(240, 105);
Save.saveGame(Main.save);
}
if (currentComboSelection.equals(choiceList[1])) {
Main.SelectedFont = "Comic Sans MS";
Main.refreshGame();
Main.opt.setSize(240, 107);
Save.saveGame(Main.save);
}
}
}
public Settings() {
super("Settings");
setLayout(new FlowLayout());
Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
int x = (int) (((screen.getWidth()) - this.getWidth()) / 2 - 200);
int y = (int) (((screen.getHeight()) - this.getHeight()) / 2 - 100);
this.setLocation(x, y);
this.setAlwaysOnTop(true);
this.setIconImage(((ImageIcon) Main.SettingsIcon).getImage());
JPanel northPanel = new JPanel();
northPanel.setIgnoreRepaint(true);
northPanel.setBorder(new LineBorder(Color.GRAY));
this.getContentPane().add(northPanel, BorderLayout.NORTH);
JComboBox choiceCombo = new JComboBox(choiceList);
northPanel.add(Main.ChooseFont);
northPanel.add(choiceCombo);
choiceCombo.addActionListener(new ChoiceComboListener());
// LOOK HEREEEEEEEEEEEEEEEEEEEEEEE
if (Main.SelectedFont.equals("Comic Sans MS")) {
[the current JComboBox value(may be the default one - choiceList[0])] = choiceList[1];
}
// LOOK HEREEEEEEEEEEEEEEEEEEEEEEE
Main.OK.setBorder(null);
Main.OK.setBorderPainted(false);
Main.OK.setContentAreaFilled(false);
add(Main.OK);
Main.OK.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Object source = e.getSource();
if (source instanceof JButton) {
Main.opt.dispose();
}
}
});
}
}
Now how do I change the currentComboSelection value if i'm not available to use it in:
public Settings() {
...
?
Suggestions:
You can set the JComboBox's selected item one of two ways: via setSelectedIndex(int index) or setSelectedItem(Object item) where the first way, you choose the index of the desired selection and the 2nd way you choose the object held by the JComboBox (here they are Strings) that is supposed to be selected.
Other issues:
you look to be getting data from the Main class by directly accessing a field, possibly a static field at that. Don't do this as it will make your code tightly coupled, a set up for hard to find bugs, and difficult to enhance or change. Use tightly controlled getter methods instead. i.e., give Main a public Font getSelectedFont() method so that this class can call the method in a non-static way without directly fiddling with Main's fields, without making Main fields static that shouldn't be static.
The same goes for your directly changing Main's state in this Settings class -- that's not how to do Java safely for the same reasons above. Good OOPs principles don't get thrown out the window just because you're creating an event-driven GUI.
It looks like this is supposed to be a secondary window that is displayed from a main GUI window. If so, this should be displayed as a dialog, such as a JDialog or JOptionPane and not a JFrame. This will prevent the display another icon on the OS's toolbar, will guarantee that this window is on top of the main window, and will allow this window to be displayed in a modal fashion if need be.
You're setting the layout of your contentPane to FlowLayout, and yet you are trying to add a JPanel to its BorderLayout.NORTH position. This obviously won't work.
For example, simple code similar to yours that uses a JOptionPane for the secondary dialog window:
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import javax.swing.*;
public class TestSettingsPanel {
private static void createAndShowGui() {
MainPanel mainPanel = new MainPanel();
JFrame frame = new JFrame("Test Settings");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
class MainPanel extends JPanel {
private static final long serialVersionUID = 1L;
public static final String[] CHOICE_LIST = { "Default (Arial)",
"Comic Sans MS", "Courier", "Times New Roman" };
private static final int PREF_W = 500;
private static final int PREF_H = 400;
private JTextField fontField = new JTextField(10);
private SettingsPanel settingsPanel;
public MainPanel() {
fontField.setFocusable(false);
fontField.setText(CHOICE_LIST[0]);
add(new JLabel("Font: "));
add(fontField);
add(new JButton(new SettingsAction("Change Settings")));
}
#Override // let's make this bigger
public Dimension getPreferredSize() {
if (isPreferredSizeSet()) {
return super.getPreferredSize();
}
return new Dimension(PREF_W, PREF_H);
}
private class SettingsAction extends AbstractAction {
private static final long serialVersionUID = 1L;
public SettingsAction(String name) {
// give our button / Action some text
super(name);
int mnemonic = (int) name.charAt(0);
putValue(MNEMONIC_KEY, mnemonic); // set alt-key combination short-cut
}
#Override
public void actionPerformed(ActionEvent e) {
// create our settingsPanel in a lazy fashion
if (settingsPanel == null) {
settingsPanel = new SettingsPanel();
}
// set the combo selection by calling the setSetting method:
settingsPanel.setSetting(fontField.getText());
// create a JOptionPane and dsiplay the settingsPanle inside of it
Component parentComponent = MainPanel.this;
String title = "Change Font Settings";
int optionType = JOptionPane.OK_CANCEL_OPTION;
int messageType = JOptionPane.PLAIN_MESSAGE;
// display JOptionPane here
int result = JOptionPane.showConfirmDialog(parentComponent,
settingsPanel, title, optionType, messageType);
// find out what button user pressed
if (result == JOptionPane.OK_OPTION) {
// if OK button pressed, extract information from settingsPanel
String fontType = settingsPanel.getFontType();
fontField.setText(fontType);
}
}
}
}
// note that SettingsPanel is completely ignorant about the GUI that displays it.
class SettingsPanel extends JPanel {
private static final long serialVersionUID = 1L;
private JComboBox<String> choiceListCombo = new JComboBox<>(MainPanel.CHOICE_LIST);
public SettingsPanel() {
add(new JLabel("Select Font:"));
add(choiceListCombo);
}
// to allow outside classes to set the combo's selected item
public void setSetting(String text) {
if (text != null && !text.trim().isEmpty()) {
choiceListCombo.setSelectedItem(text);
}
}
// allow outside classes to extract the selected item from combo
public String getFontType() {
return (String) choiceListCombo.getSelectedItem();
}
}
You can get and set the selected Item (Object) of your Combobox by using the methods:
choiceCombo.getSelectedItem();
and
choiceCombo.setSelectedItem(anObject);

Intercepting drag and drop events in a compound component?

Say I have a JPanel containing some JTextFields. I would like to perform the same drop action for this JPanel and its children. In other words, I would like the drop action onto the children to be treated the same way as a drop action onto the JPanel.
Is there any other way other than setting the same DropTargetListener for the JPanel and its children?
I know that if I set the TransferHandler of those JTextFields to null, the JPanel will receive the drag and drop event. However, this will destroy the copy and paste functionality of the textfield.
I know that I can intercept mouse events with JLayer. Is there something like this for drag events?
In the end, I added listeners separately to the child components. Because I needed the drop location relative to the parent as well, I used SwingUtilities.convertToPoint() separately on the child components. Which means a lot of different listeners used -- more memory usage. But seems to be the best way for now.
If you want to be able to drop items on a panel but you want any components on the panel to be ignored for dropping purposes you can deactivate the drop target on each of the components added to the panel. You will still be able to cut and paste within them, and there is even a way to initiate a drag from them, but you won't be able to drop anything on them - the drop event goes straight through them to the drop target associated with the panel.
To do this, simply call
component.getDropTarget().setActive(false);
for each component on the panel.
I found this useful when building a calendar panel where I wanted to be able to drag appointments around but drop them on the panel even if it was (partially or completely) covered in other appointments.
Not sure if this is what you had in mind, but, I basically added the same DropTargetListener to all of my components, which meant that it didn't matter where I dragged/dropped the incoming request, all the components triggered the same events...
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class TestDragNDrop100 {
public static void main(String[] args) {
new TestDragNDrop100();
}
public TestDragNDrop100() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel implements DropTargetListener {
public TestPane() {
DropTarget dt = new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, this, true);
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
for (int y = 0; y < 4; y++) {
gbc.gridy = y;
for (int x = 0; x < 4; x++) {
gbc.gridx = x;
JTextField field = new JTextField(10);
DropTarget child = new DropTarget(field, DnDConstants.ACTION_COPY_OR_MOVE, this, true);
add(field, gbc);
}
}
}
#Override
public void dragEnter(DropTargetDragEvent dtde) {
System.out.println("DragEnter - " + dtde.getDropTargetContext().getComponent());
}
#Override
public void dragOver(DropTargetDragEvent dtde) {
System.out.println("DragOver - " + dtde.getDropTargetContext().getComponent());
}
#Override
public void dropActionChanged(DropTargetDragEvent dtde) {
System.out.println("dropActionChanged" + dtde.getDropTargetContext().getComponent());
}
#Override
public void dragExit(DropTargetEvent dte) {
System.out.println("dragExit" + dte.getDropTargetContext().getComponent());
}
#Override
public void drop(DropTargetDropEvent dtde) {
System.out.println("drop" + dtde.getDropTargetContext().getComponent());
}
}
}
I should also note. I tested the fields cut/copy/paste functionality and had no issues.
Implement a custom TransferHandler on the container which delegates to its children as appropriate, something along the lines of:
for (int i = 0; i < 5; i++) {
parent.add(new JTextField("item" + i, 20));
};
TransferHandler handler = new TransferHandler() {
#Override
public boolean canImport(TransferSupport support) {
TransferHandler childHandler = getTargetHandler();
return childHandler.canImport(
getTargetSupport(support));
}
protected TransferSupport getTargetSupport(TransferSupport support) {
return new TransferSupport(getTarget(), support.getTransferable());
}
protected TransferHandler getTargetHandler() {
return getTarget().getTransferHandler();
}
protected JComponent getTarget() {
return (JComponent) parent.getComponent(0);
}
#Override
public boolean importData(TransferSupport support) {
return getTargetHandler().importData(getTargetSupport(support));
}
};
parent.setTransferHandler(handler);

CellRenderer Item repaint

I created my own CellRenderer which include some several strings and a JProgressBar in a JList Item ... But the JProgressBar and so the whole JList Item will painted one time and I'm looking for a way to repaint the Items ... I tried to start a thread, that will permanently repaint ... But I don't know what I have to repaint to get the result ...
JList repaint ... no result
CellRenderer repaint ... no result
JFrame repaint ... no result
Does anyone understand my Problem and know a way out?
Thank you very much!
UPDATE: [Update deleted]
NEXT UPDATE:
import java.awt.Component;
import java.awt.Dimension;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.ListCellRenderer;
public class Main extends JFrame{
public DefaultListModel contentListModel = new DefaultListModel();
public MyCellRenderer MCR = new MyCellRenderer();
public JList contentList = new JList(contentListModel);
public Main(){
super("Example");
setMinimumSize(new Dimension(300,300));
setDefaultCloseOperation(EXIT_ON_CLOSE);
contentList.setCellRenderer(MCR);
contentListModel.addElement("");
contentListModel.addElement("");
add(contentList);
}
public static void main(String[] args){
new Main().setVisible(true);
}
class MyCellRenderer extends JPanel implements ListCellRenderer{
public MyCellRenderer(){
JProgressBar jpb = new JProgressBar();
jpb.setIndeterminate(true);
add(jpb);
}
#Override
public Component getListCellRendererComponent(JList arg0, Object arg1,
int arg2, boolean arg3, boolean arg4) {
// TODO Auto-generated method stub
return this;
}
}
}
As already mentioned, renderers are not live components, that is not part of component hierarchy. Consequently, "natural" animation effects (like f.i. the movement of the indeterminate progressbar) are lost. A trick that might work - but beware: that's highly LAF dependent! - is to lie to the system and report the bar as being dispayable always. That combined with a timer faking a new value every x ms might show the animation again:
public static class ProgressBarRenderer implements TableCellRenderer {
/** The bar. */
private JProgressBar indeterminate = new JProgressBar() {
// fake displayable to trigger ui animation
#Override
public boolean isDisplayable() {
return true;
};
};
/** The bar. */
private JProgressBar determinate = new JProgressBar() ;
public ProgressBarRenderer() {
indeterminate.setStringPainted(true);
indeterminate.setIndeterminate(true);
indeterminate.setString(null);
}
#Override
public Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus, int row,
int column) {
int pbi = (Integer) value;
if (pbi < 0) {
return indeterminate;
}
determinate.setValue(pbi);
return determinate;
}
}
// a timer driving the animation
Action update = new AbstractAction() {
int count;
#Override
public void actionPerformed(ActionEvent e) {
table.setValueAt(-1, 0, AncientSwingTeam.INTEGER_COLUMN);
}
};
new Timer(100, update).start();
Cell renderers are static/rubber stamps of components, they are not "real", active components. They are simply "painted" onto the surface of the view that uses them.
This means that it's not (really) possible to repaint them as such. You can encourage the list to update by changing the value of the row you want to change. This will cause the model to trigger an update that will cause the list to repaint.
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class TestProgressListCellRenderer {
public static void main(String[] args) {
new TestProgressListCellRenderer();
}
public TestProgressListCellRenderer() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
final DefaultListModel<Float> model = new DefaultListModel<>();
model.addElement(0f);
JList<Float> list = new JList<>(model);
list.setCellRenderer(new ProgressListCellRenderer());
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new JScrollPane(list));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
Timer timer = new Timer(125, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
float value = model.getElementAt(0);
value += 0.01;
if (value >= 1f) {
value = 1f;
((Timer)e.getSource()).stop();
}
model.setElementAt(value, 0);
}
});
timer.setRepeats(true);
timer.setCoalesce(true);
timer.start();
}
});
}
public class ProgressListCellRenderer extends JProgressBar implements ListCellRenderer<Float> {
#Override
public Component getListCellRendererComponent(JList<? extends Float> list, Float value, int index, boolean isSelected, boolean cellHasFocus) {
setValue(Math.round(value * 100));
return this;
}
}
}
This is a simple example and I tried using setIndeterminate, but I couldn't get it to update (very annoying), but this might trigger some ideas
+1 for interesting issue,
but Renderer in Swing isn't designated to showing animations, this is only static painting, illusion, snapshot,
you have to animate this Object in your code, but not easy job for JProgressBar.setIndeterminate(true); if is possible without dirty hacks,
required own JProgressBar
required own animation for ListCellRenderer, and the result could be so far in compare with JProgressBar placed in JPanel e.g.
I think JList couldn't be proper JComponents, use JTable with one Column and /or without JTableHeader
simple workaround to use JPanel (layed by GridLayout) with JProgressBars for JProgressBar.setIndeterminate(true);, then output to the Swing GUI will be very similair to the JTable (without JTableHeader or JList)
put this JPanel to JScrollPane
override getPreferredSize for JScrollPane, then you'll simulating JList.setVisibleRowCount properly
change JScrollBar.setUnitIncrement for natural scrolling for JPanel with JComponents and with JList, JTable, JTextArea placed in JScrollPane
You may be able to drive the animation with your own javax.swing.Timer in a custom ProgressBarUI, seen here; incrementAnimationIndex() looks promising, but I've not tried it.
Absent a better solution, you can use a ProgressIcon in a DefaultListCellRenderer; the approach is illustrated here in a JTabbedPane.
It's easy.
public void updateListData(final JList myList) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
ListModel lm = myList.getModel();
DefaultListModel dlm = new DefaultListModel();
for (int i = 0; i < lm.getSize(); i++) {
dlm.addElement(lm.getElementAt(i));
}
myList.setModel(dlm);
}
});
}
The code above can be called from EDT and from another thread. But you also should read how to deal with Swing components and understand models (ListModel, TableModel, etc). To ask an element in JList to repaint you should modify its object in model.

JTable column widths only changing after two requests

In my application, I have a two column table which will be dynamically populated through the life of the program. I have a method called updateTableWidths which, when called, does the following:
makes the first column just as wide as it needs to be to fit cell contents
makes the second column just as wide as it needs to be to fit cell contents
if the entire table is wider than its container, enables a horizontal scrollbar
otherwise, lets last column stretch to fill container
It seems to work great, but for some really strange reason it needs to be called twice (but only in some cases, i.e. when table widths are growing rather than shrinking). The SSCCE below illustrates my dilemma.
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
#SuppressWarnings("serial")
public class TableResizeTest extends JComponent {
private JFrame frame;
private JScrollPane scrollPane;
private DefaultTableModel tableModel;
private JTable table;
private JPanel panel;
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Throwable e) {
e.printStackTrace();
}
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
TableResizeTest window = new TableResizeTest();
window.frame.setVisible(true);
window.frame.requestFocusInWindow();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public TableResizeTest() {
initialize();
}
private void initialize() {
frame = new JFrame("Test");
frame.setBounds(300, 300, 200, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
tableModel = new DefaultTableModel(new Object[]{"Col_1", "Col_2"},0);
table = new JTable(tableModel);
table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
table.setAlignmentY(Component.TOP_ALIGNMENT);
table.setAlignmentX(Component.LEFT_ALIGNMENT);
table.setBorder(new EmptyBorder(0, 0, 0, 0));
table.setFillsViewportHeight(true);
table.setTableHeader(null);
table.setEnabled(false);
table.getColumnModel().setColumnMargin(0);
frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));
panel = new JPanel();
frame.getContentPane().add(panel);
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
JButton btnFillTableShort = new JButton("Fill table short");
btnFillTableShort.setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(btnFillTableShort);
JButton btnFillTableLong = new JButton("Fill table long");
btnFillTableLong.setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(btnFillTableLong);
JButton btnUpdateTableWidths = new JButton("Update table widths");
btnUpdateTableWidths.setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(btnUpdateTableWidths);
btnUpdateTableWidths.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
updateTableWidths();
}
});
btnFillTableLong.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
fillTableLong();
}
});
btnFillTableShort.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
fillTableShort();
}
});
scrollPane = new JScrollPane(table);
scrollPane.setPreferredSize(new Dimension(200, 100));
scrollPane.setMaximumSize(new Dimension(32767, 100));
frame.getContentPane().add(scrollPane);
scrollPane.setAlignmentY(Component.TOP_ALIGNMENT);
scrollPane.setBorder(new LineBorder(new Color(0, 0, 0)));
frame.pack();
}
public void fillTableShort() {
tableModel.setRowCount(0);
tableModel.addRow(new Object[]{"short", "short"});
}
public void fillTableLong() {
tableModel.setRowCount(0);
tableModel.addRow(new Object[]{"looooooooooooooooooooong", "looooooooooooooooooooong"});
}
public void updateTableWidths() {
int col_1_width = 0;
for (int row = 0; row < table.getRowCount(); row++) {
TableCellRenderer renderer = table.getCellRenderer(row, 0);
Component comp = table.prepareRenderer(renderer, row, 0);
col_1_width = Math.max (comp.getPreferredSize().width, col_1_width);
}
col_1_width += table.getIntercellSpacing().width;
col_1_width += 10;
table.getColumn("Col_1").setMinWidth(col_1_width);
table.getColumn("Col_1").setMaxWidth(col_1_width);
System.out.println("col_1_width was set to " + col_1_width);
int col_2_width = 0;
for (int row = 0; row < table.getRowCount(); row++) {
TableCellRenderer renderer = table.getCellRenderer(row, 1);
Component comp = table.prepareRenderer(renderer, row, 1);
col_2_width = Math.max (comp.getPreferredSize().width, col_2_width);
}
col_2_width += table.getIntercellSpacing().width;
int tableWidth = col_2_width + col_1_width;
if (scrollPane.getVerticalScrollBar().isVisible()) {
tableWidth += scrollPane.getVerticalScrollBar().getWidth();
}
if (tableWidth > scrollPane.getWidth()) {
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
System.out.println("Auto resize was set to AUTO_RESIZE_OFF");
} else {
table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
col_2_width = scrollPane.getWidth() + col_1_width;
System.out.println("Auto resize was set to AUTO_RESIZE_LAST COLUMN");
}
table.getColumn("Col_2").setPreferredWidth(col_2_width);
table.getColumn("Col_2").setMinWidth(col_2_width);
System.out.println("col_2_width was set to " + col_2_width + "\n");
}
}
Here is the sequence of steps you can follow to reproduce the behavior:
1) Launch the app
2) Click button "Fill table short"
3) Click button "Update table widths" (in this case only one click is needed)
4) Click button "Fill table long"
5) Click button "Update table widths" (first click)
6) Click button "Update table widths" (second click)
After the second click, it displays properly. I have some debug info that prints to the console, and for both the first and second click, the method seems to be doing exactly the same thing:
col_1_width was set to 152
Auto resize was set to AUTO_RESIZE_OFF
col_2_width was set to 142
So it's not like there is some obvious difference happening the second time around as a result of something that was changed during the first run. And as I mentioned earlier, this only happens when table columns have grown, rather than shrunk.
I am really stumped here. Any ideas?
You probably need to specify the preferred size for the first column as well to work properly with the scroll. Try adding this line:
table.getColumn("Col_1").setPreferredWidth(col_1_width);
in the the block that handles Col_1 calculations in updateTableWidths method. It solved the issue in the posted SSCCE.
EDIT:
There is a dependency on maxWidth and minWidth in setPreferredWidth. Below is the code for setPreferredWidth. So the order may matter.
public void setPreferredWidth(int preferredWidth) {
int old = this.preferredWidth;
this.preferredWidth = Math.min(Math.max(preferredWidth, minWidth), maxWidth);
firePropertyChange("preferredWidth", old, this.preferredWidth);
}
Also exploring setMaxWidth indicates that it may set preferred width as well:
public void setMaxWidth(int maxWidth) {
int old = this.maxWidth;
this.maxWidth = Math.max(minWidth, maxWidth);
if (width > this.maxWidth) {
setWidth(this.maxWidth);
}
if (preferredWidth > this.maxWidth) {
setPreferredWidth(this.maxWidth);
}
firePropertyChange("maxWidth", old, this.maxWidth);
}
Try adding TableColumn#setWidth and/or TableColumn#setPreferredWidth
ie
table.getColumn("Col_1").setWidth(col_1_width);
table.getColumn("Col_1").setPreferredWidth(col_1_width);
//...///
table.getColumn("Col_2").setWidth(col_2_width);
table.getColumn("Col_2").setPreferredWidth(col_2_width);

How do I make JScrollPane scroll to follow input focus?

I have a Swing app with a large panel which is wrapped in a JScrollPane. Users normally move between the panel's subcomponents by tabbing, so when they tab to something out view, I want the scroll pane to autoscroll so the component with input focus is always visible.
I've tried using KeyboardFocusManager to listen for input focus changes, and then calling scrollRectToVisible.
Here's an SSCCE displaying my current strategy (just copy/paste and run!):
import java.awt.KeyboardFocusManager;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
public class FollowFocus {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final int ROWS = 100;
final JPanel content = new JPanel();
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
content.add(new JLabel(
"Thanks for helping out. Use tab to move around."));
for (int i = 0; i < ROWS; i++) {
JTextField field = new JTextField("" + i);
field.setName("field#" + i);
content.add(field);
}
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.addPropertyChangeListener("focusOwner",
new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (!(evt.getNewValue() instanceof JComponent)) {
return;
}
JComponent focused = (JComponent) evt.getNewValue();
if (content.isAncestorOf(focused)) {
System.out.println("Scrolling to " + focused.getName());
focused.scrollRectToVisible(focused.getBounds());
}
}
});
JFrame window = new JFrame("Follow focus");
window.setContentPane(new JScrollPane(content));
window.setSize(200, 200);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
});
}
}
If you run this example, you'll notice it doesn't work very well. It does get the focus change notifications, but the call to scrollRectToVisible doesn't appear to have any effect. In my app (which is too complex to show here), scrollRectToVisible works about half the time when I tab into something outside of the viewport.
Is there an established way to solve this problem? If it makes any difference, the Swing app is built on Netbeans RCP (and most of our customers run Windows).
My comment to the other answer:
scrollRectToVisible on the component itself is the whole point of that
method ;-) It's passed up the hierarchy until a parent doing the
scroll is found
... except when the component itself handles it - as JTextField does: it's implemented to scroll horizontally to make the caret visible. The way out is to call the method on the field's parent.
Edit
just for clarity, the replaced line is
content.scrollRectToVisible(focused.getBounds());
you have to take Rectangle from JPanel and JViewPort too, then compare, for example
notice (against down-voting) for final and nice output required some work for positions in the JViewPort
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
//http://stackoverflow.com/questions/8245328/how-do-i-make-jscrollpane-scroll-to-follow-input-focus
public class FollowFocus {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final int ROWS = 100;
final JPanel content = new JPanel();
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
content.add(new JLabel(
"Thanks for helping out. Use tab to move around."));
for (int i = 0; i < ROWS; i++) {
JTextField field = new JTextField("" + i);
field.setName("field#" + i);
content.add(field);
}
final JScrollPane scroll = new JScrollPane(content);
KeyboardFocusManager.getCurrentKeyboardFocusManager().
addPropertyChangeListener("focusOwner", new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (!(evt.getNewValue() instanceof JComponent)) {
return;
}
JViewport viewport = (JViewport) content.getParent();
JComponent focused = (JComponent) evt.getNewValue();
if (content.isAncestorOf(focused)) {
System.out.println("Scrolling to " + focused.getName());
Rectangle rect = focused.getBounds();
Rectangle r2 = viewport.getVisibleRect();
content.scrollRectToVisible(new Rectangle(rect.x, rect.y, (int) r2.getWidth(), (int) r2.getHeight()));
}
}
});
JFrame window = new JFrame("Follow focus");
window.setContentPane(new JScrollPane(content));
window.setSize(200, 200);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
});
}
}
Here my short summary.
Add this to your Tools class:
public static void addOnEnter(Component c, Consumer<FocusEvent> onEnter) {
FocusListener fl = new FocusListener() {
#Override
public void focusGained(FocusEvent e) {
onEnter.accept(e);
}
#Override
public void focusLost(FocusEvent e) { }
};
c.addFocusListener(fl);
}
public static void scrollToFocus(FocusEvent e) {
((JComponent) e.getComponent().getParent()).scrollRectToVisible(
e.getComponent().getBounds());
}
and use it like this:
Tools.addOnEnter(component, Tools::scrollToFocus);
component can be JTextField, JButton, ...
One major issue in your code is:
focused.scrollRectToVisible(focused.getBounds());
You are calling scrollRectToVisible on the component itself! Presumably a typo.
Make your JScrollPane a final variable and call
scrollPane.getViewport().scrollRectToVisible(focused.getBounds());
Here jtextbox is the component you want to focus and jscrollpane is your scrollpane:
jScrollpane.getVerticalScrollBar().setValue(jtextbox.getLocation().x);

Categories

Resources