How to handle RowSorter-Synchronization in JTables with RowHeader? - java

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
}
}

Related

Listen jtable change

I have a problem with table model listener. It doesn't work, and I don't know why. I have tried different methods, and read a lot of questions here, but haven't found the solution.
I've read this: Listening to JTable changes and this Row refreshing when cell is edited
but it doesn't work.
I also have read this and this
but result is the same.
Here is my code. First of all definition of the table:
private void prepareTable(JTable table, Map<String, String> tableData, int colsCount, int rowsCount, int nGram) {
//Load data, set model, remove header
NGramsTableModel nGramModel = new NGramsTableModel(tableData, allowedSymbols, colsCount, rowsCount, nGram);
nGramModel.addTableModelListener(new NGramsTableListener());
table.setModel(nGramModel);
table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
table.setTableHeader(null);
//Set editor
JTextField jtf = new JTextField();
jtf.setDocument(new NGramsTableCellDocument(nGram));
table.setDefaultEditor(String.class, new DefaultCellEditor(jtf));
//Colorize rows
for (int i = 0; i < table.getColumnModel().getColumnCount(); i++) {
table.getColumnModel().getColumn(i).setCellRenderer(new NGramsTableCellRenderer());
}
}
Here is the model listener class:
public class NGramsTableListener implements TableModelListener {
#Override
public void tableChanged(TableModelEvent e) {
System.out.println("something changed...");
System.out.println(e);
}
}
And the table model class:
public class NGramsTableModel extends AbstractTableModel implements TableModel {
private Set<TableModelListener> listeners = new HashSet<TableModelListener>();
...
...
#Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
...
//it's OK, i see this message with entered symbols
System.out.println("setValueAt: " + aValue);
//I tried use every of this, but it doesn't work. A don't see any massage from NGramsTableListener class
fireTableCellUpdated(rowIndex, columnIndex);
fireTableDataChanged();
fireTableRowsInserted(rowIndex, columnIndex);
fireTableRowsUpdated(rowIndex, columnIndex);
}
#Override
public void addTableModelListener(TableModelListener l) {
listeners.add(l);
}
#Override
public void removeTableModelListener(TableModelListener l) {
listeners.remove(l);
}
}
Actually I need to get updated object with coords(rowIndex, colIndex), because later I want get updated object and object with coords(rowIndex-1, colIndex) if exists.
Where is my mistake?
Thanks
The AbstractTableModel already implements the table model listener methods. That is the benefit of extending AbstractTableModel. The solution to your problem is to get rid of all that code.
When you extend AbstractTableModel you are responsible for implementing the other methods of TableModel, like getColumnClass(), getValueAt(...), setValueAt(...) etc.
You need to provide a method which will fireXXX notified all registered listeners for example:
public class NGramsTableModel extends AbstractTableModel implements TableModel {
private LinkedList<TableModelListener> listeners = new LinkedList<TableModelListener>();
...
...
#Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
...
//it's OK, i see this message with entered symbols
System.out.println("setValueAt: " + aValue);
//Use your fireXXX method
fireNGramTableChanged();
}
#Override
public void addTableModelListener(TableModelListener l) {
listeners.add(l);
}
#Override
public void removeTableModelListener(TableModelListener l) {
listeners.remove(l);
}
protected void fireNGramTableChanged(){
for(TableModelListener next : listeners){
next.tableChanged(new TableModelEvent());
}
}
}

How to update a JSlider's maximum value without dispatching a change event

