JTable multiple filter design paradigm - java

As title says, i wonder if you could you direct me to some document, or give me advice here, on designing (GUI design) form which main part is occupied by jtable, which has several filters.Main goal is to avoid visual clutter.

I have implemented a simple TableFilterPanel in the past which has one JTextField per table column and performs regular expression matching when text is present in a given field. I typically lay this out as a list of vertical labels + text fields (i.e. so it's fairly compact).
My key class is called ColumnSearcher, which offers the ability to manufacture a RowFilter using the contents of the JTextField:
protected class ColumnSearcher {
private final int[] columns;
private final JTextField textField;
public ColumnSearcher(int column, JTextField textField) {
this.columns = new int[1];
this.textField = textField;
this.columns[0] = column;
}
public JTextField getTextField() {
return textField;
}
public boolean isEmpty() {
String txt = textField.getText();
return txt == null || txt.trim().length() == 0;
}
/**
* #return Filter based on the associated text field's value, or null if the text does not compile to a valid
* Pattern, or the text field is empty / contains whitespace.
*/
public RowFilter<Object, Object> createFilter() {
RowFilter<Object, Object> ftr = null;
if (!isEmpty()) {
try {
ftr = new RegexFilter(Pattern.compile(textField.getText(), Pattern.CASE_INSENSITIVE), columns);
} catch(PatternSyntaxException ex) {
// Do nothing.
}
}
return ftr;
}
}
When I wish to change the filter settings I build an "and" filter from each individual filter:
protected RowFilter<Object, Object> createRowFilter() {
RowFilter<Object, Object> ret;
java.util.List<RowFilter<Object, Object>> filters = new ArrayList<RowFilter<Object, Object>>(columnSearchers.length);
for (ColumnSearcher cs : columnSearchers) {
RowFilter<Object, Object> filter = cs.createFilter();
if (filter != null) {
filters.add(filter);
}
}
if (filters.isEmpty()) {
ret = NULL_FILTER;
} else {
ret = RowFilter.andFilter(filters);
}
return ret;
}
Typically I fire a PropertyChangeEvent when I wish to update the filters and have a PropertyChangeListener respond to it and rebuild my aggregate filter. You may then choose to fire the "rowFilter" PropertyChangeEvent if the user types in one of the text fields (e.g. by adding a DocumentListener to each JTextField).
Hope that helps.

Related

TableViewer does not update the default value of a cell

I have a UI bug in a legacy code in our Java project. We display a table, with three columns (HumanReadable, name and value) in a window. In that window, users can click on each cell and update the values. Before that, user clicks the "add" button to add a new row (three new cells). Each cell has a default value, until the user decides to update the value. Now, when the users decides to update the value of the cell, he clicks on the cell and types in the value. The bug is that, once done editing, it keeps the default value in the UI. In the backend, the value has changed (if you click the cell again, it will go into editing mode and show you the value).
I uploaded a short GIF that shows the issue and can be found here.
In that GIF you can see that I updated the default value of the first column to be test. Then I click some other place (to exit the edit mode) and it showed the default value instead of test in the first column.
The method that creates the table:
private void createTable(final Composite parent) {
final Table varTable = new Table(parent, SWT.MULTI);
varTable.setHeaderVisible(true);
varTable.setLinesVisible(true);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(varTable);
varTableViewer = new TableViewer(varTable);
final DataBindingContext bindingContext = new DataBindingContext();
final TableViewerColumn col1 = GuiUtils.createTableColumn(varTableViewer, "Human Readable");
col1.setEditingSupport(new StringEditingSupport(varTableViewer, bindingContext, dataProperty));
col1.getColumn().setWidth(120);
final TableViewerColumn col2 = GuiUtils.createTableColumn(varTableViewer, "Name");
col2.getColumn().setWidth(120);
col2.setEditingSupport(new StringEditingSupport(varTableViewer, bindingContext, nameProperty));
final TableViewerColumn col3 = GuiUtils.createTableColumn(varTableViewer, "Value");
col3.setEditingSupport(new StringEditingSupport(varTableViewer, bindingContext, valueProperty));
KeyBoardNavigationSupport.createSupport(varTableViewer);
input = new WritableList(globalVars, FlowVar.class);
ViewerSupport.bind(varTableViewer, input, BeanProperties.values(new String[] { dataProperty, nameProperty, valueProperty }));
}
The StringEditingSupport class:
public class StringEditingSupport extends ObservableValueEditingSupport {
private class CellEditorPrintValidatorErrors extends TextCellEditor {
public CellEditorPrintValidatorErrors(Composite control) {
super(control);
}
#Override
protected void focusLost(){
if(this.getErrorMessage() != null) {
MessageDialog.openError(this.getControl().getShell(), "Invalid input", this.getErrorMessage());
}
}
}
private final CellEditor cellEditor;
String propertyName;
public StringEditingSupport(final ColumnViewer viewer, final DataBindingContext dbc, final String propertyName) {
super(viewer, dbc);
cellEditor = new TextCellEditor((Composite) viewer.getControl());
this.propertyName = propertyName;
}
public StringEditingSupport(final ColumnViewer viewer, final DataBindingContext dbc, final String propertyName, final ICellEditorValidator validator) {
super(viewer, dbc);
cellEditor = new CellEditorPrintValidatorErrors((Composite) viewer.getControl());
cellEditor.setValidator(validator);
this.propertyName = propertyName;
}
#Override
protected IObservableValue doCreateCellEditorObservable(final CellEditor cellEditor) {
return SWTObservables.observeText(cellEditor.getControl(), SWT.Modify);
}
#Override
protected IObservableValue doCreateElementObservable(final Object element, final ViewerCell cell) {
return BeansObservables.observeValue(element, propertyName);
}
#Override
protected CellEditor getCellEditor(final Object element) {
return cellEditor;
}
public String getErrorMessage(){
return cellEditor.getErrorMessage();
}
}
I believe it has something to do with the StringEditingSupport class. This class allows to edit the value in each cell of table. But I couldn't figure out a way to "update" the value shown in the GUI. As I understand input (of type WritableList) contains all the information. Here is the add button listener method:
private class AddButtonSelectionListener extends SelectionAdapter {
#Override
public void widgetSelected(final SelectionEvent e) {
String name = nameProperty;
String meaning = dataProperty;
final List<String> names = new ArrayList<String>();
final List<String> meanings = new ArrayList<String>();
for (final Object var : input) {
names.add(((FlowVar) var).getName());
meanings.add(((FlowVar) var).getData());
}
int index = 0;
while (names.contains(name)) {
name = nameProperty + ++index;
}
index = 0;
while (meanings.contains(meaning)) {
meaning = dataProperty + ++index;
}
input.add(new FlowVar(name, valueProperty, meaning));
}
}
So, as I understand, I need to somehow bind the input to the UI (the content of each cell). I did try many attempts like trying to set a listener to the whole table (varTableViewer.addSelectionChangedListener) but none of them worked. Is it possible to suggest a way to solve this kind of issue?
If anything is missing, please let me know and I'll add it.

JavaFX8 bind a List<SimpleStringProperty> to a TableColumn for heavy customized table cell

i have a data model "Rule"
A Rule consists of 1-x String parts saved as a List and an boolean value weather the rule is active or not.
To show this in my UI i want to add a TableView with 2 Columns.
Column 1 should display the Rule Text as a whole, but heavily customized. In the cell i add a textfield for each rule part which then get binded to the StrinProperty (Thats why i need a List of String Properties.
The 2. column should display a checkbox to activate or deactivate the rule (this is no problem an works fine)
Before my rule Model had the boolean isActive flag i used a Listview which had the whole Rule model class as Object. I made my own ListCell implementation and overrode updateItem(Object item, boolean isEmpty) to customize the cell to look like this:
I want the tablecell in column 1 to look exactly how the listcell in my listview looked.
Because ListCell and Tablecell both inherit from IndexedCell i saw no problem in my way of changing the visual of the cell.
My problem is to bind the new datamodel to the table:
private TableView<Rule> tvRules;
this.tvRules = new TableView<Rule>();
this.tvRules.setPrefSize(GuiCore.prefWidth * 0.32, GuiCore.prefHeight * 0.32);
this.tvRules.setEditable(true);
headerBoxLbl = new Label("Active");
headerBox = new CheckBox();
headerBoxLbl.setGraphic(headerBox);
headerBoxLbl.setContentDisplay(ContentDisplay.RIGHT);
headerBox.setOnAction(e -> this.changeAllActiveBoxes());
rulePartsColumn = new TableColumn<Rule, List<SimpleStringProperty>>("Rule");
rulePartsColumn.setCellFactory((callback) -> new RuleTableCell());
rulePartsColumn.setCellValueFactory(cellData -> cellData.getValue().getRulePartsProperty());
rulePartsColumn.setResizable(false);
rulePartsColumn.prefWidthProperty().bind(this.widthProperty().multiply(0.8));
isActiveColumn = new TableColumn<Rule, Boolean>();
isActiveColumn.setCellValueFactory(cellData -> cellData.getValue().getIsActiveProperty());
isActiveColumn.setCellFactory(cellData -> new CheckBoxTableCell<>());
isActiveColumn.setResizable(false);
isActiveColumn.prefWidthProperty().bind(this.widthProperty().multiply(0.2));
isActiveColumn.setStyle( "-fx-alignment: CENTER;");
isActiveColumn.setGraphic(headerBoxLbl);
this.tvRules.getColumns().addAll(rulePartsColumn, isActiveColumn);
As you see i create 2 Columns with the TableDataType Rule, one with Boolean type and one with the List as Data type.
The problem ist that i dont get the binding of the rulePartsColumn to the rule Model to work:
I really dont know how to bind this so in the cell i can work with a List of StringProperties (or SimpleStringProperties).
For reference my Model class Rule:
public class Rule {
private SimpleListProperty<SimpleStringProperty> ruleParts;
private SimpleBooleanProperty isActive;
public Rule() {
this(true, Arrays.asList("", "=", ""));
}
public Rule(final boolean isActive, final List<String> ruleParts) {
this.isActive = new SimpleBooleanProperty(isActive);
this.ruleParts = new SimpleListProperty<SimpleStringProperty>(FXCollections.observableArrayList());
for(int i = 0; i < ruleParts.size(); i++) {
this.ruleParts.add(new SimpleStringProperty(ruleParts.get(i)));
}
}
public SimpleListProperty<SimpleStringProperty> getRulePartsProperty() {
return this.ruleParts;
}
public List<SimpleStringProperty> getRulePartsProperties() {
return (List<SimpleStringProperty>)this.ruleParts;
}
public List<String> getRuleParts() {
List<String> parts = new ArrayList<String>();
for(int i = 0; i < this.ruleParts.size(); i++) {
parts.add(this.ruleParts.get(i).get());
}
return parts;
}
public SimpleBooleanProperty getIsActiveProperty() {
return this.isActive;
}
public boolean isActive() {
return isActive.get();
}
public void setActive(boolean isActive) {
this.isActive.set(isActive);
}
}
Thanks in advance

Filter JTable with Numbers without the grouping character (thousands-separator)

I'm trying to filter Rows in a JTable which contains Columns with numbers.
The filtering is working so far, but it filters over the numbers including the thousands-separators. For example, if there is a row with the number 25689 in one row and I try to filter for this row, i have to use "25.689". So it seems there is a formatting that is performed before the filtering.
I've tried to set an own default renderer and the numbers are shown without the separators but the filtering is the same.
Edit
I've added a full example re-creating my problem:
public class GroupingTest {
JFrame frame= null;
Container pane= null;
JTextField tf=null;
JXTable table=null;
public void searchTable() {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
try {
final String searchEx = "(?i)"
+ Pattern.quote(tf.getText());
final RowFilter<TableModel, Object> filter;
filter = RowFilter.regexFilter(searchEx);
table.setRowFilter(filter);
//packAll in edt
Utility.packTableView(table);
} catch (final Exception e) {
return;
}
}
});
}
public void createTable() {
frame = new JFrame();
pane=frame.getContentPane();
tf = new JTextField();
tf.setPreferredSize(new Dimension(200,25));
tf.getDocument().addDocumentListener(new DocumentListener() {
#Override
public void removeUpdate(final DocumentEvent e) {
searchTable();
}
#Override
public void insertUpdate(final DocumentEvent e) {
searchTable();
}
#Override
public void changedUpdate(final DocumentEvent e) {
searchTable();
}
});
String[] columnHeaders = {"long","strings"};
DefaultTableModel $model = new DefaultTableModel(columnHeaders, 0) {
#Override
public Class<?> getColumnClass(final int $col) {
if($col == 0) {
return Long.class;
} else if($col == 1){
return String.class;
} else {
return Object.class;
}
}
};
table = new JXTable($model);
table.setDefaultRenderer(Long.class, new DefaultTableCellRenderer() {
#Override
public java.awt.Component getTableCellRendererComponent(final JTable $table,
final Object $value, final boolean $isSelected, final boolean $hasFocus, final int $row,
final int $column) {
super.getTableCellRendererComponent($table, $value, $isSelected, $hasFocus, $row, $column);
if ($value instanceof Long) {
this.setHorizontalAlignment(SwingConstants.RIGHT);
}
return this;
}
});
Object[] line1 = {new Long(23345),"asdf"};
$model.addRow(line1);
Object[] line2 = {new Long(3),"dfw"};
$model.addRow(line2);
pane.add(tf,BorderLayout.NORTH);
pane.add(new JScrollPane(table),BorderLayout.CENTER);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setPreferredSize(new Dimension(300,200));
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
GroupingTest gt = new GroupingTest();
gt.createTable();
}
}
The Filtering is working so far, but it filters over the numbers including the thousands-separators.
When the value's format interferes with the expected functioning of sorters and filters then it's time to check if getColumnClass(int columnIndex) in the table model is retrieving the appropriate class (in this case Double).
By default AbstractTableModel implementation of such method returns Object.class which is rendered using the toString() method (that's why you see the thousands-separator) and probably filtered according to the string representation as well. Subclasses of AbstractTableModel (such as DefaultTableModel) inherit this implementation and thus this method should be overriden. For example let's say your table model is DefaultTableModel and the first column is a Double:
DefaultTableModel model = new DefaultTableModel() {
#Override
public Class<?> getColumnClass(int columnIndex) {
return columnIndex == 0 ? Double.class
: super.getColumnClass(columnIndex);
}
};
See Sorting and Filtering section of How to Use Tables tutorial for further details.
Update
Given your new MVCE it is clear now what are you trying to achieve. I'd start saying that I've mistakenly assumed your table model holds Double instead of Long which makes no difference about overriding getColumnClass() method (it should be done anyways) but it will make a slight difference in the final solution.
Now, to state the requirements clear, you need to filter that column either:
Users input a number (Long) including grouping character.
Users input a number without grouping character.
The string representation of the value contains the substring typed by the users.
To achieve this goal I'd use a custom RowFilter instead of using a regex filter like you do in your example. This is to have control about the string typed by the user and check the three conditions listed above. I've managed to modify your searchTable() to satisfy the requirements. Note: I've included the queried String as an argument in this method to keep tf text field out of the implementation. Please see the code below:
private void searchTable(final String query) {
RowFilter<TableModel, Integer> filter = null;
if (query.length() > 0) {
filter = new RowFilter<TableModel, Integer>() {
#Override
public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) {
for (int i = 0; i < entry.getValueCount(); i++) {
String stringValue = entry.getStringValue(i);
Object entryValue = entry.getValue(i);
String numberString = entryValue instanceof Long
? String.valueOf(entryValue)
: "";
if (stringValue.contains(query) || numberString.contains(query)) {
return true;
}
}
return false;
}
};
}
table.setRowFilter(filter);
}
The flow will be more or less as follows:
If the query length is 0 just let the filter be null. This means the table won't be filtered and all rentries will be included.
If not (1) then prepare a new filter which iterates over the whole row asking if the String representation of the entry or the String value of the entry contains the queried String. While those might look the same thing they are not because Entry#getStringValue(int index) might (and actually does) retrieve a different value than String#valueOf(entry#getValue(int index)). In this case the first one retrieves the Long including grouping separators (or formatted if you prefer) while the second one retrieves the Long with no formatting at all (it means, no grouping separators).
Apply the filter to the table in either case.
I hope the idea is clear enough. If you want to filter a Double then it has to be tweaked a little bit because String.valueOf(double) includes the decimal (not grouping) separator and you might want to remove it before checking if it contains the queried String.

