It is my first post in StackOverflow.
I did not find a SSCCE on a JTable whose row cells contain JRadioButton's - in several (not all) columns, contiguous columns and not always the same columns for each row - and whose ButtonGroup's correspond with the rows.
So I tried to elaborate it after revisiting the topics of Renderer and Editor in 'How to use Tables - http://docs.oracle.com/javase/tutorial/uiswing/components/table.html
and also after studying the following discussions :
- Adding (a column of) JRadioButton into JTable Adding jRadioButton into jTable (Guillaume Polet)
- How to add JRadioButton to group in JTable How to add JRadioButton to group in JTable
I make use of ...
- Swing
- TableCellEditor and TableCellRenderer
- Beans.PropertyChangeSupport, PropertyChangeListener
My Java code works ... but with an error of behaviour and incompletely.
It is even though going on a functionnality that is - I think - of great interest to much Java programmers.
A personal style rule : All syntax words of Java, Swing, ... : English, as in the libraries. All other items, particular to my application : Another language; here French.
Finally, here is the "One copy and paste" class file [TestTable2] containing 4 sub-classes [RendeurEditeur_CelluleBoutonRadio], [MonModeleTable], [GestionnObjetsDUneRangee] and [MonObjet]. It works. 420 lines for this SSCCE to copy and paste :
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractCellEditor;
import javax.swing.JFrame;
import javax.swing.JRadioButton;
// import javax.swing.ButtonGroup;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException; // main()
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
public class TestTable2
{
private JFrame cadre;
private JTable table;
protected void initUI()
{
table = new JTable(new MonModeleTable());
short nbreColo = (short) table.getColumnCount();
table.setRowHeight(25);
TableColumn col;
// With Java, you can specify cell renderers and editors either by column or by data type.
for (int i = 0 ; i < nbreColo ; i++)
{
col = table.getColumnModel().getColumn(i);
col.setCellEditor (new RendeurEditeur_CelluleBoutonRadio());
col.setCellRenderer(new RendeurEditeur_CelluleBoutonRadio());
}
cadre = new JFrame();
cadre.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
cadre.add(new JScrollPane(table), BorderLayout.CENTER);
cadre.pack();
cadre.setVisible(true);
}
/**
* #param args the command line arguments
* #throws ClassNotFoundException
* #throws InstantiationException
* #throws IllegalAccessException
* #throws UnsupportedLookAndFeelException
*/
public static void main(String[] args) throws ClassNotFoundException, InstantiationException,
IllegalAccessException, UnsupportedLookAndFeelException
{
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
SwingUtilities.invokeLater(new Runnable()
{
#Override public void run() // Implements method from java.lang.Runnable
{
new TestTable2().initUI();
}
} );
}
private class RendeurEditeur_CelluleBoutonRadio extends AbstractCellEditor
implements TableCellRenderer, TableCellEditor, ActionListener
{
private final JRadioButton radioButton;
public RendeurEditeur_CelluleBoutonRadio()
{
this.radioButton = new JRadioButton();
radioButton.addActionListener(this);
radioButton.setOpaque(false);
}
#Override public Component getTableCellRendererComponent(JTable table, Object valeur,
boolean estSelectionne, boolean hasFocus, int row, int column)
{
radioButton.setSelected(Boolean.TRUE.equals(valeur));
return radioButton;
}
#Override public Component getTableCellEditorComponent(JTable table, Object valeur,
boolean estSelectionne, int row, int column)
{
radioButton.setSelected(Boolean.TRUE.equals(valeur));
return radioButton;
}
#Override public void actionPerformed(ActionEvent e)
{
stopCellEditing();
}
#Override public Object getCellEditorValue()
{
return radioButton.isSelected();
}
}
/**
*
*/
class MonModeleTable extends AbstractTableModel implements PropertyChangeListener
{
// Manages the rows of radioButtons and their groupings.
private GestionnObjetsDUneRangee gestionn_DonneesDUneRang;
private final List<GestionnObjetsDUneRangee> gestionn_DonneesTteLatable = new ArrayList<>();
public MonModeleTable()
{
super(); // AbstractTableModel()
MonObjet objet;
for (short idxRang = 0 ; idxRang < 5 ; idxRang++)
{
gestionn_DonneesDUneRang = new GestionnObjetsDUneRangee();
/* To record 'this' (= MonModeleTable) as listener of the messages sent
* by the object ...
* - to which a property (gestionn_DonneesDUneRang) of 'MonModeleTable'-itself
* refers to, and
* - pertaining to a class (GestionnObjetsDUneRangee) containing a property (suppoChangePropri)
* refering to an instanciation of PropertyChangeSupport. This later has as argument
* the object of class 'GestionnObjetsDUneRangee'. */
// Leaking 'this' in constructor : Passing suspicious parameter in the constructor.
gestionn_DonneesDUneRang.getSuppoChangePropri().addPropertyChangeListener(this);
for (short idxColo = 0 ; idxColo < 4 ; idxColo++)
{
objet = new MonObjet(idxRang, idxColo);
gestionn_DonneesDUneRang.ajoutObjetÀRangee(objet);
if (idxColo == 0)
objet.setSelectionne(true);
/* To record 'MonModeleTable' as listener of the messages sent by each
* object of the list 'gestionn_DonneesDUneRang'.*/
objet.getSupportChangtPropri().addPropertyChangeListener(this);
}
gestionn_DonneesTteLatable.add(gestionn_DonneesDUneRang);
}
}
// Rem.: Identity of the object's row and column (JRadioButton) available in 'evt.getSource();'.
#Override public void propertyChange(PropertyChangeEvent evt)
{
System.out.print(evt.getPropertyName() + "\t");
Object objet2 = evt.getSource();
if (objet2 == gestionn_DonneesDUneRang)
{
if (evt.getPropertyName().equals("objecten")) // MonModeleTable
{
((MonObjet) evt.getNewValue()).getSupportChangtPropri().addPropertyChangeListener(this);
}
fireTableDataChanged();
System.out.println("");
} else if (objet2 instanceof MonObjet)
{ // When another button of the row has been activated ("geselecteerd")
short[] coordBP = ((MonObjet) objet2).getCoordBP();
fireTableRowsUpdated(coordBP[0], coordBP[0]); // [0] : 2 x row
System.out.println("");
}
}
#Override public int getColumnCount()
{
return gestionn_DonneesDUneRang.getObjetsDUneRangee().size();
}
#Override public int getRowCount()
{
return gestionn_DonneesTteLatable.size();
}
// Exporting non-public type through public API
public MonObjet getValueAt(int col)
{
return gestionn_DonneesDUneRang.getObjetsDUneRangee().get(col);
}
// Implements method from javax.swing.table.TableModel
#Override public Object getValueAt(int idxRang, int idxColo)
{
return getValueAt(idxColo).isSelectionne();
}
// This method can also import idxRang2 and idxColo2 in this class.
// Overrides method from javax.swing.table.AbstractTableModel
#Override public void setValueAt(Object bpActionne, int idxRang2, int idxColo2)
{
getValueAt(idxColo2).setSelectionne(Boolean.TRUE.equals(bpActionne));
}
// Overrides method from javax.swing.table.AbstractTableModel
#Override public boolean isCellEditable(int idxRang3, int idxColo3)
{
return true; // idxRang3 == 1;
}
/** There must be a specification of a renderer for the cells for the table not to invoke the
* table model's 'getColumnClass' method (which gets the data type of the column's cells). */
// Overrides method from javax.swing.table.AbstractTableModel
#Override public Class<?> getColumnClass(int rang)
{
return Object.class;
}
Class<?> getClasseDeCellule(int idxRang, int idxColo)
{
// To be modified.
return Object.class;
}
// Overrides method from javax.swing.table.AbstractTableModel
#Override public String getColumnName(int colo)
{ // Column titles
return "Col " + (colo + 1);
}
}
/** Bean.
*/
class GestionnObjetsDUneRangee
{
private List<MonObjet> objetsDUneRangee = new ArrayList<>();
// private final ButtonGroup groupeHorizBoutRad = new ButtonGroup();
private final PropertyChangeSupport suppoChangePropri = new PropertyChangeSupport(this);
// GestionnObjetsDUneRangee() { }
public void ajoutObjetÀRangee(MonObjet objet)
{
objetsDUneRangee.add(objet);
setGestionn_ObjetsDUneRangee2(objet);
suppoChangePropri.firePropertyChange("objecten", null, objet);
}
public List<MonObjet> getObjetsDUneRangee()
{
return objetsDUneRangee;
}
// ?
public void setObjetsDUneRangee2(List<MonObjet> objetsDUneRangee2)
{
List<MonObjet> ancienObjetsDUneRangee = objetsDUneRangee;
objetsDUneRangee = objetsDUneRangee2;
suppoChangePropri.firePropertyChange("objectenVanEenRij",
ancienObjetsDUneRangee, objetsDUneRangee);
}
// Method called from 'ajoutObjetÀRangee(...)' in this class.
void setGestionn_ObjetsDUneRangee2(MonObjet monObjet)
{
monObjet.gestionn_ObjetsDUneRangee = this;
monObjet.getSupportChangtPropri().firePropertyChange("dataVanEenRij", null, monObjet.gestionn_ObjetsDUneRangee);
}
// Puts all the JRadioButton's of a row in their right states (Only one selected)
public void miseàjourTousBP_Rangee(MonObjet myObject) // setSelectionne()
{
for (MonObjet obj : objetsDUneRangee)
{
obj.setSelectionne(myObject == obj);
}
}
GestionnObjetsDUneRangee getGestionn_ObjetsDUneRangee2(MonObjet monObjet)
{
return monObjet.gestionn_ObjetsDUneRangee;
}
PropertyChangeSupport getSuppoChangePropri()
{
return suppoChangePropri;
}
/*
public void
addPropertyChangeListener(PropertyChangeListener listener)
{
suppoChangePropri.addPropertyChangeListener(listener);
}
public void
removePropertyChangeListener(PropertyChangeListener listener)
{
suppoChangePropri.removePropertyChangeListener(listener);
}
*/
}
/**
* Bean
*/
class MonObjet
{
private boolean selectionne;
GestionnObjetsDUneRangee gestionn_ObjetsDUneRangee;
private final PropertyChangeSupport supportChangtPropri;
short[] coordBP = new short[2]; // idxRang and idxColo of the JRadioButton object
MonObjet(short idxRang, short idxColo)
{
supportChangtPropri = new PropertyChangeSupport(this);
coordBP[0] = idxRang;
coordBP[1] = idxColo;
}
PropertyChangeSupport getSupportChangtPropri()
{
return supportChangtPropri;
}
boolean isSelectionne()
{
return selectionne;
}
// Called by 'positionnerSelectionne(objet)' in 'gestionn_DObjetsDUneRang').
void setSelectionne(boolean selectionne2)
{
if (this.selectionne != selectionne2)
{ // Passes here only if a change of the state occured.
this.selectionne = selectionne2;
if (selectionne)
{ // Passes here only when the radiobutton just was activated.
// 'selectionne' of the object is already 'true'.
gestionn_ObjetsDUneRangee.miseàjourTousBP_Rangee(this);
}
supportChangtPropri.firePropertyChange("geselecteerd", !selectionne, selectionne);
}
}
short[] getCoordBP()
{
return coordBP;
}
public void setGestionn_ObjetsDUneRangee(GestionnObjetsDUneRangee gestionn_ObjetsDUneRangee2)
{
GestionnObjetsDUneRangee ancienGestionn_ObjetsDUneRangee = gestionn_ObjetsDUneRangee;
gestionn_ObjetsDUneRangee = gestionn_ObjetsDUneRangee2;
supportChangtPropri.firePropertyChange("ManagerObjectenVanEenRij",
ancienGestionn_ObjetsDUneRangee, gestionn_ObjetsDUneRangee2);
}
/*
public void addPropertyChangeListener(PropertyChangeListener listener)
{
supportChangtPropri.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener)
{
supportChangtPropri.removePropertyChangeListener(listener);
}
*/
}
}
}
The issue : The RadioButton's in the rows under the one where one was just activated, in the column of the activated one, also change. Observe by running my code, please.
In 'MonModèleTable', in 'void propertyChange(PropertyChangeEvent evt)', the 'gestionn_ObjetsDUneRangée' of 'objet2' always refers to the one of the last row. How to get there the right 'gestionn_ObjetsDUneRangée', I mean the one which corresponds to the row of the JRadioButton that was just activated ?
Is there an expert agreeing that this case is of general interest and who can help us (Guillaume Polet ?) ?
Many thanks in advance.
Note: I'm using JRE rev. 1.8.0_73 and NetBeans IDE rev. 8.0.2.
Related
As simple as Renderers and Editors sound and despite the dozen or so SO bookmarks I return to regarding similar issues I’m missing something elementary. I want to drag any old text file into a 2-column JTable, have the first column display the filename and the second contain a JComboBox whose options depend on the contents of the dragged file. (In the code below I just fake a few entries.)
This all works fine until I make a selection from a combo box - the selection doesn’t display - just a combo box, populated correctly but no selection made. I know it must have something to do with my misuse of renderers/editors but after at least two weeks of flailing I’m seeking professional help. And if you think I’ve totally missed the boat on how renderers and editors are written, well, I’m glad you didn’t see my earlier attempts.
Hopefully this code qualifies as an SSCCE - sincere apologies if I’ve included something I shouldn’t have. I’ve retained the DnD stuff just in case it has some significance.
For what it’s worth, I use a static list of ComboBoxModels (one per row) since each JComboBox contains different options, and likewise TableCellEditors (although I don’t know if that’s the right way to go about it).
To run this just drag any file into the table that appears and then make a selection from the JComboBox in the right column and watch it ignore you. Thanks very much, even if you have some advice without taking the trouble of running this.
Java 1.7/OS X 10.9.5/Eclipse Mars.2
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.AbstractCellEditor;
import javax.swing.DefaultCellEditor;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.MutableComboBoxModel;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.event.ListDataListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
public class Main extends JFrame {
static List<AComboBoxModel> priceComboModels = new ArrayList<AComboBoxModel>();
static List<DefaultCellEditor> editors = new ArrayList<DefaultCellEditor>();
public Main() {
setLayout(new BorderLayout());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setPreferredSize(new Dimension(500, 400));
JPanel panel = new JPanel(new BorderLayout());
JTable table = new JTable(0, 2) {
public TableCellEditor getCellEditor(int rinx, int cinx) {
if (cinx == 0) {
return super.getCellEditor(rinx, cinx);
}
return editors.get(rinx);
}
};
table.setPreferredScrollableViewportSize(new Dimension(360, 80));
table.setTransferHandler(new ATransferHandler());
table.setModel(new ATableModel());
TableColumnModel tcm = table.getColumnModel();
tcm.getColumn(0).setHeaderValue("File Name");
tcm.getColumn(1).setHeaderValue("Selection");
TableColumn column = tcm.getColumn(1);
column.setCellRenderer(new ACellRenderer());
column.setCellEditor(new ACellEditor());
table.setDragEnabled(true);
table.setFillsViewportHeight(true);
JScrollPane sp = new JScrollPane(
table,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
);
panel.add(sp, BorderLayout.CENTER);
panel.setPreferredSize(new Dimension(200, 300));
add(panel, BorderLayout.CENTER);
pack();
}
public static int addComboModel(AComboBoxModel model) {
priceComboModels.add(model);
return priceComboModels.size() - 1;
}
public static AComboBoxModel getComboModelAt(int inx) {
return priceComboModels.get(inx);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Main().setVisible(true);
}
});
}
}
class ATableModel extends DefaultTableModel {
List<ARecord> data = new ArrayList<ARecord>();
public void addRow(ARecord row) {
data.add(row);
fireTableRowsInserted(data.size() - 1, data.size() - 1);
}
#Override
public int getRowCount() {
return data == null ? 0 : data.size();
}
#Override
public int getColumnCount() {
return 2;
}
public void setValueAt(Object value, int rinx, int cinx) {
ARecord row = data.get(rinx);
switch (cinx) {
case 0:
row.setFilename((String) value);
break;
case 1:
row.setCbox((JComboBox) value);
break;
}
}
#Override
public Object getValueAt(int rinx, int cinx) {
Object returnValue = null;
ARecord row = data.get(rinx);
switch (cinx) {
case 0:
returnValue = row.getFilename();
break;
case 1:
returnValue = row.getCbox();
break;
}
return returnValue;
}
// I assume this is unnecessary since column 1 defaults to text
// and column 2 is handled by ACellRenderer. I think.
// #Override
// public Class getColumnClass(int cinx) {
// return cinx == 0 ? String.class : JComboBox.class;
// }
}
//////////////////////////////////////////////////////////////////////////////////
// This class handles the drag and drop.
class ATransferHandler extends TransferHandler {
int getSourceActions(JList<String> lst) {
return TransferHandler.COPY;
}
Transferable createTransferable(JList<String> list) {
return null;
}
void exportDone(JList<String> lst, Transferable data, int action) {
}
public boolean canImport(TransferHandler.TransferSupport info) {
return true;
}
//////////////////////////////////////////////////////////////////////////
// This is the method of interest where the dropped text file is handled.
//////////////////////////////////////////////////////////////////////////
public boolean importData(TransferHandler.TransferSupport info) {
if (! info.isDrop()) return false;
JTable table = (JTable)info.getComponent();
Transferable tr = info.getTransferable();
List<File> files = null;
try {
files = (List<File>)tr.getTransferData(DataFlavor.javaFileListFlavor);
} catch(UnsupportedFlavorException | IOException e) {
}
ATableModel tm = (ATableModel)table.getModel();
String[] options;
// For each dropped text file...
for (File fl : files) {
String fname = fl.getName();
// Just fill the JComboBox with some unique options for now
// (in practice this comes from the dropped text file contents).
String dummyText = fname.substring(0, 5);
options = new String[] { dummyText + "_A", dummyText + "_B", dummyText + "_C" };
// Create a ComboBoxModel for this JComboBox containing the selection options.
AComboBoxModel cboModel = new AComboBoxModel(options);
// Create the combo box itself.
JComboBox<String> cbox = new JComboBox<String>();
// Assign the model to the box.
cbox.setModel(cboModel);
// Create and add to the editor list the table cell editor.
Main.editors.add(new DefaultCellEditor(cbox));
// Also add the ComboBoxModel to the model list.
Main.addComboModel(cboModel);
// Add the row to the model data.
tm.addRow(new ARecord(fname, cbox));
}
return true;
}
}
///////////////////////////////////////////////////////////////////////////////////////////
class ARecord {
String filename;
JComboBox cbox;
// Just a bean to describe a table row (a filename and a JComboBox).
public ARecord(String filename, JComboBox cbox) {
super();
this.filename = filename;
this.cbox = cbox;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public JComboBox getCbox() {
return cbox;
}
public void setCbox(JComboBox cbox) {
this.cbox = cbox;
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// This is the model for the JComboBoxes. A different model is instantiated
// for each row since each one has different contents.
class AComboBoxModel implements MutableComboBoxModel {
List<String> items = new ArrayList<String>();
public AComboBoxModel(String[] items) {
this.items = Arrays.asList(items);
}
#Override
public int getSize() {
return items.size();
}
#Override
public Object getElementAt(int index) {
return items.get(index);
}
#Override
public void addListDataListener(ListDataListener l) {
}
#Override
public void removeListDataListener(ListDataListener l) {
}
#Override
public void setSelectedItem(Object anItem) {
}
#Override
public Object getSelectedItem() {
return null;
}
#Override
public void addElement(Object item) {
}
#Override
public void removeElement(Object obj) {
}
#Override
public void insertElementAt(Object item, int index) {
}
#Override
public void removeElementAt(int index) {
}
}
//////////////////////////////////////////////////////////////////////////////////////
// I won't pretend that I'm confident as to how this should work. My guess is that
// I should just retrieve the appropriate ComboBoxModel, assign it and return.
class ACellRenderer extends JComboBox implements TableCellRenderer {
#Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
int rinx, int cinx) {
setModel(Main.getComboModelAt(rinx));
return this;
}
}
/////////////////////////////////////////////////////////////////////////////////////////
class ACellEditor extends AbstractCellEditor implements TableCellEditor {
static JComboBox box = null;
// This is where I think I'm actually lost. I don't understand the significance of
// returning a JComboBox when one was already created when the text file was
// dropped. Is it correct to just assign the appropriate ComboBoxModel to a JComboBox
// and return it here?
public Component getTableCellEditorComponent(JTable table,
Object value,
boolean isSelected,
int rinx,
int cinx) {
box = (JComboBox)(table.getModel().getValueAt(rinx, cinx));
box.setModel(Main.getComboModelAt(rinx));
return box;
}
#Override
public Object getCellEditorValue() {
return box;
}
}
make a selection from the JComboBox in the right column and watch it ignore you
Something is wrong with your custom editor and I'm not sure what. You have a big problem in that you are trying to use a JComboBox as the data of the editor. This is completely wrong.
But the good new is that there is no need for you to use a custom renderer or a custom editor.
You should NOT be storing a JComboBox in the TableModel. You simply store the String of the selected item from the combo box. (This will be done for you automatically by the default combo box editor).
There is no need for you to create a new editor for every file that is dragged to the table.
the second contain a JComboBox whose options depend on the contents of the dragged file
So the only part of the table that you need to customize is the getCellEditor(...) method.
I would guess you would have a different editor for a given file extension.
So the basic code might be something like:
int modelColumn = convertColumnIndexToModel( column );
if (modelColumn == 1)
{
String file = getModel.getValueAt(row, 0);
if (file.endsWith(".txt"))
return txtEditor;
else if (file.endsWith(".html"))
return htmlEditor;
}
return super.getCellEditor(row, column);
Check out:
How to add unique JComboBoxes to a column in a JTable (Java) for a working example. The logic in that posting does have a separate editor by row for demonstration purposes only. The example demonstrates that the code works with the default renderers and editors. All you need to do is provide the items for each combo box editor.
In your case the editor will be based on the file type so the logic needs to test the data in the first column.
Note: the nested if/else statement is not a good solution. You might want to use a Hashmap of filetype/editor. Then the getCellEditor(...) method would just be a Hashmap lookup once you extract the filetype for the File.
So your dragging code should have nothing to do with the editors of the table. You need to know before hand which file types you want to support and define the valid items for each of those file types.
Also, your TableModel should NOT extend DefaultTableModel. You are providing your own data storage and implementing all the methods so you should just be extending the AbstractTableModel.
In the following code, I create a jtable with a custom cell editor for the first column and then add undo capabilities to the table. When you run the program, the program allows you to change the values in the first column (test by appending a "d" and then an "e" to the "abc" already there). Now enter control-z (undo) and enter control-z again. It works as expected. But now enter control-z (undo) again. This time the "abc" is erased. It looks like the swing system is setting the initial value of the column and creating an undo event for that action which the user can then undo. My question - how do I write my code so that the user only can undo the actions the user makes?
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.DefaultCellEditor;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JRootPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEdit;
public class UndoExample extends JFrame {
private static final long serialVersionUID = 1L;;
static Boolean objEnableUndoRedoActions = true;
UndoExample rootFrame;
public UndoExample() {
// This procedure starts the whole thing off.
//Create table
final String[] tableColumns = {"Column 1", "Column 2"};
JTable tabUndoExample = new JTable(
new DefaultTableModel(null, tableColumns) {
private static final long serialVersionUID = 1L;
});
final DefaultTableModel tabUndoExampleModel = (DefaultTableModel) tabUndoExample
.getModel();
tabUndoExampleModel.addRow(new Object[]{"abc", true});
tabUndoExampleModel.addRow(new Object[]{"zyw", false});
// Create the undo/redo manager
UndoManager objUndoManager = new UndoManager();
// Create a cell editor
JTextField tfTabField = new JTextField();
TableCellEditor objEditor = new DefaultCellEditor(tfTabField);
// Make the cell editor the default editor for this table's first column
tabUndoExample.getColumnModel().getColumn(0)
.setCellEditor(objEditor);
// Create the undo action on the field's document for the column
tfTabField.getDocument().addUndoableEditListener(
new uelUndoRedoTableCellField(objUndoManager, tabUndoExample));
// Allow undo and redo to be entered by the user
UndoRedoSetKeys(this, "Example", objUndoManager);
tabUndoExample.setInheritsPopupMenu(true);
//Add the table to the frame and show the frame
this.add(tabUndoExample);
this.pack();
setLocationRelativeTo(null);
}
public static void main(final String[] args) {
// Launches the application. This is required syntax.
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
try {
final UndoExample rootFrame = new UndoExample();
rootFrame.setVisible(true);
} catch (final Exception e) {
}
}
});
}
#SuppressWarnings("serial")
static class aueUndoRedoTableCellField extends AbstractUndoableEdit {
// Wrap the text edit action item as we need to add the table
// row and column information. This code is invoked when the
// code sees an undo event created and then later when the
// user requests an undo/redo.
JTable objTable = null;
UndoableEdit objUndoableEdit;
int objCol = -1;
int objRow = -1;
public aueUndoRedoTableCellField(UndoableEdit undoableEdit,
JTable table, int row, int col) {
super();
objUndoableEdit = undoableEdit;
objTable = table;
objCol = col;
objRow = row;
}
public void redo() throws CannotRedoException {
// When the user enters redo (or undo), this code sets
// that we are doing an redo (or undo), sets the cursor
// to the right location, and then does the undo (or redo)
// to the table cell.
UndoRedoManagerSetEnabled(false);
super.redo();
#SuppressWarnings("unused")
boolean success = objTable.editCellAt(objRow, objCol);
objTable.changeSelection(objRow, objCol, false, false);
objUndoableEdit.redo();
UndoRedoManagerSetEnabled(true);
}
public void undo() throws CannotUndoException {
super.undo();
UndoRedoManagerSetEnabled(false);
#SuppressWarnings("unused")
boolean success = objTable.editCellAt(objRow, objCol);
objTable.changeSelection(objRow, objCol, false, false);
objUndoableEdit.undo();
UndoRedoManagerSetEnabled(true);
}
}
static class aUndoRedo extends AbstractAction {
// This code is bound to the undo/redo keystrokes and tells
// Java what commands to run when the keys are later entered
// by the user.
private static final long serialVersionUID = 1L;
Boolean objUndo = true;
UndoManager objUndoManager = null;
final String objLocation;
public aUndoRedo(Boolean Undo, UndoManager undoManager, String location) {
super();
objUndo = Undo;
objUndoManager = undoManager;
objLocation = location;
}
#Override
public void actionPerformed(ActionEvent ae) {
try {
// See if operation allowed
if (!objUndoManager.canUndo() && objUndo
|| !objUndoManager.canRedo() && !objUndo)
return;
UndoRedoManagerSetEnabled(false);
if (objUndo) {
objUndoManager.undo();
} else {
objUndoManager.redo();
}
UndoRedoManagerSetEnabled(true);
// Catch errors and let user know
} catch (Exception e) {
UndoRedoManagerSetEnabled(true);
}
}
}
static class uelUndoRedoTableCellField implements UndoableEditListener {
// This action is called when the user changes the table's
// text cell. It saves the change for later undo/redo.
private UndoManager objUndoManager = null;
private JTable objTable = null;
public uelUndoRedoTableCellField(UndoManager undoManager,
JTable table) {
objUndoManager = undoManager;
objTable = table;
}
#Override
public void undoableEditHappened(UndoableEditEvent e) {
// Remember the edit but only if the code isn't doing
// an undo or redo currently.
if (UndoRedoManagerIsEnabled()) {
objUndoManager.addEdit(new aueUndoRedoTableCellField(e
.getEdit(), objTable, objTable.getSelectedRow(),
objTable.getSelectedColumn()));
}
}
}
static public Boolean UndoRedoManagerIsEnabled() {
// See if we are currently doing an undo/redo.
// Return true if so.
return objEnableUndoRedoActions;
}
static public void UndoRedoManagerSetEnabled(Boolean state) {
// Set the state of whether we are in undo/redo code.
objEnableUndoRedoActions = state;
}
static void UndoRedoSetKeys(JFrame frame, final String location, UndoManager undoManager) {
// Allow undo and redo to be called via these keystrokes for this dialog
final String cntl_y = "CNTL_Y";
final KeyStroke ksCntlY = KeyStroke.getKeyStroke("control Y");
final String cntl_z = "CNTL_Z";
final KeyStroke ksCntlZ = KeyStroke.getKeyStroke("control Z");
JRootPane root = frame.getRootPane();
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(ksCntlZ, cntl_z);
root.getActionMap().put(
cntl_z,
new aUndoRedo(true, undoManager, location));
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(ksCntlY, cntl_y);
root.getActionMap().put(
cntl_y,
new aUndoRedo(false, undoManager, location));
}
}
When you press a key, a series of things occur. The JTable, process the key stroke, it checks to see if the cell is editable (as the TableModel), it then asks the editor for the currently selected cell if the event should edit the cell (CellEditor#isCellEditable(EventObject)).
If this method returns true, the editor is prepared, the value from the TableModel is applied to the editor (ie setText is called), and the editor is added to the JTable, finally, the event which triggered the edit mode is re-dispatched to the editor, in your case the Ctrl+Z, which then triggers and undo event, returning the editor it's initial state (before setText was called).
You can try and use something like...
TableCellEditor objEditor = new DefaultCellEditor(tfTabField) {
#Override
public boolean isCellEditable(EventObject anEvent) {
boolean isEditable = super.isCellEditable(anEvent); //To change body of generated methods, choose Tools | Templates.
if (isEditable && anEvent instanceof KeyEvent) {
KeyEvent ke = (KeyEvent) anEvent;
if (ke.isControlDown() && ke.getKeyCode() == KeyEvent.VK_Z) {
isEditable = false;
}
}
return isEditable;
}
};
to prevent the JTable from been placed into edit when a specific key stroke occurs
Updated
So based on Andrew's answer from JTextArea setText() & UndoManager, I devised a "configurable" UndoableEditListener which can be set to ignore undoable actions, for example...
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
public class FixedField {
public static void main(String[] args) {
new FixedField();
}
public FixedField() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public static class UndoableEditHandler implements UndoableEditListener {
private static final int MASK
= Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
private UndoManager undoManager = new UndoManager();
private boolean canUndo = true;
public UndoableEditHandler(JTextField field) {
Document doc = field.getDocument();
doc.addUndoableEditListener(this);
field.getActionMap().put("Undo", new AbstractAction("Undo") {
#Override
public void actionPerformed(ActionEvent evt) {
try {
if (undoManager.canUndo()) {
undoManager.undo();
}
} catch (CannotUndoException e) {
System.out.println(e);
}
}
});
field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, MASK), "Undo");
field.getActionMap().put("Redo", new AbstractAction("Redo") {
#Override
public void actionPerformed(ActionEvent evt) {
try {
if (undoManager.canRedo()) {
undoManager.redo();
}
} catch (CannotRedoException e) {
System.out.println(e);
}
}
});
field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, MASK), "Redo");
}
#Override
public void undoableEditHappened(UndoableEditEvent e) {
if (canUndo()) {
undoManager.addEdit(e.getEdit());
}
}
public void setCanUndo(boolean canUndo) {
this.canUndo = canUndo;
}
public boolean canUndo() {
return canUndo;
}
}
public class TestPane extends JPanel {
public TestPane() {
JTextField field = new JTextField(10);
UndoableEditHandler handler = new UndoableEditHandler(field);
handler.setCanUndo(false);
field.setText("Help");
handler.setCanUndo(true);
add(field);
}
}
}
Now, you're going to have to devices your own TableCellEditor to support this, for example...
public static class MyCellEditor extends AbstractCellEditor implements TableCellEditor {
private JTextField editor;
private UndoableEditHandler undoableEditHandler;
public MyCellEditor(JTextField editor) {
this.editor = editor;
undoableEditHandler = new UndoableEditHandler(editor);
editor.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
fireEditingStopped();
}
});
}
#Override
public Object getCellEditorValue() {
return editor.getText();
}
#Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
undoableEditHandler.setCanUndo(false);
editor.setText(value == null ? null : value.toString());
undoableEditHandler.setCanUndo(true);
return editor;
}
}
I am trying to make a Checkbox change value on click in a JTable. Here is the code I use for that in the MouseListener
public void mouseClicked(MouseEvent e) {
Point mouse = e.getPoint();
int row = table.rowAtPoint(mouse);
int col = table.columnAtPoint(mouse);
if (col == 0) tableModel.setValueAt(new Boolean(!(Boolean) tableModel.getValueAt(row, col)), row, col);
}
The problem is, that when I sort the table, this happens
Here is an SSCCE
import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;
#SuppressWarnings("serial")
public class SSCCE extends JFrame {
JTable table;
public SSCCE() {
setSize(300, 200);
Object[][] data = { {false, "This is false"}, {true, "This is true"}};
table = new JTable(new CustomTableModel(data));
add(table);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setVisible(true);
}
private class CustomTableModel extends AbstractTableModel {
Object[][] data;
public CustomTableModel(Object[][] data) {
this.data = data;
}
public Class<?> getColumnClass(int columnIndex) {
return data[0][columnIndex].getClass();
}
public int getColumnCount() {
return data[0].length;
}
public int getRowCount() {
return data.length;
}
public Object getValueAt(int rowIndex, int columnIndex) {
return data[rowIndex][columnIndex];
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new SSCCE();
}
});
}
}
Is there a way around this? Or a better method (not ListListener) to detect clicks on cells?
There is no need to use a MouseListener. You just need to use a proper editor for the column and the table will handle it for you.
Read the section from the Swing tutorial on How to Use Tables for more information and working examples.
Basically you need to do two things:
Add Boolean data to the TableModel
Override the getColumnClass(...) method of the TableModel to return Boolean.class for that column and the table will choose the appropriate editor.
Above is the answer for your question, but for future information the MouseEvent is relative to the table, so you want to use table methods to access the data. That is you would use table.getValueAt(...) and table.setValueAt(...). These reference the data as it is currently displayed in the view of the table. That is the view could be sorted or the column could have been moved.
I want to show a JButton within a JTable. This is nothing special, and I found a lot of examples doing this. However, I always have issues with pressing the buttons via Keyboard (not via mouse). I expect that I can select a cell and push (also visually) the button by pressing SPACE (no mnemonics).
Two snippets work like a charm, except supporting keys:
http://tips4java.wordpress.com/2009/07/12/table-button-column/
The author claims that keys work. I believe that they did, but not on all my systems I checked. However, supported mnemonics work perfectly.
(posted here: Adding Jbutton to JTable)
http://www.java2s.com/Code/Java/Swing-Components/ButtonTableExample.htm
(posted here: Adding Jbutton to JTable)
In the example, it works perfectly! However, it doesn't work for my table. Just disable row selection (I have to use cell selection), and pressing the button via key doesn't work anymore:
table.setRowSelectionAllowed(false);
I tried hard figuring out what's going wrong or how to fix it, but I failed. My only achievement is to call the action behind the button, but the button is not pressed (I mean the visual behavior).
Some information added:
I used... (in many combinations)
Ubuntu 10.04, Windows 7, Windows 8
Java 7u21, JDK 1.6.0_33, OpenJDK Runtime Environment (IcedTea6 1.8.1) (6b18-1.8.1-0ubuntu1)
WindowsLookAndFeel, Metal (Cross Platform LAF), Nimbus
0% success!
TableTest.java
import java.awt.event.ActionEvent;
import java.util.LinkedList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.table.AbstractTableModel;
public class TableTest extends JFrame {
public TableTest() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JTable table = new JTable(new TestModel());
table.setRowSelectionAllowed(false);
table.getColumnModel().getColumn(1).setPreferredWidth(3);
table.getColumnModel().getColumn(2).setPreferredWidth(3);
this.add(new JScrollPane(table));
Action increase = new AbstractAction("+") {
#Override
public void actionPerformed(ActionEvent e) {
JTable table = (JTable) e.getSource();
int row = Integer.valueOf(e.getActionCommand());
TestModel model = (TestModel) table.getModel();
model.increment(row, 0);
}
};
ButtonColumn inc = new ButtonColumn(table, increase, 1);
Action decrease = new AbstractAction("-") {
#Override
public void actionPerformed(ActionEvent e) {
JTable table = (JTable) e.getSource();
int row = Integer.valueOf(e.getActionCommand());
TestModel model = (TestModel) table.getModel();
model.decrement(row, 0);
}
};
ButtonColumn dec = new ButtonColumn(table, decrease, 2);
pack();
}
public static void main(String[] args) {
new TableTest().setVisible(true);
}
}
class TestModel extends AbstractTableModel {
List<TestRecord> records = new LinkedList<TestRecord>();
private static class TestRecord {
private int val = 0;
}
public void increment(int row, int col) {
records.get(row).val++;
fireTableCellUpdated(row, 0);
}
public void decrement(int row, int col) {
records.get(row).val--;
fireTableCellUpdated(row, 0);
}
public TestModel() {
records.add(new TestRecord());
records.add(new TestRecord());
}
#Override
public Class<?> getColumnClass(int col) {
if (col == 0) {
return Integer.class;
} else {
return ButtonColumn.class;
}
}
#Override
public boolean isCellEditable(int row, int col) {
return true;
}
#Override
public int getColumnCount() {
return 3;
}
#Override
public int getRowCount() {
return records.size();
}
#Override
public Object getValueAt(int row, int col) {
if (col == 0) {
return records.get(row).val;
} else if (col == 1) {
return "+";
} else {
return "-";
}
}
}
ButtonColumn.java
import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.AbstractCellEditor;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;
/**
* The ButtonColumn class provides a renderer and an editor that looks like a
* JButton. The renderer and editor will then be used for a specified column in
* the table. The TableModel will contain the String to be displayed on the
* button.
*
* The button can be invoked by a mouse click or by pressing the space bar when
* the cell has focus. Optionally a mnemonic can be set to invoke the button.
* When the button is invoked the provided Action is invoked. The source of the
* Action will be the table. The action command will contain the model row
* number of the button that was clicked.
*
*/
public class ButtonColumn extends AbstractCellEditor implements
TableCellRenderer, TableCellEditor, ActionListener, MouseListener {
private JTable table;
private Action action;
private int mnemonic;
private Border originalBorder;
private Border focusBorder;
private JButton renderButton;
private JButton editButton;
private Object editorValue;
private boolean isButtonColumnEditor;
/**
* Create the ButtonColumn to be used as a renderer and editor. The renderer
* and editor will automatically be installed on the TableColumn of the
* specified column.
*
* #param table
* the table containing the button renderer/editor
* #param action
* the Action to be invoked when the button is invoked
* #param column
* the column to which the button renderer/editor is added
*/
public ButtonColumn(JTable table, Action action, int column) {
this.table = table;
this.action = action;
renderButton = new JButton();
editButton = new JButton();
editButton.setFocusPainted(false);
editButton.addActionListener(this);
originalBorder = editButton.getBorder();
setFocusBorder(new LineBorder(Color.BLUE));
TableColumnModel columnModel = table.getColumnModel();
columnModel.getColumn(column).setCellRenderer(this);
columnModel.getColumn(column).setCellEditor(this);
table.addMouseListener(this);
}
/**
* Get foreground color of the button when the cell has focus
*
* #return the foreground color
*/
public Border getFocusBorder() {
return focusBorder;
}
/**
* The foreground color of the button when the cell has focus
*
* #param focusBorder
* the foreground color
*/
public void setFocusBorder(Border focusBorder) {
this.focusBorder = focusBorder;
editButton.setBorder(focusBorder);
}
public int getMnemonic() {
return mnemonic;
}
/**
* The mnemonic to activate the button when the cell has focus
*
* #param mnemonic
* the mnemonic
*/
public void setMnemonic(int mnemonic) {
this.mnemonic = mnemonic;
renderButton.setMnemonic(mnemonic);
editButton.setMnemonic(mnemonic);
}
#Override
public Component getTableCellEditorComponent(JTable table, Object value,
boolean isSelected, int row, int column) {
if (value == null) {
editButton.setText("");
editButton.setIcon(null);
} else if (value instanceof Icon) {
editButton.setText("");
editButton.setIcon((Icon) value);
} else {
editButton.setText(value.toString());
editButton.setIcon(null);
}
this.editorValue = value;
return editButton;
}
#Override
public Object getCellEditorValue() {
return editorValue;
}
//
// Implement TableCellRenderer interface
//
#Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
if (isSelected) {
renderButton.setForeground(table.getSelectionForeground());
renderButton.setBackground(table.getSelectionBackground());
} else {
renderButton.setForeground(table.getForeground());
renderButton.setBackground(UIManager.getColor("Button.background"));
}
if (hasFocus) {
renderButton.setBorder(focusBorder);
} else {
renderButton.setBorder(originalBorder);
}
// renderButton.setText( (value == null) ? "" : value.toString() );
if (value == null) {
renderButton.setText("");
renderButton.setIcon(null);
} else if (value instanceof Icon) {
renderButton.setText("");
renderButton.setIcon((Icon) value);
} else {
renderButton.setText(value.toString());
renderButton.setIcon(null);
}
return renderButton;
}
//
// Implement ActionListener interface
//
/*
* The button has been pressed. Stop editing and invoke the custom Action
*/
#Override
public void actionPerformed(ActionEvent e) {
int row = table.convertRowIndexToModel(table.getEditingRow());
fireEditingStopped();
// Invoke the Action
ActionEvent event = new ActionEvent(table,
ActionEvent.ACTION_PERFORMED, "" + row);
action.actionPerformed(event);
}
//
// Implement MouseListener interface
//
/*
* When the mouse is pressed the editor is invoked. If you then then drag
* the mouse to another cell before releasing it, the editor is still
* active. Make sure editing is stopped when the mouse is released.
*/
#Override
public void mousePressed(MouseEvent e) {
if (table.isEditing() && table.getCellEditor() == this)
isButtonColumnEditor = true;
}
#Override
public void mouseReleased(MouseEvent e) {
if (isButtonColumnEditor && table.isEditing())
table.getCellEditor().stopCellEditing();
isButtonColumnEditor = false;
}
#Override
public void mouseClicked(MouseEvent e) {
}
#Override
public void mouseEntered(MouseEvent e) {
}
#Override
public void mouseExited(MouseEvent e) {
}
}
The problem isn't the editor. The SPACE key stroke is not forwarded to the default editor in the first column either.
The problem is that JTable defines an Action for the SPACE key so it is intercepted before it has a chance to be passed to the editor. Search my blog for the Key Bindings entry where you will find a program that lists all the default Key Bindings for JTable. The Action that is invoked is called "addToSelection", so I'm not sure why it works differently depending on the row selection.
Anyway one solution is to remove this Action:
InputMap im = table.getInputMap(JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
KeyStroke space = KeyStroke.getKeyStroke("SPACE");
im.put(space, "none");
Both TableTest, which uses ButtonColumn, and TablePopupEditor are complete examples that work correctly when the Space key is pressed in a selected button cell. Neither evinces the typical ButtonModel-defined appearance of a stand-alone button, but you can provide your own visual queues as required. This related example uses a colored border.
I have to build a complex GUI in JAVA with Swing (for the moment I have near 80 classes).
The graphic partv of the application is split as follows: a first series of tabs (eg "Management", "Administration", "Configuration"), then a second level (for example, "User", "Group", "Game"). For now I'm two grade levels (one for each level of tabs). The next level is a JPanel that manages a business object (my whole GUI is built around my business model), at this level there are 2 type of JPanel: who manages simple objects (eg, "User", "Category" , "Game", "Level") and those which manages objects "composite primary key" (eg "User_Game" which represent the form of a double-entry table for each game level for all users).
My second level of tabs can contain multiple JPanel.
When my JPanel manages a single object is composed of a JTable and two buttons (Add and Remove) on which I put events, if not it is a simple JTable. When I have foreign keys (eg "Group" for "User" and "Category" to "Game" or "Level" to "User_Game") it is a JComboBox that takes its information directly from JTableModel. When it comes to managing a JTable object to "composite primary key" the columns and rows also directly dependent models (eg "Game" and "User" "User_Game").
Each has its own JTable model that deals with the persistence layer (Hibernate for information) and other TableModel.
To manage changes (such as adding, modifying or deleting a "User") I use the code below:
import java.awt.event.*;
import javax.swing.*;
import java.beans.*;
/*
* This class listens for changes made to the data in the table via the
* TableCellEditor. When editing is started, the value of the cell is saved
* When editing is stopped the new value is saved. When the oold and new
* values are different, then the provided Action is invoked.
*
* The source of the Action is a TableCellListener instance.
*/
public class TabCellListener implements PropertyChangeListener, Runnable
{
private JTable table;
private Action action;
private int row;
private int column;
private Object oldValue;
private Object newValue;
/**
* Create a TableCellListener.
*
* #param table the table to be monitored for data changes
* #param action the Action to invoke when cell data is changed
*/
public TabCellListener(JTable table, Action action)
{
this.table = table;
this.action = action;
this.table.addPropertyChangeListener( this );
this.table.getModel().addTableModelListener(new ModelListenerTableGui(this.table, this.action));
}
/**
* Create a TableCellListener with a copy of all the data relevant to
* the change of data for a given cell.
*
* #param row the row of the changed cell
* #param column the column of the changed cell
* #param oldValue the old data of the changed cell
* #param newValue the new data of the changed cell
*/
private CellListenerTableGui(JTable table, int row, int column, Object oldValue, Object newValue)
{
this.table = table;
this.row = row;
this.column = column;
this.oldValue = oldValue;
this.newValue = newValue;
}
/**
* Get the column that was last edited
*
* #return the column that was edited
*/
public int getColumn()
{
return column;
}
/**
* Get the new value in the cell
*
* #return the new value in the cell
*/
public Object getNewValue()
{
return newValue;
}
/**
* Get the old value of the cell
*
* #return the old value of the cell
*/
public Object getOldValue()
{
return oldValue;
}
/**
* Get the row that was last edited
*
* #return the row that was edited
*/
public int getRow()
{
return row;
}
/**
* Get the table of the cell that was changed
*
* #return the table of the cell that was changed
*/
public JTable getTable()
{
return table;
}
//
// Implement the PropertyChangeListener interface
//
#Override
public void propertyChange(PropertyChangeEvent e)
{
// A cell has started/stopped editing
if ("tableCellEditor".equals(e.getPropertyName()))
{
if (table.isEditing())
processEditingStarted();
else
processEditingStopped();
}
}
/*
* Save information of the cell about to be edited
*/
private void processEditingStarted()
{
// The invokeLater is necessary because the editing row and editing
// column of the table have not been set when the "tableCellEditor"
// PropertyChangeEvent is fired.
// This results in the "run" method being invoked
SwingUtilities.invokeLater( this );
}
/*
* See above.
*/
#Override
public void run()
{
row = table.convertRowIndexToModel( table.getEditingRow() );
column = table.convertColumnIndexToModel( table.getEditingColumn() );
oldValue = table.getModel().getValueAt(row, column);
newValue = null;
}
/*
* Update the Cell history when necessary
*/
private void processEditingStopped()
{
newValue = table.getModel().getValueAt(row, column);
// The data has changed, invoke the supplied Action
if ((newValue == null && oldValue != null) || (newValue != null && !newValue.equals(oldValue)))
{
// Make a copy of the data in case another cell starts editing
// while processing this change
CellListenerTableGui tcl = new CellListenerTableGui(
getTable(), getRow(), getColumn(), getOldValue(), getNewValue());
ActionEvent event = new ActionEvent(
tcl,
ActionEvent.ACTION_PERFORMED,
"");
action.actionPerformed(event);
}
}
}
And the following action:
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import javax.swing.Action;
public class UpdateTableListener<N> extends AbstractTableListener implements Action
{
protected boolean enabled;
public UpdateTableListener(AbstractTableGui<N> obs)
{
super(obs);
this.enabled = true;
}
#Override
public void actionPerformed(ActionEvent e)
{
if (null != e && e.getSource() instanceof CellListenerTableGui)
{
TabCellListener tcl = (TabCellListener)e.getSource();
this.obs.getModel().setValueAt(tcl.getNewValue(), tcl.getRow(), tcl.getColumn());
int sel = this.obs.getModel().getNextRequiredColumn(tcl.getRow());
if (sel == -1)
this.obs.getModel().save(tcl.getRow());
}
}
#Override
public void addPropertyChangeListener(PropertyChangeListener arg0)
{
}
#Override
public Object getValue(String arg0)
{
return null;
}
#Override
public boolean isEnabled()
{
return this.enabled;
}
#Override
public void putValue(String arg0, Object arg1)
{
}
#Override
public void removePropertyChangeListener(PropertyChangeListener arg0)
{
}
#Override
public void setEnabled(boolean arg0)
{
this.enabled = arg0;
}
}
This code works well, data are well persisted.
Then I add this code to refresh dependent components:
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import javax.swing.Action;
public class ChangeTableListener implements Action
{
protected AbstractTableGui table;
public ChangeTableListener(AbstractTableGui table)
{
this.table = table;
}
#Override
public void actionPerformed(ActionEvent arg0)
{
this.table.getModel().fireTableDataChanged();
this.table.repaint();
}
#Override
public void addPropertyChangeListener(PropertyChangeListener arg0)
{
}
#Override
public Object getValue(String arg0)
{
return null;
}
#Override
public boolean isEnabled()
{
return false;
}
#Override
public void putValue(String arg0, Object arg1)
{
}
#Override
public void removePropertyChangeListener(PropertyChangeListener arg0)
{
}
#Override
public void setEnabled(boolean arg0)
{
}
}
My TableModel.fireTableDataChanged rebuild JTable content (calle super.fireTableDataChanged and fireTableStructureChanged), JTable.repaint reset the Renderers, and it works for Combobox (forein keys) and it update well title on double-entry tables, but it can't add or delete columns or rows on double-entry tables.
Moreover I see more high latency if there is the slightest change.
My question is simple: how do you manage inter-dependent components?
For your help,
In advance,
Thanks.
Edit :
Here an example of TableCellEditor.
import javax.swing.DefaultCellEditor;
import javax.swing.JTextField;
public class TextColumnEditor extends DefaultCellEditor
{
public TextColumnEditor()
{
super(new JTextField());
}
public boolean stopCellEditing()
{
Object v = this.getCellEditorValue();
if(v == null || v.toString().length() == 0)
{
this.fireEditingCanceled();
return false;
}
return super.stopCellEditing();
}
}
An example of TableModel :
import java.util.ArrayList;
public class GroupModelTable extends AbstractModelTable<Groups>
{
protected GroupsService service;
public GroupModelTable(AbstractTableGui<Groups> content)
{
super(content, new ArrayList<String>(), new ArrayList<Groups>());
this.headers.add("Group");
this.content.setModel(this);
this.service = new GroupsService();
this.setLines(this.service.search(new Groups()));
}
public Object getValueAt(int rowIndex, int columnIndex)
{
switch (columnIndex)
{
case 0:
return this.lines.get(rowIndex).getTitle();
default:
return "";
}
}
public void setValueAt(Object aVal, int rowIndex, int columnIndex)
{
switch (columnIndex)
{
case 0:
this.lines.get(rowIndex).setTitle(aVal.toString());
break;
default:
break;
}
}
#Override
public Groups getModel(int line, int column)
{
return null;
}
#Override
public Groups getModel(int line)
{
return this.lines.get(line);
}
public boolean isCellEditable(int row, int column)
{
return true;
}
#Override
public GroupModelTableGui newLine()
{
this.lines.add(new Groups());
return this;
}
#Override
public int getNextRequiredColumn(int row)
{
Groups g = this.getModel(row);
if (g != null && g.getTitle() != null && g.getTitle().length() > 0)
return -1;
return 0;
}
#Override
public void save(int row)
{
Groups g = this.getModel(row);
if (g != null)
{
try
{
if (g.getId() == null)
this.service.create(g);
else
this.service.update(g);
}
catch (Exception e)
{
}
}
}
#Override
public void removeRow(int row)
{
Groups g = this.getModel(row);
if (g != null)
{
try
{
if (g.getId() != null)
this.service.delete(g);
super.removeRow(row);
}
catch (Exception e)
{
}
}
}
}
An example of Table :
public class GroupTable extends AbstractTable<Groups>
{
public GroupTable()
{
new GroupModelTableGui(this);
new CellListenerTableGui(this.getContent(), new UpdateTableListenerGui<Groups>(this));
this.getContent().getColumnModel().getColumn(0).setCellEditor(new TextColumnEditorGui());
}
}
I hope it will help you to understand :/
Your TabCellListener is unfamiliar to me. Your TableCellEditor should not interact with the TableModel directly. It should implement getTableCellEditorComponent() and getCellEditorValue(), as shown in this example. When the editor concludes, the model will have the new value. Your TableModel should handle persistence. More than one view may listen to a single TableModel via TableModelListener.
Addendum: Your CellEditor, TextColumnEditor, probably shouldn't invoke fireEditingCanceled(). Pressing Escape should be sufficient to revert the edit, as shown in this example. You might also look at the related tutorial section and example.