Skip to main content

JXDatePicker - Cell Editor - Losing Focus

24 replies [Last post]
java2cool
Offline
Joined: 2010-09-03

I've read other topics concerning JXDatePicker and losing focus, but I don't think any of them address exactly what I need. I need some advice on how to handle this scenario.

I have a window with a tabbed interface. Each tab has fields for data entry. Some tabs have just components, and others have JTables. For the tabs with JTables, I am trying to change the date cell editors. I'm currently using just a JFormattedTextField, but I'm trying to change to JXDatePicker. I have a custom cell editor, and everything works fine except for tracking the losing of focus.

What I need to do is validate the date in the JXDatePicker before the component loses focus. If the user clicks on another cell, the validation works fine because the validation is in stopCellEditing(). The problem is if a user clicks on another tab. The JXDatePicker loses focus, but the JTable does not stop editing. I was using a focus listener to force the JTable to stop editing if the editor component lost focus, but I can't do that with JXDatePicker.

Does anyone have any suggestion on how I can force the JTable to stop editing? Am I missing something obvious here? There are more scenarios than just clicking on another tab too.

Thanks in advance for your help.

Reply viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
java2cool
Offline
Joined: 2010-09-03

walterln, thanks for your help. I really thought that "terminateEditOnFocusLost" was going to work, but just based on a quick test, I had two problems.

1. If I enter an invalid date into the table and click on another tab, stopCellEditing() with my custom error checking fires. What is supposed to happen is that an error message is displayed and the focus is returned to the cell editor. With terminateEditOnFocusLost, the error displays, but the focus is changed to the other tab and the changes are lost.

2. If I enter an invalid date into the table and click on another cell, the stopCellEditing() fires and the focus remains on the invalid date. The problem is when I try to use the JXDatePicker. If I use the calendar to scroll to another date, the table seems to lose focus. I abruptly stop cell editing and my changes are lost.

There may be ways around these problems. I just did a quick test because I thought that this might be a simple solution to my problems.

I haven't tried using the JXTable or DatePickerCellEditor. These components may handle these operations better. I just haven't had a chance to explore using them yet.

java2cool
Offline
Joined: 2010-09-03

I forgot to mark this as answered. Thanks to everyone who helped!

java2cool
Offline
Joined: 2010-09-03

Karl: Thanks. I haven't had a chance to look at your validation mechanism yet, but I will.

Jeanette: No offense taken. ;-) I read the other topics on this forum about the pitfalls of using the focus listeners. I'm just not sure how else to make the software behave the way my customer wants. I can always do the validations when the user saves the record (and I do in certain cases), but from a usability standpoint, it's nicer to show errors right away, which is typically when the user attempts to move to another field. Fortunately, this is the first time I've had to deal with validating using compound components. The only other compound component I use currently is JComboBox, but that doesn't need to be validated because I only present the valid options to the user. One thing that I didn't understand about your suggestion was: when does commitChange() get called? When I tried to test it in my sample program, it didn't get called when I switched tabs.

To All: I think that I may have overlooked a simple solution. Everything works except when I switch tabs. So, why not force the tables to stop editing when I switch tabs? I added a change listener to my sample program. When I switch a tab, I force the tables in the other tabs to stop editing. If the stopCellEditing() fails, the focus is switched back to the offending cell. It seems to work in my sample program. I'm not sure if it will work in my real program because my data validation is much more elaborate. It includes message boxes to prompt the user to verify changes that look suspicious. I need to make sure that the message boxes don't trigger a second data validation and prompt the user twice.

kleopatra
Offline
Joined: 2003-06-11

my code works for me (replacing in your code). If it doesn't for you, it's your job to find why not ;-)

good luck
Jeanette

java2cool
Offline
Joined: 2010-09-03

I think I finally put together a workable sample that will illustrate what I'm doing. I don't usually code this way, but I put together a self-contained program with everything in one class.

You should be able to copy this code, compile it, and run it.

If you type a date in the future in the first table and click on another tab, it will not let you out of the first table. This implements the old way that I was using date editing.