I have wee bit of code that updates a JSlider; and at another point in time, the JSlider's maximum value needs to updated. The problem is when I call setMaximum() on the slider, it also dispatches a ChangeEvent. To avoid that I'm doing this:
slider.removeChangeListener(this);
slider.setMaximum(newMax);
slider.addChangeListener(this);
Is there a cleaner/more elegant way of doing this?
A clean way might be (depending a bit on what you actually need) to implement a custom BoundedRangeModel which fires a custom ChangeEvent that can carry the actually changed properties:
/**
* Extended model that passes the list of actually changed properties
* in an extended changeEvent.
*/
public static class MyBoundedRangeModel extends DefaultBoundedRangeModel {
public MyBoundedRangeModel() {
}
public MyBoundedRangeModel(int value, int extent, int min, int max) {
super(value, extent, min, max);
}
#Override
public void setRangeProperties(int newValue, int newExtent, int newMin,
int newMax, boolean adjusting) {
int oldMax = getMaximum();
int oldMin = getMinimum();
int oldValue = getValue();
int oldExtent = getExtent();
boolean oldAdjusting = getValueIsAdjusting();
// todo: enforce constraints of new values for all
List<String> changedProperties = new ArrayList<>();
if (oldMax != newMax) {
changedProperties.add("maximum");
}
if (oldValue != newValue) {
changedProperties.add("value");
}
// todo: check and add other properties
changeEvent = changedProperties.size() > 0 ?
new MyChangeEvent(this, changedProperties) : null;
super.setRangeProperties(newValue, newExtent, newMin, newMax, adjusting);
}
}
/**
* Extended ChangeEvent that provides a list of actually
* changed properties.
*/
public static class MyChangeEvent extends ChangeEvent {
private List<String> changedProperties;
/**
* #param source
*/
public MyChangeEvent(Object source, List<String> changedProperties) {
super(source);
this.changedProperties = changedProperties;
}
public List<String> getChangedProperties() {
return changedProperties;
}
}
Its usage something like:
final JSlider slider = new JSlider();
slider.setModel(new MyBoundedRangeModel(0, 0, -100, 100));
ChangeListener l = new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
if (e instanceof MyChangeEvent) {
MyChangeEvent me = (MyChangeEvent) e;
if (me.getChangedProperties().contains("value")) {
System.out.println("new value: " +
((BoundedRangeModel) e.getSource()).getValue());
}
if (me.getChangedProperties().contains("maximum")) {
System.out.println("new max: " +
((BoundedRangeModel) e.getSource()).getMaximum());
}
} else {
// do something else or nothing
}
}
};
slider.getModel().addChangeListener(l);
Note that you have to register the listener with the model, not with the slider (reason being that the slider creates a new changeEvent of the plain type)
You could check who triggered the change in the listener. It's still pretty dirty but you won't have to remove the change listener.
It looks like the same problem, in essence, as this question. In which case, I fear the answer is "no"
if you just need your slider to fire events when value is changing you can simplify kleopatra's answer in the following way:
// make sure slider only fires changEvents when value changes
slider.setModel(new DefaultBoundedRangeModel() {
final ChangeEvent theOne=new ChangeEvent(this);
#Override
public void setRangeProperties(int newValue, int newExtent, int newMin,int newMax, boolean adjusting)
changeEvent= (getValue() != newValue ? theOne:null);
super.setRangeProperties(newValue, newExtent, newMin, newMax, adjusting);
}
#Override
protected void fireStateChanged()
{
if(changeEvent==null) return;
super.fireStateChanged();
}
});
Then all you need to do is register a "Standard" ChangeListener on the model and it will only get called when the value is changing not when maximum or minimum changes.
slider.getModel().addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
// do something here when value changes
});
});

Swing dependent components and event system

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.

Span all rows of a specific column of a JTable (to render a JTextPane)