Adding multiple buttons || cells in a cell in GXT 3.0 Grid

I'm using GXT 3.0 and I want to develop a grid table in it. In table, a cell assigned to be have multiple jobs, like save, delete, update. So I need to develop a grid table which has multiple buttons in a cell. To visualize the problem I'm sharing this image :
I tried to add just a cell via
ColumnConfig.setCell()
method, and It's succeeded. But I must add multiple buttons, or cells to handle events. In short form I need multiple Cells inside a Cell.
I know there is a method called ColumnConfig.setWidget(), but it didn't helped. It just added toolbar(or any widget element) to top(header part).
Remember that I use GXT 3.0
Thanks for any help.
You must use a CompositeCell :
private CompositeCell<ObjectRow> createCompositeCell(){
HasCell<ObjectRow, String> button1 = new HasCell<ObjectRow, String>() {
public Cell<String> getCell() {
return new ButtonCell();
}
public FieldUpdater<ObjectRow, String> getFieldUpdater() {
return null;
}
public String getValue(ObjectRow object) {
return "Button 1";
}};
HasCell<ObjectRow, String> button2 = new HasCell<ObjectRow,String>(){
public Cell<String> getCell() {
return new ButtonCell();
}
public FieldUpdater<ObjectRow, String> getFieldUpdater() {
return null;
}
public String getValue(ObjectRow object) {
return "Button 2";
}
};
List<HasCell<ObjectRow, ?>> cells = new ArrayList<HasCell<ObjectRow, ?>>();
cells.add(buton1);
cells.add(button2);
CompositeCell<ObjectRow> compositeCell = new CompositeCell<ObjectRow>(cells);
return compositeCell;
}
You can set a different FieldUpdater for handle button click.