The second table illustrates how I was trying to replace my text field with a JXDateEditor. If you type a date in the future and click on another tab, it allows you to leave the second table even though the date is invalid. If you type a date in the future and click on another cell, the focus stays in the original cell, as it should.

The third table is a quick attempt to use the DatePickerCellEditor. I must be doing something wrong there. If you type a date in the future and click on another tab, it allows you to leave the third table even though the date is invalid. If you type a date in the future and try to go to another cell, the editor goes crazy because it keeps losing and regaining focus. I'm not sure what is happening on that one.

I thought about adding a fourth table to try out JXTable, but the demo program was already getting too big.

Please let me know if you have any problems running this sample. Thanks for your help.

[code]
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import javax.swing.*;
import java.util.*;
import javax.swing.table.*;
import org.jdesktop.swingx.*;
import org.jdesktop.swingx.table.*;

public class TestFrame extends JFrame {

private Date today = new Date(); //Today's date - for validation.
private JLabel myStatus; //JLabel to show errors in date validations.
private JTabbedPane myTabs; //Tabs for showing lostFocus behavior.
private JPanel firstPanel;
private JPanel secondPanel;
private JPanel thirdPanel;
private JTable firstTable;
private JTable secondTable;
private JTable thirdTable;
private JFormattedTextField myDateField;
private JXDatePicker myDatePicker;
private JXDatePicker innerDatePicker;
private MyFocusListener myFocusListener;

public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new TestFrame().setVisible(true);
}
});
}

public TestFrame() {
//Exit on close.
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

//Create the main panel.
JPanel mainPanel = new JPanel(new BorderLayout());
getContentPane().add(mainPanel);

//Create the status message label.
myStatus = new JLabel("Ready!", JLabel.CENTER);
mainPanel.add(myStatus, BorderLayout.PAGE_START);

//Create the tabbed pane.
myTabs = new JTabbedPane();
mainPanel.add(myTabs, BorderLayout.CENTER);

//Create the first tab.
firstPanel = new JPanel(new BorderLayout());
myTabs.addTab("JFormattedTextField", firstPanel);

//Create the second tab.
secondPanel = new JPanel(new BorderLayout());
myTabs.addTab("JXDatePicker", secondPanel);

//Create the third tab.
thirdPanel = new JPanel(new BorderLayout());
myTabs.addTab("DatePickerCellEditor", thirdPanel);

//Initialize the focus listener.
myFocusListener = new MyFocusListener();

//Add the tables.
addTables();

//Set the size of the frame.
setSize(700, 700);
}

//Add the tables to the tabs.
private void addTables() {
//Add the first table.
firstTable = new JTable(new MyTableModel());
firstPanel.add(firstTable, BorderLayout.CENTER);
firstTable.setDefaultEditor(Date.class, new DateCellEditor());

//Add the second table.
secondTable = new JTable(new MyTableModel());
secondPanel.add(secondTable, BorderLayout.CENTER);
secondTable.setDefaultEditor(Date.class, new JXDatePickerEditor());

//Add the third table.
thirdTable = new JTable(new MyTableModel());
thirdPanel.add(thirdTable, BorderLayout.CENTER);
thirdTable.setDefaultEditor(Date.class, new CustomDatePickerCellEditor());
}

//Stop a table editing.
private void stopTableEditing(JTable table, Component comp, int tabIndex) {
//Check if the table is editing.
if (table.isEditing()) {
TableCellEditor editor = table.getCellEditor(); //Get the cell editor.

//Try to stop cell editing. If it fails, return focus to the component.
if (!editor.stopCellEditing()) {
System.out.println("Requesting focus...");
myTabs.setSelectedIndex(tabIndex);
comp.requestFocusInWindow();
}
}
}

//Validate a date.
private boolean validateDate(Date myDate) {
if (myDate != null) {
if (myDate.after(today)) {
myStatus.setText("The date cannot be after today!");
return false;
} else {
myStatus.setText("The date is fine.");
return true;
}
}

return true;
}