I'd like to build a standard JTable but with all rows spanned on one specific column. So that column has to contain only one cell with a JTextPane as its renderer.
Do you know any simply way to do that?
NB: no third part software required.
Thanks.
Triggered by #MvG idea of some kind of overlay here's a proof-of-concept using JLayer (added to core in jdk7, for earlier versions, use JXLayer in SwingLabs which is very similar)
The basic ingredients:
a LayerUI which manages a textArea in its glassPane
a custom layoutManager which sizes/locates the textArea over a column
some listeners which force a re-layout of the glassPane
It's surprisingly straightforward to at least make it work. There are some rough edges, though:
as always, navigation in the table: when table has focus, its cell selection is moved under the textArea. Probably needs a custom selectionModel
during the move of a column, the table rows under the column are visible
?? probably another bunch of devils, after all we are confusing the ui :-)
Some code:
public static class RowSpanUI extends LayerUI<JTable> {
public static String COLUMN_TO_SPAN_KEY = "Table.columnToSpan";
private JLayer layer;
private JTextArea area;
#Override
public void installUI(JComponent c) {
super.installUI(c);
this.layer = (JLayer) c;
installTextArea();
installListeners();
}
#Override
public void doLayout(JLayer<? extends JTable> l) {
super.doLayout(l);
l.getGlassPane().doLayout();
}
private void installTextArea() {
area = new JTextArea(10, 20);
layer.getGlassPane().setBorder(BorderFactory.createLineBorder(Color.RED));
layer.getGlassPane().setLayout(new ColumnLayoutManager(this));
layer.getGlassPane().add(area);
layer.getGlassPane().setVisible(true);
}
public JTable getView() {
return (JTable) layer.getView();
}
public int getViewColumnToSpan() {
Object clientProperty = getView().getClientProperty(COLUMN_TO_SPAN_KEY);
if (clientProperty instanceof Integer) {
return getView().convertColumnIndexToView((int) clientProperty);
}
return -1;
}
/**
* Install listeners to manually trigger a layout of the glassPane.
* This is incomplete, just the minimum for demonstration!
*/
protected void installListeners() {
ComponentListener compL = new ComponentListener() {
#Override
public void componentShown(ComponentEvent e) {
doLayout(layer);
}
#Override
public void componentResized(ComponentEvent e) {
doLayout(layer);
}
#Override
public void componentMoved(ComponentEvent e) {
doLayout(layer);
}
#Override
public void componentHidden(ComponentEvent e) {
// TODO Auto-generated method stub
}
};
layer.addComponentListener(compL);
TableColumnModelListener columnL = new TableColumnModelListener() {
#Override
public void columnRemoved(TableColumnModelEvent e) {
doLayout(layer);
}
#Override
public void columnMoved(TableColumnModelEvent e) {
doLayout(layer);
}
#Override
public void columnMarginChanged(ChangeEvent e) {
doLayout(layer);
}
#Override
public void columnAdded(TableColumnModelEvent e) {
doLayout(layer);
}
#Override
public void columnSelectionChanged(ListSelectionEvent e) {
}
};
getView().getColumnModel().addColumnModelListener(columnL);
}
}
public static class ColumnLayoutManager implements LayoutManager {
private RowSpanUI ui;
public ColumnLayoutManager(RowSpanUI ui) {
this.ui = ui;
}
#Override
public void layoutContainer(Container parent) {
Component child = parent.getComponent(0);
child.setBounds(getColumnBounds());
}
#Override
public Dimension preferredLayoutSize(Container parent) {
return ui.getView().getSize();
}
protected Rectangle getColumnBounds() {
int viewColumn = ui.getViewColumnToSpan();
if (viewColumn < 0) {
return new Rectangle();
}
Rectangle r = ui.getView().getCellRect(0, viewColumn, false);
r.height = ui.getView().getHeight();
return r;
}
#Override
public Dimension minimumLayoutSize(Container parent) {
return preferredLayoutSize(parent);
}
#Override
public void addLayoutComponent(String name, Component comp) {
}
#Override
public void removeLayoutComponent(Component comp) {
}
}
// usage
JTable table = new JTable(myModel);
table.putClientProperty(RowSpanUI.COLUMN_TO_SPAN_KEY, 2);
JLayer layer = new JLayer(table, new RowSpanUI());
The rendering of a JTable, including the clipping to the cell rectangle, is buries deep inside the user interface implementations of the pluggable look and feel in question. Changing stuff there is going to be messy business, and highly dependent on the actual JFC implementation.
So instead I'd suggest drawing the cells of those roes any way you like, and having another transparent pane on top of the table to overlay those areas with the spanned content you describe. Still messy, probably quite a bit of work, but more likely to be portable.

Controlled editing of a row selection in JTable

