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.
Related
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
My Setup
I have some code in an Eclipse RCP application that looks like this (it is in the #PostConstruct method of a Part):
scroll = new ScrolledComposite(parent, SWT.V_SCROLL | SWT.H_SCROLL | SWT.BORDER);
taskingInputsGroup = new Composite(scroll, SWT.NONE);
textSendTime = new Text(taskingInputsGroup, SWT.BORDER);
textSubject = new Text(taskingInputsGroup, SWT.BORDER);
textTaskStartTime = new Text(taskingInputsGroup, SWT.BORDER);
I'm trying to set a private Control field for an Enum's constants to each of these Text objects:
textSendTime = new Text(taskingInputsGroup, SWT.BORDER);
MsgField.SEND_TIME.setControl(textSendTime);
In the Enum, I just have a simple getter/setter for the Control field.
I have a method that is called when a user presses a Button. This method loops through the Enum constants and sets the text of some TreeItems to whatever is in the Control objects:
MsgField[] msgFields= MsgField.values();
for (int i = 0; i < msgFields.length; i++) {
Control control = msgFields[i].getControl();
if (control != null) {
if (control instanceof Text) {
root.getItem(i).getItem(0).setText(((Text)control).getText());
}
}
}
My Question
I am getting empty text from ((Text)control).getText() even if there is text in the Text field. Why could this be? I know I'm overlooking something simple (it's been a long day). I've read a bunch of SO posts on Java passing by value, but I can't seem to apply the answers to this issue. This works fine if I call getText() directly on the object in my view class:
root.getItem(1).getItem(0).setText(textSendTime.getText());
EDIT - Some MsgField enum code:
public enum MsgField {
private boolean isRequiredField;
private String treeName;
public abstract void setValue(Msg msg, String value);
public abstract Object getValue(Msg msg);
private MsgField(boolean req, String name) {
isRequiredField = req;
treeName = name;
}
SEND_TIME("Send Time", true) {
#Override
public void setValue(Msg msg, String value) {
msg.setSendTime(value);
}
#Override
public Object getValue(Msg msg) {
return msg.getSendTime();
}
},
// ....
// other fields
// ....
START_TIME("Start Time", false) {
#Override
public void setValue(Msg msg, String value) {
msg.setStartTime(value);
}
#Override
public Object getValue(Msg msg) {
return msg.getStartTime();
}
};
}
So the idea here is to represent the fields for the Msg class and have these get/set methods for each one so that I can easily iterate over them and get/set the fields for my Msg object. This worked fine (although maybe you can suggest a better alternative), but the trouble came with the addition of that Control field as I mentioned.
I am using smartgwt ( not the paid license version ) and I have a listgrid with three entries.
Key, Value, Reset.
The reset-field is a button that should reset any changes to the value and here is where I struggle.
I tried to implement it as simple as
#Override
public void onClick(ClickEvent event)
{
DataSource ds = this.grid.getDataSource();
ds.removeData(record);
ds.fetchData();
this.grid.redraw();
}
grid being my ListGrid and record the row that has been clicked to be reseted.
But this only removes the entry, it is there again if I reload ( even with the right value , because that is what my server does if he gets remove-requests), but I would like that it is there immediately after I click the button and not after clicking around a bit.
I assumed the fetchData and redraw request combined would accomplish this.
edit: Okay some more code, this shows my constructor for the ListGrid and the RevertButton which should remove and add the Record again.
private static final String REVERT_NAME = "revertField";
public MyListGrid(final String name)
{
this.setDataSource(PropertyListDS.getInstance(name);
ListGridField keyField = new ListGridField(ConfigurationDataSourceFields.PROPERTY_NAME, "Property");
ListGridField valueField = new ListGridField(ConfigurationDataSourceFields.PROPERTY_VALUE, "Value");
ListGridField revertField = new ListGridField(REVERT_NAME, "Revert to Default");
valueField.setCanEdit(true);
this.setShowRecordComponents(true);
this.setShowRecordComponentsByCell(true);
this.setAutoFetchData(true);
this.setFields(keyField, valueField, revertField);
}
#Override
protected Canvas createRecordComponent(final ListGridRecord record, Integer colNum)
{
String fieldName = this.getFieldName(colNum);
Canvas canvas = null;
if ( REVERT_NAME.equals(fieldName) )
{
canvas = new RevertButton(this, record);
}
return canvas;
}
private class RevertButton extends IButton implements ClickHandler
{
private final MyListGrid grid;
private final ListGridRecord record;
public RevertButton(final MyListGrid grid, final ListGridRecord record)
{
super();
this.setTitle("Revert to Default");
this.grid = grid;
this.record = record;
this.addClickHandler(this);
}
#Override
public void onClick(ClickEvent event)
{
DataSource ds = this.grid.getDataSource();
ds.removeData(record);
ds.fetchData();
this.grid.redraw();
}
}
Do in this way using DSCallback.
DataSource#removeData() is a async call to the server. Either redraw the grid again or fetch the data again after getting response from server that record has been deleted in DSCallback.
DataSource dataSource = grid.getDataSource();
dataSource.removeData(record,new DSCallback() {
#Override
public void execute(DSResponse dsResponse, Object data, DSRequest dsRequest){
Record[] records=dsResponse.getData();//deleted records
grid.fetchData();//fetch data again
}
});
Please have a look at this thread Removing local record from listGrid without committing
Try with ListGrid#saveAllEdits() before fetching the data again.
You can try with ListGrid#removeSelectedData() to remove the currently selected records from this component. If this is a databound grid, the records will be removed directly from the DataSource.
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.
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.