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.
Related
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.
Working in a shared table model example I realized that if we attach a row filter to a table's row sorter this filter doesn't have any effect on cell update events. According to RowSorter API:
Concrete implementations of RowSorter need to reference a model such
as TableModel or ListModel. The view classes, such as JTable and
JList, will also have a reference to the model. To avoid ordering
dependencies, RowSorter implementations should not install a listener
on the model. Instead the view class will call into the RowSorter when
the model changes. For example, if a row is updated in a TableModel
JTable invokes rowsUpdated. When the model changes, the view may call
into any of the following methods: modelStructureChanged,
allRowsChanged, rowsInserted, rowsDeleted and rowsUpdated.
So as I understand this paragraph, a cell update is a particular case of row update and as such rowsUpdated should be called and row filtered accordingly.
To illustrate what I'm saying, please consider this simple filter:
private void applyFilter() {
DefaultRowSorter sorter = (DefaultRowSorter)table.getRowSorter();
sorter.setRowFilter(new RowFilter() {
#Override
public boolean include(RowFilter.Entry entry) {
Boolean value = (Boolean)entry.getValue(2);
return value == null || value;
}
});
}
Here the third column is expected to be a Boolean and entry (row) has to be included if the cell value is either null or true. If I edit a cell placed at third column and set its value to false then I'd expect this row just "disappear" from the view. However, to accomplish this I have to set a new filter again because it doesn't seem to work "automatically".
Attaching a TableModelListener to the model as follows, I can see the update event on cell edits:
model.addTableModelListener(new TableModelListener() {
#Override
public void tableChanged(TableModelEvent e) {
if (e.getType() == TableModelEvent.UPDATE) {
int row = e.getLastRow();
int column = e.getColumn();
Object value = ((TableModel)e.getSource()).getValueAt(row, column);
String text = String.format("Update event. Row: %1s Column: %2s Value: %3s", row, column, value);
System.out.println(text);
}
}
});
As I've said, if I reset the filter using this TableModelListener then it works as expected:
model.addTableModelListener(new TableModelListener() {
#Override
public void tableChanged(TableModelEvent e) {
if (e.getType() == TableModelEvent.UPDATE) {
applyFilter();
}
}
});
Question: is this a bug/implementation issue? Or I'm misunderstanding the API?
Here is a complete MCVE illustrating the problem.
import java.awt.BorderLayout;
import javax.swing.BorderFactory;
import javax.swing.DefaultRowSorter;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.RowFilter;
import javax.swing.SwingUtilities;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
public class Demo {
private JTable table;
private void createAndShowGUI() {
DefaultTableModel model = new DefaultTableModel(5, 3) {
#Override
public boolean isCellEditable(int row, int column) {
return column == 2;
}
#Override
public Class<?> getColumnClass(int columnIndex) {
return columnIndex == 2 ? Boolean.class : super.getColumnClass(columnIndex);
}
};
model.addTableModelListener(new TableModelListener() {
#Override
public void tableChanged(TableModelEvent e) {
if (e.getType() == TableModelEvent.UPDATE) {
int row = e.getLastRow();
int column = e.getColumn();
Object value = ((TableModel)e.getSource()).getValueAt(row, column);
String text = String.format("Update event. Row: %1s Column: %2s Value: %3s", row, column, value);
System.out.println(text);
// applyFilter(); un-comment this line to make it work
}
}
});
table = new JTable(model);
table.setAutoCreateRowSorter(true);
applyFilter();
JPanel content = new JPanel(new BorderLayout());
content.setBorder(BorderFactory.createEmptyBorder(8,8,8,8));
content.add(new JScrollPane(table));
JFrame frame = new JFrame("Demo");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.add(content);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private void applyFilter() {
DefaultRowSorter sorter = (DefaultRowSorter)table.getRowSorter();
sorter.setRowFilter(new RowFilter() {
#Override
public boolean include(RowFilter.Entry entry) {
Boolean value = (Boolean)entry.getValue(2);
return value == null || value;
}
});
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new Demo().createAndShowGUI();
}
});
}
}
Well, after doing some research and reading the API, bugs reports and Oracle forum I've found some interesting things.
1. Set DefaultRowSorter's sortsOnUpdate property to true
The first thing I found is we have to set sortsOnUpdate property to true in order to enable the notification chain when rowsUpdated(...) is called. Otherwise no RowSorterEvent will be fired and the view (our JTable) won't be aware that something happened and won't repaint accordingly. So making this little change:
DefaultRowSorter sorter = (DefaultRowSorter)table.getRowSorter();
sorter.setRowFilter(new RowFilter() {
#Override
public boolean include(RowFilter.Entry entry) {
Boolean value = (Boolean)entry.getValue(2);
return value == null || value;
}
});
sorter.setSortsOnUpdates(true);
We won't have to re-apply the filter on a table model update. But...
2. There's a bug in JTable component processing the RowSorterEvent notification
While JTable implements RowSorterListener interface, subscribes itself to the row sorter as a listener and process RowSorterEvents. there's a bug repainting the table. The odd behavior is well described in these posts:
How to Filter JTable Rows as Data Changes
DefaultRowSorter.setSortsOnUpdates(true) Disaster
In a nutshell:
When a RowSorterEvent.TYPE.SORTED event is processed by JTable, it repaints only the area related to the involded rows but not the rest of the table, which remains as it was. Let's say we edit the first row and it should be filtered now. Then the rest of the rows should be shifted up one row to top but it turns out they are not: only the first row will be correctly repainted to show the second row but the rest of the table still the same. This is in fact a bug because in this particular case the whole table needs to be repainted. See core bug # 6791934
As a workaround we could either attach a new RowSorterListener to the RowSorter or override JTable's sorterChanged(...) as follows in order to force a whole repaint on our table (IMHO the second method is preferred).
DefaultRowSorter sorter = (DefaultRowSorter)table.getRowSorter();
...
sorter.addRowSorterListener(new RowSorterListener() {
#Override
public void sorterChanged(RowSorterEvent e) {
if (e.getType() == RowSorterEvent.Type.SORTED) {
// We need to call both revalidate() and repaint()
table.revalidate();
table.repaint();
}
}
});
Or
JTable table = new JTable(tableModel) {
#Override
public void sorterChanged(RowSorterEvent e) {
super.sorterChanged(e);
if (e.getType() == RowSorterEvent.Type.SORTED) {
resizeAndRepaint(); // this protected method calls both revalidate() and repaint()
}
}
};
3. JXTable component has a workaround to this bug
The JXTable component that is part of SwingX library and extends from JTable doesn't have this problem because SwingLabs team has overwrote sorterChanged(...) mthod as follows to hack around this bug:
//----> start hack around core issue 6791934:
// table not updated correctly after updating model
// while having a sorter with filter.
/**
* Overridden to hack around core bug
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6791934
*
*/
#Override
public void sorterChanged(RowSorterEvent e) {
super.sorterChanged(e);
postprocessSorterChanged(e);
}
/** flag to indicate if forced revalidate is needed. */
protected boolean forceRevalidate;
/** flag to indicate if a sortOrderChanged has happened between pre- and postProcessModelChange. */
protected boolean filteredRowCountChanged;
/**
* Hack around core issue 6791934: sets flags to force revalidate if appropriate.
* Called before processing the event.
* #param e the TableModelEvent received from the model
*/
protected void preprocessModelChange(TableModelEvent e) {
forceRevalidate = getSortsOnUpdates() && getRowFilter() != null && isUpdate(e) ;
}
/**
* Hack around core issue 6791934: forces a revalidate if appropriate and resets
* internal flags.
* Called after processing the event.
* #param e the TableModelEvent received from the model
*/
protected void postprocessModelChange(TableModelEvent e) {
if (forceRevalidate && filteredRowCountChanged) {
resizeAndRepaint();
}
filteredRowCountChanged = false;
forceRevalidate = false;
}
/**
* Hack around core issue 6791934: sets the sorter changed flag if appropriate.
* Called after processing the event.
* #param e the sorter event received from the sorter
*/
protected void postprocessSorterChanged(RowSorterEvent e) {
filteredRowCountChanged = false;
if (forceRevalidate && e.getType() == RowSorterEvent.Type.SORTED) {
filteredRowCountChanged = e.getPreviousRowCount() != getRowCount();
}
}
//----> end hack around core issue 6791934:
So, this is one more reason (if some missing) to use SwingX.
I have a table cell factory responsible for creating an editable cell in a JavaFX TableView.
I'm trying to implement some added functionality to the tableview so that when the user clicks outside the editable cell a commit is made (the edited text is saved, and not discarded as per the default tableview behavior.)
I added an textField.focusedProperty() event handler, where I commit the text from the text field. However, when one clicks outside the current cell cancelEdit() gets called and calling commitEdit(textField.getText()); has no effect.
I have come to realize that once cancelEdit() is called the TableCell.isEditing() returns false and so the commit will never happen.
How can I make so that when the user clicks outside the editable cell the text is committed?
After committing an setOnEditCommit() event handler will take care of the validation and database logic. I haven't included it here since it will most likely complicate things even further.
// EditingCell - for editing capability in a TableCell
public static class EditingCell extends TableCell<Person, String> {
private TextField textField;
public EditingCell() {
}
#Override public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setText(null);
setGraphic(textField);
textField.selectAll();
}
#Override public void cancelEdit() {
super.cancelEdit();
setText((String) getItem());
setGraphic(null);
}
#Override public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(textField.getText());
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (!newValue) {
commitEdit(textField.getText());
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
Since I could not find kuaw26's source code (dead link) I developed my own solution for java 8. I found out that the TextField in the code above never receives a keyReleased event for the esc-key, therefore his code does not work.
Unfortunately I needed to duplicate code from TextFieldTableCell and CellUtils and adapt it, since TextFieldTableCell uses a private TextField and CellUtils is package protected. This is probably not the best OO way.
Here is my solution:
// package yourLib;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.util.Callback;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
/**
* A class containing a {#link TableCell} implementation that draws a
* {#link TextField} node inside the cell. If the TextField is
* left, the value is commited.
*
*/
public class AcceptOnExitTableCell<S,T> extends TableCell<S,T> {
/***************************************************************************
* *
* Static cell factories *
* *
**************************************************************************/
/**
* Provides a {#link TextField} that allows editing of the cell content when
* the cell is double-clicked, or when
* {#link TableView#edit(int, javafx.scene.control.TableColumn)} is called.
* This method will only work on {#link TableColumn} instances which are of
* type String.
*
* #return A {#link Callback} that can be inserted into the
* {#link TableColumn#cellFactoryProperty() cell factory property} of a
* TableColumn, that enables textual editing of the content.
*/
public static <S> Callback<TableColumn<S,String>, TableCell<S,String>> forTableColumn() {
return forTableColumn(new DefaultStringConverter());
}
/**
* Provides a {#link TextField} that allows editing of the cell content when
* the cell is double-clicked, or when
* {#link TableView#edit(int, javafx.scene.control.TableColumn) } is called.
* This method will work on any {#link TableColumn} instance, regardless of
* its generic type. However, to enable this, a {#link StringConverter} must
* be provided that will convert the given String (from what the user typed
* in) into an instance of type T. This item will then be passed along to the
* {#link TableColumn#onEditCommitProperty()} callback.
*
* #param converter A {#link StringConverter} that can convert the given String
* (from what the user typed in) into an instance of type T.
* #return A {#link Callback} that can be inserted into the
* {#link TableColumn#cellFactoryProperty() cell factory property} of a
* TableColumn, that enables textual editing of the content.
*/
public static <S,T> Callback<TableColumn<S,T>, TableCell<S,T>> forTableColumn(
final StringConverter<T> converter) {
return list -> new AcceptOnExitTableCell<S,T>(converter);
}
/***************************************************************************
* *
* Fields *
* *
**************************************************************************/
private TextField textField;
private boolean escapePressed=false;
private TablePosition<S, ?> tablePos=null;
/***************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates a default TextFieldTableCell with a null converter. Without a
* {#link StringConverter} specified, this cell will not be able to accept
* input from the TextField (as it will not know how to convert this back
* to the domain object). It is therefore strongly encouraged to not use
* this constructor unless you intend to set the converter separately.
*/
public AcceptOnExitTableCell() {
this(null);
}
/**
* Creates a TextFieldTableCell that provides a {#link TextField} when put
* into editing mode that allows editing of the cell content. This method
* will work on any TableColumn instance, regardless of its generic type.
* However, to enable this, a {#link StringConverter} must be provided that
* will convert the given String (from what the user typed in) into an
* instance of type T. This item will then be passed along to the
* {#link TableColumn#onEditCommitProperty()} callback.
*
* #param converter A {#link StringConverter converter} that can convert
* the given String (from what the user typed in) into an instance of
* type T.
*/
public AcceptOnExitTableCell(StringConverter<T> converter) {
this.getStyleClass().add("text-field-table-cell");
setConverter(converter);
}
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
// --- converter
private ObjectProperty<StringConverter<T>> converter =
new SimpleObjectProperty<StringConverter<T>>(this, "converter");
/**
* The {#link StringConverter} property.
*/
public final ObjectProperty<StringConverter<T>> converterProperty() {
return converter;
}
/**
* Sets the {#link StringConverter} to be used in this cell.
*/
public final void setConverter(StringConverter<T> value) {
converterProperty().set(value);
}
/**
* Returns the {#link StringConverter} used in this cell.
*/
public final StringConverter<T> getConverter() {
return converterProperty().get();
}
/***************************************************************************
* *
* Public API *
* *
**************************************************************************/
/** {#inheritDoc} */
#Override public void startEdit() {
if (! isEditable()
|| ! getTableView().isEditable()
|| ! getTableColumn().isEditable()) {
return;
}
super.startEdit();
if (isEditing()) {
if (textField == null) {
textField = getTextField();
}
escapePressed=false;
startEdit(textField);
final TableView<S> table = getTableView();
tablePos=table.getEditingCell();
}
}
/** {#inheritDoc} */
#Override public void commitEdit(T newValue) {
if (! isEditing())
return;
final TableView<S> table = getTableView();
if (table != null) {
// Inform the TableView of the edit being ready to be committed.
CellEditEvent editEvent = new CellEditEvent(
table,
tablePos,
TableColumn.editCommitEvent(),
newValue
);
Event.fireEvent(getTableColumn(), editEvent);
}
// we need to setEditing(false):
super.cancelEdit(); // this fires an invalid EditCancelEvent.
// update the item within this cell, so that it represents the new value
updateItem(newValue, false);
if (table != null) {
// reset the editing cell on the TableView
table.edit(-1, null);
// request focus back onto the table, only if the current focus
// owner has the table as a parent (otherwise the user might have
// clicked out of the table entirely and given focus to something else.
// It would be rude of us to request it back again.
// requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
}
}
/** {#inheritDoc} */
#Override public void cancelEdit() {
if(escapePressed) {
// this is a cancel event after escape key
super.cancelEdit();
setText(getItemText()); // restore the original text in the view
}
else {
// this is not a cancel event after escape key
// we interpret it as commit.
String newText=textField.getText(); // get the new text from the view
this.commitEdit(getConverter().fromString(newText)); // commit the new text to the model
}
setGraphic(null); // stop editing with TextField
}
/** {#inheritDoc} */
#Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
updateItem();
}
/***************************************************************************
* *
* // djw code taken and adapted from package protected CellUtils. *
* *
**************************************************************************/
private TextField getTextField() {
final TextField textField = new TextField(getItemText());
// Use onAction here rather than onKeyReleased (with check for Enter),
// as otherwise we encounter RT-34685
textField.setOnAction(event -> {
if (converter == null) {
throw new IllegalStateException(
"Attempting to convert text input into Object, but provided "
+ "StringConverter is null. Be sure to set a StringConverter "
+ "in your cell factory.");
}
this.commitEdit(getConverter().fromString(textField.getText()));
event.consume();
});
textField.setOnKeyPressed(t -> { if (t.getCode() == KeyCode.ESCAPE) escapePressed = true; else escapePressed = false; });
textField.setOnKeyReleased(t -> {
if (t.getCode() == KeyCode.ESCAPE) {
// djw the code may depend on java version / expose incompatibilities:
throw new IllegalArgumentException("did not expect esc key releases here.");
}
});
return textField;
}
private String getItemText() {
return getConverter() == null ?
getItem() == null ? "" : getItem().toString() :
getConverter().toString(getItem());
}
private void updateItem() {
if (isEmpty()) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getItemText());
}
setText(null);
setGraphic(textField);
} else {
setText(getItemText());
setGraphic(null);
}
}
}
private void startEdit(final TextField textField) {
if (textField != null) {
textField.setText(getItemText());
}
setText(null);
setGraphic(textField);
textField.selectAll();
// requesting focus so that key input can immediately go into the
// TextField (see RT-28132)
textField.requestFocus();
}
}
You could do it by overriding the method commitEdit as next:
#Override
public void commitEdit(T item) {
// This block is necessary to support commit on losing focus, because
// the baked-in mechanism sets our editing state to false before we can
// intercept the loss of focus. The default commitEdit(...) method
// simply bails if we are not editing...
if (!isEditing() && !item.equals(getItem())) {
TableView<S> table = getTableView();
if (table != null) {
TableColumn<S, T> column = getTableColumn();
CellEditEvent<S, T> event = new CellEditEvent<>(
table, new TablePosition<S,T>(table, getIndex(), column),
TableColumn.editCommitEvent(), item
);
Event.fireEvent(column, event);
}
}
super.commitEdit(item);
}
This workaround comes from https://gist.github.com/james-d/be5bbd6255a4640a5357#file-editcell-java-L109
Here's how I did it - I binded the textField's text property with the text property of the cell (bidirectional).
class EditingCell<S, T> extends TableCell<S, T> {
private final TextField mTextField;
public EditingCell() {
super();
mTextField = new TextField();
mTextField.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if( event.getCode().equals(KeyCode.ENTER) )
commitEdit((T)mTextField.getText());
}
});
mTextField.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if( !newValue )
commitEdit((T)mTextField.getText());
}
});
mTextField.textProperty().bindBidirectional(textProperty());
}
#Override
public void startEdit() {
super.startEdit();
setGraphic(mTextField);
}
#Override
public void cancelEdit() {
super.cancelEdit();
setGraphic(null);
}
#Override
public void updateItem(final T item, final boolean empty) {
super.updateItem(item, empty);
if( empty ) {
setText(null);
setGraphic(null);
}
else {
if( item == null ) {
setGraphic(null);
}
else {
if( isEditing() ) {
setGraphic(mTextField);
setText((String)getItem());
}
else {
setGraphic(null);
setText((String)getItem());
}
}
}
}
}
I created my own workaround (but for JavaFX 2). Main idea - transform cancelEdit() to commitEdit(). With possible validation of committed text via validator.
/** Validator. */
public interface TextColumnValidator<T> {
boolean valid(T rowVal, String newVal);
}
/**
* Special table text field cell that commit its content on focus lost.
*/
public class TextFieldTableCellEx<S> extends TextFieldTableCell<S, String> {
/** */
private final TextColumnValidator<S> validator;
/** */
private boolean cancelling;
/** */
private boolean hardCancel;
/** */
private String curTxt = "";
/** Create cell factory. */
public static <S> Callback<TableColumn<S, String>, TableCell<S, String>>
cellFactory(final TextColumnValidator<S> validator) {
return new Callback<TableColumn<S, String>, TableCell<S, String>>() {
#Override public TableCell<S, String> call(TableColumn<S, String> col) {
return new TextFieldTableCellEx<>(validator);
}
};
}
/**
* Text field cell constructor.
*
* #param validator Input text validator.
*/
private TextFieldTableCellEx(TextColumnValidator<S> validator) {
this.validator = validator;
}
/** {#inheritDoc} */
#Override public void startEdit() {
super.startEdit();
curTxt = "";
hardCancel = false;
Node g = getGraphic();
if (g != null) {
final TextField tf = (TextField)g;
tf.textProperty().addListener(new ChangeListener<String>() {
#Override public void changed(ObservableValue<? extends String> val, String oldVal, String newVal) {
curTxt = newVal;
}
});
tf.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override public void handle(KeyEvent evt) {
if (KeyCode.ENTER == evt.getCode())
cancelEdit();
else if (KeyCode.ESCAPE == evt.getCode()) {
hardCancel = true;
cancelEdit();
}
}
});
// Special hack for editable TextFieldTableCell.
// Cancel edit when focus lost from text field, but do not cancel if focus lost to VirtualFlow.
tf.focusedProperty().addListener(new ChangeListener<Boolean>() {
#Override public void changed(ObservableValue<? extends Boolean> val, Boolean oldVal, Boolean newVal) {
Node fo = getScene().getFocusOwner();
if (!newVal) {
if (fo instanceof VirtualFlow) {
if (fo.getParent().getParent() != getTableView())
cancelEdit();
}
else
cancelEdit();
}
}
});
Platform.runLater(new Runnable() {
#Override public void run() {
tf.requestFocus();
}
});
}
}
/** {#inheritDoc} */
#Override public void cancelEdit() {
if (cancelling)
super.cancelEdit();
else
try {
cancelling = true;
if (hardCancel || curTxt.trim().isEmpty())
super.cancelEdit();
else if (validator.valid(getTableView().getSelectionModel().getSelectedItem(), curTxt))
commitEdit(curTxt);
else
super.cancelEdit();
}
finally {
cancelling = false;
}
}
}
Update: this code was written as a part of Apache Ignite Schema Import GUI Utility.
See full version of TableCell code: https://github.com/apache/ignite/blob/ignite-1.9/modules/schema-import/src/main/java/org/apache/ignite/schema/ui/Controls.java
Also you could build this utility (it is a very simple utility with 2 screens) and play with it under Java7/javaFx2 & Java8/JavaFx8.
I tested - it works under both of them.
Following Situation: I have a J(X)Table with RowHeader (As guidline I used one of Rob Camicks great Examples). All worked as expected.
By requirement the data I receive from server already contains a tablerownumber, which I have to show in the rowheader and the data should be filterable. So I extended the example, and I added a filter. When I filtered the view I saw gaps in my row numbers (for example: 1, 3, 6,..), which is the desired effect.
To be able to filter and sort the table by my own tablerow, I added a TableRowSorter. And here I started to get confused. The Example uses the same TableModel and SelectionModel for mainTable and rowHeaderTable:
setModel( main.getModel() );
setSelectionModel( main.getSelectionModel() );
This is great, since I don’t have to synchronize them. But concerning TableRowSorter I suddenly wasn’t sure, if I also can or even have to use the same TableRowSorter-Instance or if I have to create a TableRowSorter for each table. First I added the same to both Tables, since this seemed practically, but then I got IndexOutOfBound-Exceptions in many cases. After some digging I found out that this is because the TableRowSorter gets updated twice at each TableModelEvent, because each table (RowHeader and MainTable) notifies the TableRowSorter about tablechanges on its own.
Now I am not sure which the right way to go is. Following solutions came into my mind: Should I add a second TableRowSorter (one for each table) and synchronize these, or should I wrap the TableModel within the RowHeaderTable and let it not fireing any Events? Or maybe I should create my own kind of RowHeaderTable which doesn’t notify Sorters about changes at all?
Here's a quick (beware: not formally tested! the usage example works fine, though) implementation of a wrapping RowSorter.
does nothing on receiving notification of model changes
delegates all status queries
listens to wrapped rowSorter and propagates its events
It's client's responsibility to keep it in synch with the rowSorter used in the main table
Usage example (in terms of SwingX test infrastructure and with SwingX sortController/table):
public void interactiveRowSorterWrapperSharedXTable() {
final DefaultTableModel tableModel = new DefaultTableModel(list.getElementCount(), 2) {
#Override
public Class<?> getColumnClass(int columnIndex) {
return Integer.class;
}
};
for (int i = 0; i < tableModel.getRowCount(); i++) {
tableModel.setValueAt(i, i, 0);
tableModel.setValueAt(tableModel.getRowCount() - i, i, 1);
}
final JXTable master = new JXTable(tableModel);
final TableSortController<TableModel> rowSorter = (TableSortController<TableModel>) master.getRowSorter();
master.removeColumn(master.getColumn(0));
final JXTable rowHeader = new JXTable(master.getModel());
rowHeader.setAutoCreateRowSorter(false);
rowHeader.removeColumn(rowHeader.getColumn(1));
rowHeader.setRowSorter(new RowSorterWrapper<TableModel>(rowSorter));
rowHeader.setSelectionModel(master.getSelectionModel());
// need to disable selection update on one of the table's
// otherwise the selection is not kept in model coordinates
rowHeader.setUpdateSelectionOnSort(false);
JScrollPane scrollPane = new JScrollPane(master);
scrollPane.setRowHeaderView(rowHeader);
JXFrame frame = showInFrame(scrollPane, "xtables (wrapped sortController): shared model/selection");
Action fireAllChanged = new AbstractAction("fireDataChanged") {
#Override
public void actionPerformed(ActionEvent e) {
tableModel.fireTableDataChanged();
}
};
addAction(frame, fireAllChanged);
Action removeFirst = new AbstractAction("remove firstM") {
#Override
public void actionPerformed(ActionEvent e) {
tableModel.removeRow(0);
}
};
addAction(frame, removeFirst);
Action removeLast = new AbstractAction("remove lastM") {
#Override
public void actionPerformed(ActionEvent e) {
tableModel.removeRow(tableModel.getRowCount() - 1);
}
};
addAction(frame, removeLast);
Action filter = new AbstractAction("toggle filter") {
#Override
public void actionPerformed(ActionEvent e) {
RowFilter filter = rowSorter.getRowFilter();
if (filter == null) {
rowSorter.setRowFilter(RowFilter.regexFilter("^1", 1));
} else {
rowSorter.setRowFilter(null);
}
}
};
addAction(frame, filter);
addStatusMessage(frame, "row header example with RowSorterWrapper");
show(frame);
}
The RowSorterWrapper:
/**
* Wrapping RowSorter for usage (f.i.) in a rowHeader.
*
* Delegates all state queries,
* does nothing on receiving notification of model changes,
* propagates rowSorterEvents from delegates.
*
* Beware: untested!
*
* #author Jeanette Winzenburg, Berlin
*/
public class RowSorterWrapper<M> extends RowSorter<M> {
private RowSorter<M> delegate;
private RowSorterListener rowSorterListener;
public RowSorterWrapper(RowSorter<M> delegate) {
this.delegate = delegate;
delegate.addRowSorterListener(getRowSorterListener());
}
/**
* Creates and returns a RowSorterListener which re-fires received
* events.
*
* #return
*/
protected RowSorterListener getRowSorterListener() {
if (rowSorterListener == null) {
RowSorterListener listener = new RowSorterListener() {
#Override
public void sorterChanged(RowSorterEvent e) {
if (RowSorterEvent.Type.SORT_ORDER_CHANGED == e.getType()) {
fireSortOrderChanged();
} else if (RowSorterEvent.Type.SORTED == e.getType()) {
fireRowSorterChanged(null); }
}
};
rowSorterListener = listener;
}
return rowSorterListener;
}
#Override
public M getModel() {
return delegate.getModel();
}
#Override
public void toggleSortOrder(int column) {
delegate.toggleSortOrder(column);
}
#Override
public int convertRowIndexToModel(int index) {
return delegate.convertRowIndexToModel(index);
}
#Override
public int convertRowIndexToView(int index) {
return delegate.convertRowIndexToView(index);
}
#Override
public void setSortKeys(List keys) {
delegate.setSortKeys(keys);
}
#Override
public List getSortKeys() {
return delegate.getSortKeys();
}
#Override
public int getViewRowCount() {
return delegate.getViewRowCount();
}
#Override
public int getModelRowCount() {
return delegate.getModelRowCount();
}
#Override
public void modelStructureChanged() {
// do nothing, all work done by delegate
}
#Override
public void allRowsChanged() {
// do nothing, all work done by delegate
}
#Override
public void rowsInserted(int firstRow, int endRow) {
// do nothing, all work done by delegate
}
#Override
public void rowsDeleted(int firstRow, int endRow) {
// do nothing, all work done by delegate
}
#Override
public void rowsUpdated(int firstRow, int endRow) {
// do nothing, all work done by delegate
}
#Override
public void rowsUpdated(int firstRow, int endRow, int column) {
// do nothing, all work done by delegate
}
}
In windows it is possible to show a grayed out JCheckbox, to show that the collection of data which it represents not all items have the same value.
Is this even possible with a JCheckBox?
How do i go about this?
(Hoping there's a way to not override it)
Thanks
JIDE Common Layer has a TristateCheckBox.
It's possible with some of work.
I have this code from some years ago. Is based in some examples I found in internet, but I cannot find any reference to the original creator, so I apologize
import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.ActionMapUIResource;
import java.awt.event.*;
/**
* Maintenance tip - There were some tricks to getting this code
* working:
*
* 1. You have to overwite addMouseListener() to do nothing
* 2. You have to add a mouse event on mousePressed by calling
* super.addMouseListener()
* 3. You have to replace the UIActionMap for the keyboard event
* "pressed" with your own one.
* 4. You have to remove the UIActionMap for the keyboard event
* "released".
* 5. You have to grab focus when the next state is entered,
* otherwise clicking on the component won't get the focus.
* 6. You have to make a TristateDecorator as a button model that
* wraps the original button model and does state management.
*/
public class TristateCheckBox extends JCheckBox {
/** This is a type-safe enumerated type */
public static class State { private State() { } }
public static final State NOT_SELECTED = new State();
public static final State SELECTED = new State();
public static final State DONT_CARE = new State();
private final TristateDecorator model;
public TristateCheckBox(String text, Icon icon, State initial){
super(text, icon);
// Add a listener for when the mouse is pressed
super.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
grabFocus();
model.nextState();
}
});
// Reset the keyboard action map
ActionMap map = new ActionMapUIResource();
map.put("pressed", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
grabFocus();
model.nextState();
}
});
map.put("released", null);
SwingUtilities.replaceUIActionMap(this, map);
// set the model to the adapted model
model = new TristateDecorator(getModel());
setModel(model);
setState(initial);
}
public TristateCheckBox(String text, State initial) {
this(text, null, initial);
}
public TristateCheckBox(String text) {
this(text, DONT_CARE);
}
public TristateCheckBox() {
this(null);
}
/** No one may add mouse listeners, not even Swing! */
public void addMouseListener(MouseListener l) { }
/**
* Set the new state to either SELECTED, NOT_SELECTED or
* DONT_CARE. If state == null, it is treated as DONT_CARE.
*/
public void setState(State state) { model.setState(state); }
/** Return the current state, which is determined by the
* selection status of the model. */
public State getState() { return model.getState(); }
public void setSelected(boolean b) {
if (b) {
setState(SELECTED);
} else {
setState(NOT_SELECTED);
}
}
/**
* Exactly which Design Pattern is this? Is it an Adapter,
* a Proxy or a Decorator? In this case, my vote lies with the
* Decorator, because we are extending functionality and
* "decorating" the original model with a more powerful model.
*/
private class TristateDecorator implements ButtonModel {
private final ButtonModel other;
private TristateDecorator(ButtonModel other) {
this.other = other;
}
private void setState(State state) {
if (state == NOT_SELECTED) {
other.setArmed(false);
setPressed(false);
setSelected(false);
} else if (state == SELECTED) {
other.setArmed(false);
setPressed(false);
setSelected(true);
} else { // either "null" or DONT_CARE
other.setArmed(true);
setPressed(true);
setSelected(true);
}
}
/**
* The current state is embedded in the selection / armed
* state of the model.
*
* We return the SELECTED state when the checkbox is selected
* but not armed, DONT_CARE state when the checkbox is
* selected and armed (grey) and NOT_SELECTED when the
* checkbox is deselected.
*/
private State getState() {
if (isSelected() && !isArmed()) {
// normal black tick
return SELECTED;
} else if (isSelected() && isArmed()) {
// don't care grey tick
return DONT_CARE;
} else {
// normal deselected
return NOT_SELECTED;
}
}
/** We rotate between NOT_SELECTED, SELECTED and DONT_CARE.*/
private void nextState() {
State current = getState();
if (current == NOT_SELECTED) {
setState(SELECTED);
} else if (current == SELECTED) {
setState(DONT_CARE);
} else if (current == DONT_CARE) {
setState(NOT_SELECTED);
}
}
/** Filter: No one may change the armed status except us. */
public void setArmed(boolean b) {
}
/** We disable focusing on the component when it is not
* enabled. */
public void setEnabled(boolean b) {
setFocusable(b);
other.setEnabled(b);
}
/** All these methods simply delegate to the "other" model
* that is being decorated. */
public boolean isArmed() { return other.isArmed(); }
public boolean isSelected() { return other.isSelected(); }
public boolean isEnabled() { return other.isEnabled(); }
public boolean isPressed() { return other.isPressed(); }
public boolean isRollover() { return other.isRollover(); }
public void setSelected(boolean b) { other.setSelected(b); }
public void setPressed(boolean b) { other.setPressed(b); }
public void setRollover(boolean b) { other.setRollover(b); }
public void setMnemonic(int key) { other.setMnemonic(key); }
public int getMnemonic() { return other.getMnemonic(); }
public void setActionCommand(String s) {
other.setActionCommand(s);
}
public String getActionCommand() {
return other.getActionCommand();
}
public void setGroup(ButtonGroup group) {
other.setGroup(group);
}
public void addActionListener(ActionListener l) {
other.addActionListener(l);
}
public void removeActionListener(ActionListener l) {
other.removeActionListener(l);
}
public void addItemListener(ItemListener l) {
other.addItemListener(l);
}
public void removeItemListener(ItemListener l) {
other.removeItemListener(l);
}
public void addChangeListener(ChangeListener l) {
other.addChangeListener(l);
}
public void removeChangeListener(ChangeListener l) {
other.removeChangeListener(l);
}
public Object[] getSelectedObjects() {
return other.getSelectedObjects();
}
}
}
My colleague who this question came from thought of this;
Create a dummy JCheckBox which is disabled and selected. set the same size as the real one.
Create an Icon which' paint method actually paints the dummy JCheckbox.
Set the original JCheckBox' Icon to the one painting the dummy.
Remove the icon as soon as the JCheckBox is clicked.
++ No overridden JCheckBox
-- not a real tri-state Combo
I think he's satisfied.
Thanks for the help