I have a JTable displaying rows from an SQL database. The table is relatively small (only 4 columns and up to 1000 rows).
I would like to give the user the opportunity to edit any cells in the table but want to avoid restricting it so much so that they must use an edit dialog box (this makes for far easier error checking and validation but is less intuitive)
I have tried a few different ways of controlling edit selections using the valueChanged method of my JTable but haven't had much luck.
I would like each row to be edited and written to the database at the conclusion of editing. I would like that once a cell has been clicked to start the editing of that row, no other rows can be selected until the user has finished editing the row (other rows are grayed out). After editing each cell and pressing enter, the edit selection should jump to the next column in the same row.
Can anyone give pointers on how I can achieve this?
// Create table with database data
table = new JTable(new DefaultTableModel(data, columnNames)) {
public Class getColumnClass(int column) {
for (int row = 0; row < getRowCount(); row++) {
Object o = getValueAt(row, column);
if (o != null){
return o.getClass();
}
}
return Object.class;
}
#Override
public boolean isCellEditable(int row, int col){
return true;
}
#Override
public boolean editCellAt(int row, int column) {
boolean ans = super.editCellAt(row, column);
if (ans) {
Component editor = table.getEditorComponent();
editor.requestFocusInWindow();
}
return ans;
}
#Override
public void valueChanged(ListSelectionEvent source) {
super.valueChanged(source);
if (table!=null)
table.changeSelection(getSelectedRow(), getSelectedColumn()+1, false, false);
}
};
Edit - custom cell editor with table pointer seems to be a start
public class ExchangeTableCellEditor extends AbstractCellEditor implements TableCellEditor {
private JTable table;
JComponent component = new JTextField();
public ExchangeTableCellEditor(JTable table) {
this.table = table;
}
public boolean stopCellEditing() {
boolean ans = super.stopCellEditing();
//now we want to increment the cell count
table.editCellAt(table.getSelectedRow(), table.getSelectedColumn()+1);
return ans;
}
#Override
public void cancelCellEditing() {
//do nothing... must accept cell changes
}
#Override
public Object getCellEditorValue() {
return ((JTextField)component).getText();
}
#Override
public Component getTableCellEditorComponent(JTable arg0, Object value,
boolean arg2, int arg3, int arg4) {
((JTextField)component).setText((String)value);
return component;
}
}
The default renderer and editor is typically adequate for most data types, but you can define custom renderers and editors as needed.
Addendum: I'm unfamiliar with the approach shown in your fragment. Instead, register a TableModelListener with your model, as shown below, and update the database with whatever granularity is warranted. See also How to Use Tables: Listening for Data Changes.
Addendum: #kleopatra is correct about your TableCellEditor. One convenient way to notify listeners is to invoke the super implementation, as shown here. Note that the delegate invokes fireEditingStopped().
/** #see https://stackoverflow.com/questions/9155596 */
public class NewJavaGUI extends JPanel {
private final JTable table;
public NewJavaGUI() {
String[] colNames = {"C1", "C2", "C3"};
DefaultTableModel model = new DefaultTableModel(colNames, 0) {
#Override
public boolean isCellEditable(int row, int col) {
// return your actual criteria
return true;
}
#Override
public Class getColumnClass(int col) {
// return your actual type tokens
return getValueAt(0, col).getClass();
}
};
// Add data; note auto-boxing
model.addRow(new Object[]{"A1", "A2", 42});
model.addRow(new Object[]{"B1", "B2", 42d});
model.addTableModelListener(new TableModelListener() {
#Override
public void tableChanged(TableModelEvent e) {
// DML as indicated
}
});
table = new JTable(model);
this.add(table);
}
private void display() {
JFrame f = new JFrame("NewJavaGUI");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(this);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new NewJavaGUI().display();
}
});
}
}
The behaviour you mention can be achieved by forcing your table to start editing again.
First make sure you now yourRow and Column and that you add your own tablecelleditor that extands from the AbstractCellEditor
then add this to your stopCellEditing method:
EventQueue.invokeLater(new Runnable()
{
public void run()
{
yourTable.editCellAt( yourRow, yourColumn+1);
}
});

Categories