//Inner class for the focus listener.
private class MyFocusListener implements FocusListener {

public void focusGained(final FocusEvent e) {
//Nothing to do.
}

public void focusLost(FocusEvent e) {
//Stop cell editing on lost table focus.
if (e.getComponent() == myDateField) {
System.out.println("Date field lost focus.");
stopTableEditing(firstTable, myDateField, 0);
} else if (e.getComponent() == myDatePicker) {
System.out.println("Date picker lost focus.");
stopTableEditing(secondTable, myDatePicker, 1);
} else if (e.getComponent() == innerDatePicker) {
System.out.println("Date picker cell editor lost focus.");
stopTableEditing(thirdTable, innerDatePicker, 2);
}
}
}

//Inner class for the table column model.
private class MyTableModel extends AbstractTableModel {

private String[] columnNames = {"Date 1",
"Date 2",
"Date 3",
"Date 4",
"Date 5"};
private Object[][] data = new Date[5][5];

@Override
public String getColumnName(int col) {
return columnNames[col];
}

@Override
public Class getColumnClass(int c) {
//All classes will be dates.
return Date.class;
}

@Override
public int getColumnCount() {
return columnNames.length;
}

@Override
public int getRowCount() {
return data.length;
}

@Override
public Object getValueAt(int intRow, int intCol) {
return data[intRow][intCol];
}

@Override
public void setValueAt(Object value, int row, int col) {
data[row][col] = value;
fireTableCellUpdated(row, col);
}

@Override
public boolean isCellEditable(int intRow, int intCol) {
//All cells are editable.
return true;
}
}

//Inner class for date cell editor.
private class DateCellEditor extends DefaultCellEditor {

//Constructor
DateCellEditor() {
super(new JFormattedTextField(new SimpleDateFormat("MM/dd/yyyy")));
myDateField = (JFormattedTextField) getComponent();
myDateField.addFocusListener(myFocusListener);
}

@Override
//Override stopCellEditing() to include date check.
public boolean stopCellEditing() {
JFormattedTextField field = (JFormattedTextField) getComponent();

try {
field.commitEdit();
} catch (Exception ex) {
System.out.println("Could not commit changes.");
}

if (!validateDate((Date) field.getValue())) {
return false;
}

return super.stopCellEditing();
}

@Override
public Object getCellEditorValue() {
JFormattedTextField field = (JFormattedTextField) getComponent();

Object value = field.getValue();

if (value == null) {
return null;
} else if (value instanceof java.util.Date) {
return value;
} else {
try {
SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy");
return format.parse(value.toString());
} catch (ParseException ex) {
System.out.println("Could not parse date.");
return null;
}
}
}

@Override
public Component getTableCellEditorComponent(JTable table,
Object value, boolean isSelected, int row, int column) {

JFormattedTextField field =
(JFormattedTextField) super.getTableCellEditorComponent(
table, value, isSelected, row, column);

field.setValue(value);

return field;
}
}

//Inner class for date cell editor.
private class JXDatePickerEditor extends AbstractCellEditor implements TableCellEditor {

//Constructor
JXDatePickerEditor() {
myDatePicker = new JXDatePicker();
myDatePicker.setFormats("MM/dd/yyyy");
//datePicker.addFocusListener(myFocusListener);
myDatePicker.getEditor().addFocusListener(myFocusListener);
}

@Override
//Override stopCellEditing() to include date check.
public boolean stopCellEditing() {
JFormattedTextField field = myDatePicker.getEditor();

try {
field.commitEdit();
} catch (Exception ex) {
System.out.println("Could not commit changes.");
}

if (!validateDate((Date) field.getValue())) {
return false;
}

return super.stopCellEditing();
}

@Override
public Object getCellEditorValue() {
JFormattedTextField field = myDatePicker.getEditor();

Object value = field.getValue();

if (value == null) {
return null;
} else if (value instanceof java.util.Date) {
return value;
} else {
try {
SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy");
return format.parse(value.toString());
} catch (ParseException ex) {
System.out.println("Could not parse date.");
return null;
}
}
}

public Component getTableCellEditorComponent(JTable table,
Object value, boolean isSelected, int row, int column) {

if (value != null) {
myDatePicker.setDate((Date) value);
} else {
myDatePicker.setDate(null);
}

return myDatePicker;
}
}