Databind and validate a TableViewer?

I use the org.eclipse.core.databinding framework to bind some Text fields in an SWT application. I add an update strategy to validate the data and to set the value on the model only when the user click on the save button:
UpdateValueStrategy toModel = new UpdateValueStrategy(UpdateValueStrategy.POLICY_CONVERT);
if (validator != null) {
toModel.setAfterGetValidator(validator);
}
UpdateValueStrategy fromModel = new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE);
binding = bindingContext.bindValue(SWTObservables.observeText(this, SWT.Modify),
BeansObservables.observeValue(pVO, propertyName), toModel, fromModel);
This piece of code works really well.
But how can I do the same on a TableViewer?
I want it to work so that when I add something in the IHM, the model stay unchanged until I call getBindingContext().updateModels();
You do not need use the JFace Databinding Framework in TableViewer. Manipulation the structured data is simpler then SWT controls, such TableViewer, ListViewer and TreeViewer. You can use those viewer in the same way:
create viewer
set content provider
set label provider (suggested)
set filter (optional)
set sorter (optional)
After the viewer created, just invoke viewer.setInput(data) to put all the things to your viewer.
There are a list of model:
TableViewer tableViewer = new TableViewer(parent);
Table table = tableViewer.getTable();
table.setHeaderVisible(true);
table.setLinesVisible(true);`
for (int i = 0; i < COLUMN_NAMES.length; i++) {
TableColumn tableColumn = new TableColumn(table, SWT.LEFT);
tableColumn.setText(COLUMN_NAMES[i]);
tableColumn.setWidth(COLUMN_WIDTHS[i]);
}
tableViewer.setContentProvider(new ModelContentProvider());
tableViewer.setLabelProvider(new ModelLabelProvider());
tableViewer.setInput(models);
The magic happens in the content provider:
class ModelContentProvider implements IStructuredContentProvider {
#SuppressWarnings("unchecked")
#Override
public Object[] getElements(Object inputElement) {
// The inputElement comes from view.setInput()
if (inputElement instanceof List) {
List models = (List) inputElement;
return models.toArray();
}
return new Object[0];
}
/* ... other methods */
}
Each model will become a TableItem and the model in the TableItem(item.getData()).
However, a table composed by many columns, you need the LabelProvider to help you mapping the property of model to the TableItem:
class ModelLabelProvider extends LabelProvider implements
ITableLabelProvider {
#Override
public Image getColumnImage(Object element, int columnIndex) {
// no image to show
return null;
}
#Override
public String getColumnText(Object element, int columnIndex) {
// each element comes from the ContentProvider.getElements(Object)
if (!(element instanceof Model)) {
return "";
}
Model model = (Model) element;
switch (columnIndex) {
case 0:
return model.getFoo();
case 1:
return model.getBar();
default:
break;
}
return "";
}
}
The propagation of models to viewer is easy. If you will propagate viewer to the binded model, using the CellEditor is simple as well.
To use CellEditor, you need set the column properties, cell editors and cell modifier to TableViewer:
tableViewer.setColumnProperties(COLUMNS_PROPERTIES);
tableViewer.setCellEditors(new CellEditor[] {
new TextCellEditor(table), new TextCellEditor(table) });
tableViewer.setCellModifier(new ModelCellModifier(tableViewer));
The CellModifier likes this:
class ModelCellModifier implements ICellModifier {
TableViewer viewer;
public ModelCellModifier(TableViewer viewer) {
this.viewer = viewer;
}
#Override
public boolean canModify(Object element, String property) {
// property is defined by viewer.setColumnProperties()
// allow the FOO column can be modified.
return "foo_prop".equals(property);
}
#Override
public Object getValue(Object element, String property) {
if ("foo_prop".equals(property)) {
return ((Model) element).getFoo();
}
if ("bar_prop".equals(property)) {
return ((Model) element).getBar();
}
return "";
}
#Override
public void modify(Object element, String property, Object value) {
if ("foo_prop".equals(property)) {
TableItem item = (TableItem) element;
((Model) item.getData()).setFoo("" + value);
// refresh the viewer to show the changes to our user.
viewer.refresh();
}
}
}
Everything is simple but there are many steps to make all together.
Use ViewerSupport:
TableViewer tableViewer = ...
IObservableList tableElements = ...
IValueProperty[] columnProperties = ...
ViewerSupport.bind(tableViewer, tableElements, columnProperties);
i agree with qualidafial.
Snippet017TableViewerWithDerivedColumns from the jface.databinding snippets is a full example of this.

Categories

Resources