Basically the problem here is as follow, I have this TableCellRenderer:
public class CellRenderer_LocalDate extends DefaultTableCellRenderer implements TableCellRenderer {
private static final long serialVersionUID = -9184414041940041458L;
String displayFormat;
public CellRenderer_LocalDate(String dateFormat) {
this.displayFormat = dateFormat;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
if (value instanceof LocalDate) {
LocalDate date = (LocalDate) value;
value = date.format(DateTimeFormatter.ofPattern(displayFormat));
}
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
}
}
And this function to search a JTable:
/**
* Filters the table to show a specific text
*/
public void searchTable(JTextField searchTextField) {
// Get model & instantiate table sorter
DefaultTableModel tableToFilter = (DefaultTableModel) this.getModel();
TableRowSorter<DefaultTableModel> trSorter = new TableRowSorter<DefaultTableModel>(tableToFilter);
// Get text to search & Set row filter for this table
String search = searchTextField.getText();
// Filter table in correct format
trSorter.setRowFilter(RowFilter.regexFilter("(?i)" search)); // Insensitive to upper case or lower case
this.setRowSorter(trSorter);
}
I have a column in the JTable that holds LocalDate in it. I made TableCellRenderer to display LocalDate column as dd.mm.yyyy. It works the way it should.
The problem is that when I use search function in format dd.mm.yyyy it does not show me any rows that match the way LocalDate is displayed in JTable.
An example: I have a column that holds LocalDate 2022-05-10. Due to the TableCellRenderer it is shown as 10.05.2022. Now if I use search and look for 10.05.2022 nothing will be shown as if nothing matches. But if I write in search field 2022-05-10 it will immediately show me the correct result. Please, what should I change in my functions to fix this bug?
CodePudding user response:
Do not use CellRenderers for changing the text. CellRenderers should be used for things like colors, fonts, borders, and images.
A CellRenderer is just a visual painting of a cell’s value. JTable cannot look at pixels and know what text they contain.
To make JTable’s sorting and filtering work, you need to return a wrapper object whose toString()
method returns your formatted data. For example:
public class DisplayableDate
implements Comparable<DisplayableDate> {
private final LocalDate value;
private final String text;
public DisplayableDate(LocalDate value,
String text) {
this.value = value;
this.text = text;
}
@Override
public String toString() {
return text;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof DisplayableDate) {
DisplayableDate other = (DisplayableDate) obj;
return Objects.equals(this.value, other.value);
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(value);
}
@Override
public int compareTo(DisplayableDate other) {
if (this.value == null) {
return other.value == null ? 0 : -1;
} else if (other.value == null) {
return 1;
} else {
return this.value.compareTo(other.value);
}
}
}
You would place instances of the above class in your TableModel instead of plain LocalDate objects. For instance:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(displayFormat);
tableModel.addRow(new Object[] {
record.getName(),
new DisplayableDate(record.getDate(),
formatter.format(record.getDate()))
});
Because DisplayableDate implements Comparable, the RowSorter will know how to sort it. Because filters look at the toString value of each cell, table filtering will use the text that happens to be the text displayed.
CodePudding user response:
So, based on this example, I dove into the Java source code and extracted the RowFilter.dateFilter
and modified it to support LocalDate
directly. This means, you get all the features and functionality of RowFilter.dateFilter
, but with LocalDate
instead of Date
.
Caveat
If I was providing a searchable solution for a mixed series of values, I would design a dedicated "filter component" which provided the mechanisms for allowing a user to configure the filter them selves. In this case, that would mean allowing the user to, independently, configure filter parameters for the date values (ie, included/exclude a given range, filter by month or year or straight up, only this day)
This would then build the RowSorter
accordingly, based on the user preferences
Runnable example
The following example is overly simplistic and is intended only a demonstration of how you could customise a RowSorter
to work with different data types.
To search by a date value, you must supply a valid date in the format of dd/MM/yyyy
.
The example is making use of RowFilter.orFilter
, so it will either match the LocalDateFilter
OR the RowFilter.regexFilter
Again, this is intended as a demonstration only.
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.RowFilter;
import javax.swing.RowFilter.ComparisonType;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableRowSorter;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private JTable table;
private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
public TestPane() {
setLayout(new BorderLayout());
table = new JTable();
DefaultTableModel model = new DefaultTableModel(
new Object[][]{
{"A", 1, LocalDate.parse("12/05/2000", formatter)},
{"B", 2, LocalDate.parse("12/06/2000", formatter)},
{"C", 3, LocalDate.parse("12/07/2000", formatter)},
{"D", 4, LocalDate.parse("12/08/2000", formatter)},
{"E", 5, LocalDate.parse("12/09/2000", formatter)},
{"F", 1, LocalDate.parse("12/10/2000", formatter)},
{"G", 2, LocalDate.parse("12/11/2000", formatter)},
{"H", 3, LocalDate.parse("12/12/2000", formatter)},
{"I", 4, LocalDate.parse("12/01/1990", formatter)},
{"J", 5, LocalDate.parse("12/02/1990", formatter)},
{"K", 1, LocalDate.parse("12/03/1990", formatter)},
{"L", 2, LocalDate.parse("12/04/1995", formatter)},
{"M", 3, LocalDate.parse("12/05/1995", formatter)},
{"N", 4, LocalDate.parse("12/06/1995", formatter)},
{"O", 5, LocalDate.parse("12/07/1995", formatter)},
{"P", 1, LocalDate.parse("12/08/1980", formatter)},
{"Q", 2, LocalDate.parse("12/09/1980", formatter)},
{"R", 3, LocalDate.parse("12/10/1980", formatter)},
{"S", 4, LocalDate.parse("12/11/1980", formatter)},
{"T", 5, LocalDate.parse("12/12/1980", formatter)},
{"U", 1, LocalDate.parse("12/01/1985", formatter)},
{"V", 2, LocalDate.parse("12/02/1985", formatter)},
{"W", 3, LocalDate.parse("12/03/1985", formatter)},
{"X", 4, LocalDate.parse("12/04/1985", formatter)},
{"Y", 5, LocalDate.parse("12/05/1985", formatter)},
{"Z", 1, LocalDate.parse("12/06/1985", formatter)},},
new Object[]{"Name", "Number", "Date"}) {
@Override
public Class<?> getColumnClass(int columnIndex) {
switch (columnIndex) {
case 0:
return String.class;
case 1:
return Integer.class;
case 2:
return LocalDate.class;
}
return Object.class;
}
};
table.setModel(model);
table.setAutoCreateRowSorter(true);
table.setDefaultRenderer(LocalDate.class, new LocalDateTableCellRenderer(formatter));
add(new JScrollPane(table));
JTextField textField = new JTextField(10);
add(textField, BorderLayout.SOUTH);
textField.addActionListener(new ActionListener() {
protected LocalDate toLocalDate(String text) {
try {
return LocalDate.parse(textField.getText(), formatter);
} catch (DateTimeParseException exp) {
return null;
}
}
@Override
public void actionPerformed(ActionEvent e) {
String text = textField.getText();
if (text.isEmpty()) {
table.setRowSorter(null);
return;
}
List<RowFilter<Object, Object>> filters = new ArrayList<>(2);
// Include the date only if we can parse the text
LocalDate searchDate = toLocalDate(text);
if (searchDate != null) {
filters.add(new LocalDateFilter(ComparisonType.EQUAL, searchDate, 2));
// You can also support date rangers if you want
//filters.add(new LocalDateFilter(ComparisonType.BEFORE, searchDate, 2));
//filters.add(new LocalDateFilter(ComparisonType.AFTER, searchDate, 2));
}
// OR filter every thing else...
filters.add(RowFilter.regexFilter("(?i)" text, new int[]{0, 1}));
TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>((DefaultTableModel) table.getModel());
sorter.setRowFilter(RowFilter.orFilter(filters));
table.setRowSorter(sorter);
}
});
}
}
public class LocalDateTableCellRenderer extends DefaultTableCellRenderer {
private DateTimeFormatter formatter;
public LocalDateTableCellRenderer(DateTimeFormatter formatter) {
this.formatter = formatter;
}
public DateTimeFormatter getFormatter() {
return formatter;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (value instanceof LocalDate) {
setText(getFormatter().format((LocalDate) value));
}
return this;
}
}
public abstract class AbstractFilter<M, I> extends RowFilter<M, I> {
private int[] columns;
public AbstractFilter(int[] columns) {
this.columns = columns;
}
@Override
public boolean include(RowFilter.Entry<? extends M, ? extends I> value) {
int count = value.getValueCount();
if (columns.length > 0) {
for (int i = columns.length - 1; i >= 0; i--) {
int index = columns[i];
if (index < count) {
if (include(value, index)) {
return true;
}
}
}
} else {
while (--count >= 0) {
if (include(value, count)) {
return true;
}
}
}
return false;
}
protected abstract boolean include(RowFilter.Entry<? extends M, ? extends I> value, int index);
}
public class LocalDateFilter<M, I> extends AbstractFilter<M, I> {
private LocalDate date;
private RowFilter.ComparisonType type;
public LocalDateFilter(RowFilter.ComparisonType type, LocalDate date, int column) {
this(type, date, new int[]{column});
}
public LocalDateFilter(RowFilter.ComparisonType type, LocalDate date, int[] columns) {
super(columns);
if (type == null) {
throw new IllegalArgumentException("type must be non-null");
}
this.type = type;
this.date = date;
}
@Override
protected boolean include(RowFilter.Entry<? extends M, ? extends I> value, int index) {
Object v = value.getValue(index);
if (v instanceof LocalDate) {
LocalDate vDate = (LocalDate) v;
switch (type) {
case BEFORE:
return (vDate.isBefore(date));
case AFTER:
return (vDate.isAfter(date));
case EQUAL:
return (vDate.equals(date));
case NOT_EQUAL:
return !(vDate.equals(date));
default:
break;
}
}
return false;
}
}
}
You "could" modify the LocalDateFilter
to convert the date values to String
via a supplied DateTimeFormatter
, but I'd be concerned that you're mixing presentation and data concepts inappropriately, as they should be agnostic to each other - but that's me.