//Inner class to implement the DatePickerCellEditor.
private class CustomDatePickerCellEditor extends DatePickerCellEditor {
//Constructor

CustomDatePickerCellEditor() {
super(new SimpleDateFormat("MM/dd/yyyy"));
innerDatePicker = datePicker;
innerDatePicker.addFocusListener(myFocusListener);
}

@Override
//Override stopCellEditing() to include date check.
public boolean stopCellEditing() {
if (!validateDate(getCellEditorValue())) {
return false;
}

return super.stopCellEditing();
}
}
}
[/code]

edited to format the code - insert [ code ] ... [ /code ] tags

Message was edited by: kleopatra

kleopatra
Offline
Joined: 2003-06-11

no offense meant but ... your code's a good example of some reasons why focusListener is a bad idea

- it's utterly useless for compound components (like JXDatePicker): your focusListener always checks against the compound
- even with the compoundness in mind and knowledge about the compound's internals, it's hard to remember always: in JXDatePickerCellEditor, you register it with the picker's editor, in CustomDatePickerEditor you register it with the picker itself
- it's hard to interpret the focusEvent: a compound component may decide to move the focus around its subcomponents, if it thinks doing so appropriate. In JXDatePicker the monthView can get the focus while the editor is still editing.

So repeating myself: stay clear off using FocusListener. That rule holds always, but especially for editing components in a collection view: the focus transfers are extremely ill-defined.

Instead, build some kind of validation error notification (and processing). In an ideal world, that would be supported out of the box in core .. sigh. Below is an little outline (very raw and not re-usable) - you can try to go from there

[code]

private void addTables() {
....

//Add the third table.
thirdTable = new JXTable(new MyTableModel());
// if using JTable, we need to set the terminateEditOnFocusLost client property
// thirdTable = new JTable(new MyTableModel());
// thirdTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
thirdPanel.add(new JScrollPane(thirdTable), BorderLayout.CENTER);
thirdTable.setDefaultEditor(Date.class, new CustomDatePickerCellEditor());
}

public void validationError(final CellContent cellContent) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
int tab = findTab(cellContent.table);
myTabs.setSelectedIndex(tab);
if (cellContent.table.isEditing()) return;
cellContent.table.requestFocusInWindow();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
cellContent.table.editCellAt(cellContent.row, cellContent.column);
cellContent.table.getEditorComponent().requestFocusInWindow();
}
});
}
});
}

private int findTab(JTable table) {
for (int i = 0; i < myTabs.getTabCount(); i++) {
Container container = (Container) myTabs.getComponentAt(i);
if (SwingUtilities.isDescendingFrom(table, container)) return i;
}
return -1;

}

//Inner class to implement the DatePickerCellEditor with validation.
private class CustomDatePickerCellEditor extends DatePickerCellEditor {
//Constructor

private CellContent cellContent;

/**
* @inherited

*/
@Override
public Component getTableCellEditorComponent(JTable table,
Object value, boolean isSelected, int row, int column) {
this.cellContent = new CellContent(table, row, column);
return super.getTableCellEditorComponent(table, value, isSelected, row, column);
}

/**
* @inherited

*/
@Override
protected boolean commitChange() {
boolean committed = super.commitChange();
if (committed) {
boolean valid = validateDate(datePicker.getDate());
if (!valid) {
committed = false;
cellContent.setInvalidValue(datePicker.getDate());
validationError(cellContent);
}
}
return committed;
}

}

private static class CellContent {

final public int column;
final public int row;
final public JTable table;
private Object invalidValue;

public CellContent(JTable table, int row, int column) {
this.table = table;
this.row = row;
this.column = column;
}

public void setInvalidValue(Object value) {
this.invalidValue = value;
}

public Object getInvalidValue() {
return invalidValue;
}
}

[/code]

Basically, the editor checks whether the value it is about to commit is valid, if not, notify some interested party about the context of the invalid input and then simply refuse to stopEditing. The interested party here is a simple callback method which tries its best to stay in editing mode and only if that's not possible, re-start the edit. The nested invoke looks funny, but both are needed: the first to undo any triggered focustransfers, the second to wait until it's safe to restart an edit.

HTH
Jeanette

kschaefe
Offline
Joined: 2006-06-08

