I have table field with editable column. When I change value in one cell I need to calculate value in other fields.
I would like to know, how to detect when value in cell has change.
I couldn't find any hooks that will tell me that value in cell has change.
I found :
#Override
public void fireTableEventInternal(final TableEvent e) {
if (e.getType() == TableEvent.TYPE_ROWS_UPDATED) {
AbstractProductsTableField.this.execPriceValueChange();
} else {
super.fireTableEventInternal(e);
}
}
but I don't think this is a way to go.
I think that execCompleteEdit is the function you need.
#Order(10)
public class MyColumn extends AbstractStringColumn {
#Override
protected String getConfiguredHeaderText() {
return "MyColumn";
}
#Override
protected void execCompleteEdit(ITableRow row, IFormField editingField) {
//Test if there is a new value:
//compare getValue(row) and ((IStringField) editingField).getValue()
//Call super to store the value to the cell (default behavior):
super.execCompleteEdit(row, editingField);
}
}
Related
I have a TableView which is updated from an ObservableList. It has two columns. When a file is loaded, the list is populated and the table updates, (initially just the first column is populated). After validation of the items in the list the second column is populated with a success or failure flag. Using the setRowFactory I update the background style of the row to either green for success or red for failure. Some items don't get validated and are styled with "". The table has about a dozen rows visible out of a couple of thousand rows total. The problem I have is that the visible rows don't get their background style updated until they're scrolled out of view and then back in again.
I've been able to overcome this by using the table's refresh() method, but that causes another problem. The first column is editable to allow the data to be corrected before re-validation. If the refresh() method is used then it breaks the ability to edit a cell. The textfield still appears, but is disabled, (no focus border and no ability to highlight or edit its content).
If I leave out the refresh() method editing works just fine. Include the refresh() and the table displays correctly without the need for scrolling, but editing is broken.
So I can either have editable cells or properly displayed rows, but not both. Apart from this problem the code works fine. I've read countless examples and TableView issues, and associated solutions, and nothing I've tried has fixed the problem. In my efforts I can see that the overriden updateItem method is only ever called when the row is redrawn after becoming visible again. My thinking is that I need another mechanism to style the rows on the validationResponse change but this is where I get stuck.
So my question is how to have the visible table rows get their style updated without scrolling while not breaking cell editing? Thanks!!
Edit:
Reproducible code example follows. Click the first button to populate the table with initial data. Click the second button to simulate validation. The second column will update with the validation response, but the styling doesn't take effect until the rows are scrolled out of view and then back in to view. At this point first column is editable. If you uncomment the tblGCode.refresh() line and re-run the test the styling is applied immediately without scrolling, but editing a cell in the first column no longer works.
Main class:
public class TableViewTest extends Application {
private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList(
item -> new Observable[]{item.validatedProperty(), item.errorDescriptionProperty()});
private final TableView tblGCode = new TableView();
#Override
public void start(Stage stage) {
TableColumn<GCodeItem, String> colGCode = new TableColumn<>("GCode");
colGCode.setCellValueFactory(new PropertyValueFactory<>("gcode"));
TableColumn<GCodeItem, String> colStatus = new TableColumn<>("Status");
colStatus.setCellValueFactory(new PropertyValueFactory<>("validationResponse"));
// Set first column to be editable
tblGCode.setEditable(true);
colGCode.setEditable(true);
colGCode.setCellFactory(TextFieldTableCell.forTableColumn());
colGCode.setOnEditCommit((TableColumn.CellEditEvent<GCodeItem, String> t) -> {
((GCodeItem) t.getTableView().getItems().get(t.getTablePosition().getRow())).setGcode(t.getNewValue());
});
// Set row factory
tblGCode.setRowFactory(tbl -> new TableRow<GCodeItem>() {
private final Tooltip tip = new Tooltip();
{
tip.setShowDelay(new Duration(250));
}
#Override
protected void updateItem(GCodeItem item, boolean empty) {
super.updateItem(item, empty);
if(item == null || empty) {
setStyle("");
setTooltip(null);
} else {
if(item.isValidated()) {
if(item.hasError()) {
setStyle("-fx-background-color: #ffcccc"); // red
tip.setText(item.getErrorDescription());
setTooltip(tip);
} else {
setStyle("-fx-background-color: #ccffdd"); // green
setTooltip(null);
}
} else {
setStyle("");
setTooltip(null);
}
}
//tblGCode.refresh(); // this works to give desired styling, but breaks editing
}
});
tblGCode.getColumns().setAll(colGCode, colStatus);
tblGCode.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
// buttons to simulate issue
Button btnPopulate = new Button("1. Populate Table");
btnPopulate.setOnAction(eh -> populateTable());
Button btnValidate = new Button("2. Validate Table");
btnValidate.setOnAction(eh -> simulateValidation());
var scene = new Scene(new VBox(tblGCode, btnPopulate, btnValidate), 640, 320);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
private void populateTable() {
// simulates updating of ObservableList with first couple of dozen lines of a file
gcodeItems.add(new GCodeItem("(1001)"));
gcodeItems.add(new GCodeItem("(T4 D=0.25 CR=0 - ZMIN=-0.4824 - flat end mill)"));
gcodeItems.add(new GCodeItem("G90 G94"));
gcodeItems.add(new GCodeItem("G17"));
gcodeItems.add(new GCodeItem("G20"));
gcodeItems.add(new GCodeItem("G28 G91 Z0"));
gcodeItems.add(new GCodeItem("G90"));
gcodeItems.add(new GCodeItem(""));
gcodeItems.add(new GCodeItem("(Face1)"));
gcodeItems.add(new GCodeItem("T4 M6"));
gcodeItems.add(new GCodeItem("S5000 M3"));
gcodeItems.add(new GCodeItem("G54"));
gcodeItems.add(new GCodeItem("M8"));
gcodeItems.add(new GCodeItem("G0 X1.3842 Y-1.1452"));
gcodeItems.add(new GCodeItem("Z0.6"));
gcodeItems.add(new GCodeItem("Z0.2"));
gcodeItems.add(new GCodeItem("G1 Z0.015 F20"));
gcodeItems.add(new GCodeItem("G18 G3 X1.3592 Z-0.01 I-0.025 K0"));
gcodeItems.add(new GCodeItem("G1 X1.2492"));
gcodeItems.add(new GCodeItem("X-1.2492 F40"));
gcodeItems.add(new GCodeItem("X-1.25"));
gcodeItems.add(new GCodeItem("G17 G2 X-1.25 Y-0.9178 I0 J0.1137"));
gcodeItems.add(new GCodeItem("G1 X1.25"));
gcodeItems.add(new GCodeItem("G3 X1.25 Y-0.6904 I0 J0.1137"));
// Add list to table
tblGCode.setItems(gcodeItems);
}
private void simulateValidation() {
// sets validationResponse on certain rows (not every row is validated)
gcodeItems.get(2).setValidationResponse("ok");
gcodeItems.get(3).setValidationResponse("ok");
gcodeItems.get(4).setValidationResponse("ok");
gcodeItems.get(5).setValidationResponse("ok");
gcodeItems.get(6).setValidationResponse("ok");
gcodeItems.get(9).setValidationResponse("error:20");
gcodeItems.get(10).setValidationResponse("ok");
gcodeItems.get(11).setValidationResponse("ok");
gcodeItems.get(12).setValidationResponse("ok");
gcodeItems.get(13).setValidationResponse("ok");
gcodeItems.get(14).setValidationResponse("ok");
gcodeItems.get(15).setValidationResponse("ok");
gcodeItems.get(16).setValidationResponse("ok");
gcodeItems.get(17).setValidationResponse("ok");
gcodeItems.get(18).setValidationResponse("ok");
gcodeItems.get(19).setValidationResponse("ok");
gcodeItems.get(20).setValidationResponse("ok");
gcodeItems.get(21).setValidationResponse("ok");
gcodeItems.get(22).setValidationResponse("ok");
gcodeItems.get(23).setValidationResponse("ok");
}
}
GCodeItem model:
public class GCodeItem {
private final SimpleStringProperty gcode;
private final SimpleStringProperty validationResponse;
private ReadOnlyBooleanWrapper validated;
private ReadOnlyBooleanWrapper hasError;
private ReadOnlyIntegerWrapper errorNumber;
private ReadOnlyStringWrapper errorDescription;
public GCodeItem(String gcode) {
this.gcode = new SimpleStringProperty(gcode);
this.validationResponse = new SimpleStringProperty("");
this.validated = new ReadOnlyBooleanWrapper();
this.hasError = new ReadOnlyBooleanWrapper();
this.errorNumber = new ReadOnlyIntegerWrapper();
this.errorDescription = new ReadOnlyStringWrapper();
validated.bind(Bindings.createBooleanBinding(
() -> ! "".equals(getValidationResponse()),
validationResponse
));
hasError.bind(Bindings.createBooleanBinding(
() -> ! ("ok".equals(getValidationResponse()) ||
"".equals(getValidationResponse())),
validationResponse
));
errorNumber.bind(Bindings.createIntegerBinding(
() -> {
String vResp = getValidationResponse();
if ("ok".equals(vResp)) {
return 0;
} else {
// should handle potential exceptions here...
if(vResp.contains(":")) {
int en = Integer.parseInt(vResp.split(":")[1]);
return en ;
} else {
return 0;
}
}
}, validationResponse
));
errorDescription.bind(Bindings.createStringBinding(
() -> {
int en = getErrorNumber() ;
return GrblDictionary.getErrorDescription(en);
}, errorNumber
));
}
public final String getGcode() {
return gcode.get();
}
public final void setGcode(String value) {
gcode.set(value);
}
public SimpleStringProperty gcodeProperty() {
return this.gcode;
}
public final String getValidationResponse() {
return validationResponse.get();
}
public final void setValidationResponse(String value) {
validationResponse.set(value);
}
public SimpleStringProperty validationResponseProperty() {
return this.validationResponse;
}
public Boolean isValidated() {
return validatedProperty().get();
}
public ReadOnlyBooleanProperty validatedProperty() {
return validated.getReadOnlyProperty();
}
// ugly method name to conform to method naming pattern:
public final boolean isHasError() {
return hasErrorProperty().get();
}
// better method name:
public final boolean hasError() {
return isHasError();
}
public ReadOnlyBooleanProperty hasErrorProperty() {
return hasError.getReadOnlyProperty();
}
public final int getErrorNumber() {
return errorNumberProperty().get();
}
public ReadOnlyIntegerProperty errorNumberProperty() {
return errorNumber.getReadOnlyProperty() ;
}
public final String getErrorDescription() {
return errorDescriptionProperty().get();
}
public ReadOnlyStringProperty errorDescriptionProperty() {
return errorDescription.getReadOnlyProperty();
}
}
Supporting dictionary class (abridged):
public class GrblDictionary {
private static final Map<Integer, String> ERRORS = Map.ofEntries(
entry(1, "G-code words consist of a letter and a value. Letter was not found."),
entry(2, "Numeric value format is not valid or missing an expected value."),
entry(17, "Laser mode requires PWM outentry."),
entry(20, "Unsupported or invalid g-code command found in block."),
entry(21, "More than one g-code command from same modal group found in block."),
entry(22, "Feed rate has not yet been set or is undefined.")
);
public static String getErrorDescription(int errorNumber) {
return ERRORS.containsKey(errorNumber) ? ERRORS.get(errorNumber) : "Unrecognized error number.";
}
}
Edit #2:
If I replace the TableView.setRowFactory code with TableColumn.setCellFactory as shown below I get the desired effect and editing still works. Is this a sensible solution, or should I really be using setRowFactory and getting the list changes recognised correctly by setRowFactory? In my testing it only ever seemed like the overriden updateItem method was being called when rows scrolled in to view.
colStatus.setCellFactory(tc -> new TableCell<GCodeItem, String>() {
private final Tooltip tip = new Tooltip();
{
tip.setShowDelay(new Duration(250));
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
TableRow<GCodeItem> row = this.getTableRow();
GCodeItem rowItem = row.getItem();
if(item == null || empty) {
row.setStyle("");
row.setTooltip(null);
} else {
if(rowItem.isValidated()) {
if(rowItem.hasError()) {
row.setStyle("-fx-background-color: #ffcccc"); // red
tip.setText(rowItem.getErrorDescription());
row.setTooltip(tip);
} else {
row.setStyle("-fx-background-color: #ccffdd"); // green
row.setTooltip(null);
}
} else {
row.setStyle("");
row.setTooltip(null);
}
setText(item);
}
}
});
Edit #3:
Many thanks to kleopatra and James_D I now have a solution. Overriding isItemChanged() in the row factory has solved my issue.
The place to install conditional row styling is a custom TableRow - nowhere else. As always, contained nodes - like tableCells here - must not interfere with their parent's state, never-ever!.
The base problem with such styling in a tableRow is that row.updateItem(...) is not called when we might expect it, in particular, not after an update of a property. There are two options to solve (apart from making sure that the table is notified at all on updates of properties not shown in columns by using an extractor as already suggested by James)
A quick option is to unconditionally force an update always, by overriding isItemChanged:
#Override
protected boolean isItemChanged(GCodeItem oldItem,
GCodeItem newItem) {
return true;
}
Another option is to update the styling in both updateItem(...) and updateIndex(...) (the latter is called always when anything chances in the data)
#Override
protected void updateIndex(int i) {
super.updateIndex(i);
doUpdateItem(getItem());
}
#Override
protected void updateItem(CustomItem item, boolean empty) {
super.updateItem(item, empty);
doUpdateItem(item);
}
protected void doUpdateItem(CustomItem item) {
// actually do the update and styling
}
Choosing between both depends on context and requirements. Have seen contexts where the one or other didn't work properly, without a clean indication when/why that happened (too lazy to really dig ;)
Aside - a couple of comments to the question which did improve considerably over time but still is not quite a [MCVE]:
the data item is both too complex (for basic styling, there's no need for several direct/indirect intertwined conditions) and not complete enough to really demonstrate the requirements (like update after editing the value that drives the error condition)
the data item exposes properties (good thing!) - so use those (vs. PropertyValueFactory, bad thing!)
with a writable property a custom edit commit handler is not needed
TableColumn is editable by default, making col.setEditable(true) a no-op. If only some columns should editable, the others must be set to false
The basic issue is that the table is not forcing updates on the table row when the relevant properties change. Using the "extractor" as you do with
private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList(
item -> new Observable[]{item.validatedProperty(), item.errorDescriptionProperty()});
should work, but it seems the table does not force row updates when the underlying data list fires updated type changes. (I'd consider this a bug; it's possible the JavaFX team simply doesn't consider this a supported feature.)
One approach here is to have the TableRow register a listener with the current item's validationResponseProperty() (or any other desired property), and update the row when it changes. A little care is needed here, because the current item that the row displays can change (e.g. when scrolling or when the data in the list change), so you need to observe the itemProperty() and ensure the listener is registered with the property in the correct item. This looks like:
// Set row factory
tblGCode.setRowFactory(tbl -> new TableRow<GCodeItem>() {
private final Tooltip tip = new Tooltip();
private final ChangeListener<String> listener = (obs, oldValidationResponse, newValidationResponse) ->
updateStyleAndTooltip();
{
tip.setShowDelay(new Duration(250));
itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.validationResponseProperty().removeListener(listener);
}
if (newItem != null) {
newItem.validationResponseProperty().addListener(listener);
}
updateStyleAndTooltip();
});
}
#Override
protected void updateItem(GCodeItem item, boolean empty) {
super.updateItem(item, empty);
updateStyleAndTooltip();
}
private void updateStyleAndTooltip() {
GCodeItem item = getItem();
if(item == null || isEmpty()) {
setStyle("");
setTooltip(null);
} else {
if(item.isValidated()) {
if(item.hasError()) {
setStyle("-fx-background-color: #ffcccc"); // red
tip.setText(item.getErrorDescription());
setTooltip(tip);
} else {
setStyle("-fx-background-color: #ccffdd"); // green
setTooltip(null);
}
} else {
setStyle("");
setTooltip(null);
}
}
}
});
Note now you no longer need the list created with the extractor:
private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList();
and indeed this would work without the dependent properties being implemented as JavaFX (bound) properties (as long as they are kept consistent with the other data); though I still consider the version you currently have to be the better implementation.
BTW, as a brief aside, your style will work better if you use -fx-background instead of -fx-background-color. By default, the background color (-fx-background-color) of a row is set equal to -fx-background. However, the color of the text is made dependent on -fx-background: if -fx-background is light, then a dark text is used, and vice-versa. By default, selecting a row changes -fx-background, which results in a change in text color, so in your implementation you'll notice the text is hard to read in a selected (validated or error) row. In short, modifying -fx-background will play better with selection than modifying -fx-background-color.
I would like to calculate a table's cell content asynchronously in the background. I came up with the following solution:
public abstract class AsynchronousCellFactory<E, T> implements Callback<TableColumn<E, T>, TableCell<E, T>> {
#Override
public TableCell<E, T> call(final TableColumn<E, T> param) {
final TableCell<E, T> cell = new TableCell<E, T>() {
private Service<T> service;
#Override
public void updateItem(final T item, final boolean empty) {
super.updateItem(item, empty);
if (service != null) {
service.cancel();
}
if (empty || this.getTableRow() == null || this.getTableRow().getItem() == null) {
setText(null);
} else {
setText("Calculating..");
final E rowDataItem = (E) this.getTableRow().getItem();
service = new Service<T>() {
#Override
protected Task<T> createTask() {
return getTask(rowDataItem);
}
};
service.setOnSucceeded(e -> {
setText(e.getSource().getValue().toString());
});
service.setOnFailed(e -> {
final Throwable t = e.getSource().getException();
setText(t.getLocalizedMessage());
});
service.start();
}
}
};
return cell;
}
protected abstract Task<T> getTask(E rowDataItem);
}
I use this factory for almost all of my columns to calculate different values.
But this does not work very well.
I frequently see cell contents saying Calculating. When I double click on that cell, which triggers a repaint apparently, the correct value appears.
Furthermore, even worse, I see sometimes that cell content is "switched" between columns: stringA, that should be in columnA, is in comlumnB, for example. Again, a double click on that cell fixes that.
I know that those factories are reused by the table columns to calculate cell content for different items, I assume the error is connected to that fact.
EDIT: When I move the service field to the TableCell, the Calculating error seems to be fixed, nevertheless, the switched-values problem persists.
Find a MWE here. It is actually not a working example, since it does not reproduce the problem. I guess the cell factory is working OK, and I am doing something wrong somewhere else.
Here is what i have now.
Simply my RowsFragment is look like this,
public static class SampleFragmentB extends RowsFragment {
private final ArrayObjectAdapter mRowsAdapter;
public SampleFragmentB() {
mRowsAdapter = new ArrayObjectAdapter(new ShadowRowPresenterSelector());
setAdapter(mRowsAdapter);
setOnItemViewClickedListener(new OnItemViewClickedListener() {
#Override
public void onItemClicked(
Presenter.ViewHolder itemViewHolder,
Object item,
RowPresenter.ViewHolder rowViewHolder,
Row row) {
Toast.makeText(getActivity(), "Implement click handler", Toast.LENGTH_SHORT)
.show();
}
});
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
createRows();
getMainFragmentAdapter().getFragmentHost().notifyDataReady(getMainFragmentAdapter());
}
private void createRows() {
String json = Utils.inputStreamToString(getResources().openRawResource(
R.raw.page_row_example));
CardRow[] rows = new Gson().fromJson(json, CardRow[].class);
for (CardRow row : rows) {
mRowsAdapter.add(createCardRow(row));
}
}
private Row createCardRow(CardRow cardRow) {
PresenterSelector presenterSelector = new CardPresenterSelector(getActivity());
ArrayObjectAdapter adapter = new ArrayObjectAdapter(presenterSelector);
for (Card card : cardRow.getCards()) {
adapter.add(card);
}
HeaderItem headerItem = new HeaderItem(cardRow.getTitle());
return new CardListRow(headerItem, adapter, cardRow);
}
}
This is what i exactly want to do,
So I want to make always visible Header of each row without selecting into or focusing into RowsFragment. I'am using leanback v24 to add multiple rows into each header item.
You might want to check the Sofa library for Android TV that extends the Leanback library capabilities by offering a set of more powerful features. In BrowseSupportFragment, mRowsFragment.setExpand should be true. Additional reference: how to always show headers in RowsFragment
I am trying to change the color of the table rows when I set a boolean.
So I have this code:
boolean searchmode = false;
....
columns.forEach(c -> c.setCellFactory(column -> {
return new TableCell<ShowableInWarenkorb, String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? null : item);
if (searchmode) {
getStyleClass().add("searchmode");
} else{
getStyleClass().remove("searchmode");
}
}
};
}));
This CSS:
.searchmode {
-fx-background-color: rgba(153,153,153,0.3);
})
And then I switch searchmode eventually in my code before I am updating the table contents.
But the color does not change immediatley, sometimes I have to click a little bit around before it changes, how can I trigger it manually?
From your code, it looks like you want to apply this to all cells in the table. You can do this without a cell factory at all (though you may need one for other purposes).
Do
PseudoClass searchmodePseudoClass = PseudoClass.getPseudoClass("searchmode");
and then when you change the value of searchmode, do
table.pseudoClassStateChanged(searchmode);
In your css, do
.table-view:searchmode .table-cell {
-fx-background-color: rgba(153,153,153,0.3);
}
If you want to "automate" the update to the pseudoclass state, use a boolean property and add a listener:
private final BooleanProperty searchmode = new SimpleBooleanProperty(false);
public final boolean isSearchmode() {
return searchmodeProperty().get();
}
public final void setSearchmode(boolean searchmode) {
searchmodeProperty().set(searchmode);
}
public BooleanProperty searchmodeProperty() {
return searchmode ;
}
Then if you add the listener
searchmode.addListener((obs, wasSearchmode, isNowSearchmode) ->
table.pseudoClassStateChanged(searchmodePseudoClass, isNowSearchmode));
everything will be wired automatically so the table changes whenever you call setSearchmode(...).
Im set custom CssResource for DataGrid.
First column in table is ordered column with specific style.
So when row is selected i need set another specific style for the order column.
Something like that:
You can override .getCellStyleNames method for your column:
Column<Object, String> numberColumn = new Column<Object, String>(new TextCell()) {
#Override
public String getCellStyleNames(Context context, Object object) {
if (selectionModel.isSelected(object)) {
return "boldStyle";
}
};
Try with AbstractHasData#addCellPreviewHandler().
dataGrid.addCellPreviewHandler(new Handler<T>() {
#Override
public void onCellPreview(CellPreviewEvent<T> event) {
if ("click".equals(event.getNativeEvent().getType())) {
table.getRowElement(event.getIndex()).getCells().getItem(0).getStyle()
.setBackgroundColor("#444444");
}
}
});
Note: This code is for SingleSelectionModel. If you need it for MultiSelectionModel then do same thing for all selected rows.