My [url=https://jdnc-incubator.dev.java.net/source/browse/jdnc-incubator/trunk/src/kschaefe/validator/]incubator[/url] contains a simple validation mechanism. That is highly reusable and should serve your purpose.

Karl

kleopatra
Offline
Joined: 2003-06-11

Karl,

hmm .. not sure: as far as I can see it supports a hook for validators (which is nice). The harder part is what to do if a validation error happens. Could implement a kind of "active" validator but that's quite some work (there are whole frameworks out there trying :-) plus it would need contextual information.

CU
Jeanette

kschaefe
Offline
Joined: 2006-06-08

All of them currently do the same thing, make the border red and prevent the cell editing from stopping. Same as standard editor error notification. Would need a hook to allow more elaborate messaging. I could provide that.

Karl

kleopatra
Offline
Joined: 2003-06-11

Karl,

what's needed is not a more elaborate messaging (or maybe it is, kind of - if the message is a rich enough Object :-) - it's a hook that allows application code to react to that error notification as it deems necessary. In this thread, the requirement was to go back to offending cell and keep/start editing again. To be able to do so, the hook needs to provide some context of where the error happened, what caused it and probably more. That's what I tried to simulate with the CellContent in my code above (JGoodies Validation has some elaborate mechansm to illustrate what's needed)

I think we need two thingies
- the Validator, that's the small coin, can be as simple as taking the value to check for validity (that's what your incubator code is providing)
- the error notification: some mechanism to tell the world enough so that the world can do something reasonable about it

Just loud thinking: one option might be to enhance the CellEditorListener with an additional method which carries a dedicated CellEditorEvent which has appropriate context information:

[code]
// re-use CellContext - enrich to carry error info
class CellContext {
/**
* returns the value which the user wanted to set but was decided by the
* Validator to be invalid
*/
public Object getInvalidValue() {
return invalidValue;
}

public void setInvalidValue(Object invalidValue) {
this.invalidValue = invalidValue;
}

}

class CellEditorEvent extends EventObject {

public CellEditorEvent(Object source, CellContext errorContext) {
super(source)
this.errorContext = errorContext;
}
// re-use CellContext -
public CellContext getErrorContext() {
return errorContext;
};
}

public interface CellEditorListenerExt extends CellEditorListener {

void editingError(CellEditorEvent evt);
}

// concrete editors, can wrap around the real editor as you do it

public class ValidatingCellEditor implements TableCellEditor {

TableCellContext context;

public Component getXXCellEditorComponent(JTable table, ... otherParameters....) {
context = new TableCellContext();
context.install(table, .... otherParameters)
....
}

boolean stopCellEditing() {
if (isValidValue()) {
return cleanup(delegate.stopCellEditing());
}
context.setInvalidValue(delegate.getCellEditorValue()) ;
fireEditingError(...);
// dor error visuals
return false;

}
}
[/code]

Not sure if I would understand myself if I read this again . Need to go back to work right now, maybe I'll find time to play a bit at the weekend

CU
Jeanette

kschaefe
Offline
Joined: 2006-06-08

Jeanette,

Yeah, by more elaborate messaging, I really meant some kind of notification hook. You're spot on with what direction I was going, though I hadn't thought to add info to the CellContext. What's the advantage of that? Shouldn't the listener be able to work with just a simple notification containing the bad data?

Karl

kleopatra
Offline
Joined: 2003-06-11

Karl,

with only the bad value how would you re-start the edit in the cell which produce it? At a minimum, you'd need the cell-coordinates and the target component, I think. We already have the CellContext which has it, so would be tempted to reuse

CU
Jeanette

kschaefe
Offline
Joined: 2006-06-08

Jeanette,

The edit never stops, at least with my current implementation. The users focus doesn't leave the cell, so there is no need to restart the editing process.

Karl

kleopatra
Offline
Joined: 2003-06-11

Karl,

it does: change your example to contain a focusable component and click on it :-)

CU
Jeanette

kschaefe
Offline
Joined: 2006-06-08

Ugh. Didn't check that.

Karl

kleopatra
Offline
Joined: 2003-06-11

:-)

Anyway, requirements might be so that we need support the other way round: generally, it's considered bad usability style to force users to stay inside a component. So an advanced table (or other component with cell editors) with support for validation should be allowed to not keep the editing alive but instead simply mark the cell as attemped-invalid input and implement alternative paths to correct the error.

CU and have a nice weekend
Jeanette

java2cool
Offline
Joined: 2010-09-03

I've been loosely keeping track of this part of the conversation. I'd like to just add my $0.02 as an application developer. Whether or not the focus is allowed to leave the component is dependent on the individual business cases.

In my application, there are some validations that are so complex that it would be very confusing to the user if I allowed them to leave the field if their input is incorrect. What's more confusing is that I'm using a tabbed interface. If they click on another tab while there is an error, they have to go back to the original tab to find the error. If they continued working with the error still in place and tried to save the record, they'd have to get another error message and go back to fix the error. They could enter multiple bad items, which means on save, I'd either have to compile a list of errors, or the user would have to keeping clicking "Save" and fixing each error one-by-one.

I'm not sure I'm making sense. It's hard to explain without knowing my application. I guess what I'm trying to say is that if I were using someone else's validator, I'd want the choice of whether or not to leave the field when the edit is invalid. Possibly the validator could let me set a flag, but this might be on a cell-by-cell case. At the very least, I should be able to override the validator to force focus back onto the bad field in the case of invalid data.

walterln
Offline
Joined: 2007-04-17

You might be looking for jTable.putClientProperty("terminateEditOnFocusLost", true);. Not sure if JXTable does anything with that by default.

kleopatra
Offline
Joined: 2003-06-11

> You might be looking for
> jTable.putClientProperty("terminateEditOnFocusLost",
> true);. Not sure if JXTable does anything with that
> by default.

indeed, JXTable has a truely bound property terminateEditOnFocusLost which is true by default :-)

to OP: funny attitude - you see your custom editor not working and at the same don't see any advantage in using the pre-defined and tested DatePickerEditor? That said, there are a couple of glitches out there (may well be the same for the swingx editor):

- focusListeners are useless when it comes to cellEditors: the editing components might not even ever have focus
- tabPane has its own idea about focus traversal (read: there are contexts where the former focus owner is still the focusOwner after selecting another tab)
- JXTable has a much more elaborate implementation for CellEditorRemover (aka: make sure the edit is terminated correctly) for terminateFocusLost than core
- validation of cell edits is poorly supported, even in SwingX

In your shoes, I would follow Karl's suggestion and provide a small ... bla .. bla ;-)

CU
Jeanette

java2cool
Offline
Joined: 2010-09-03

Jeannette, thanks for your help.

I didn't intend to imply that there wouldn't be any advantage to using DatePickerCellEditor. I just meant that it didn't seem to help my particular problems. My custom cell editor is very complex, particularly the data validation, and I was trying to touch the code as little as possible. I was hoping that just switching out the JFormattedTextField for a JXDatePicker would do the trick. It works great, except for this one problem.

I think I understand what you are saying about the focus problems. I had it working when I was just using a JFormattedTextField for the dates. As you know, the JXDatePicker is a complex component, so using a focus listener doesn't work anymore. The tabs are really the problem. Everything else works without those.

I'm trying to put together a sample program. I need to make it simple enough to post, but complex enough to illustrate my problems.

There are probably some cool features in SwingX that I could use. The users complain the most about the date fields, and I was focusing on just changing that. I'll have some more time to explore the rest of the SwingX features later.

kschaefe
Offline
Joined: 2006-06-08

Are you using DatePickerCellEditor?

Karl

java2cool
Offline
Joined: 2010-09-03

No, I am using my own custom cell editor because I have very complex data validation to do. I thought it would be easier to just replace the component of my cell editor. I suppose I could use the DatePickerCellEditor. I just started using these libraries a few days ago, and I didn't see any advantage of using that editor because I would have to override a lot of it anyway. I would welcome any advise on the subject.

Thanks for your response.

kschaefe
Offline
Joined: 2006-06-08

Can you please post a small, runnable test case?

Karl

java2cool
Offline
Joined: 2010-09-03

Karl, thanks for your help. I haven't yet had a chance to put together a sample program yet, but I will.