This is the first time I've had to work with JavaFX (and hopefully the last) so I don't exactly understand how everything works. I'll try to sum where I am briefly
I am trying to make my table highlight duplicate cells on a specific column
I need editable cells and no TableCell extensions I've come across work, I've been spending most of today trying to fix their bugs to no avail. I've given up on that approach.
I found TextFieldTableCell but that does not allow me to extend and override functions like updateItem. At this point I have no interest in re-implementing any of this functionality.
Currently what I do is the following:
CollectionName.setCellValueFactory(new PropertyValueFactory<>("CollectionName"));
CollectionName.setCellFactory(EditingCell.<Item>forTableColumn(this)); //At the moment this just passes though TextFieldTableCell, the parameter is totally inconsequential
CollectionName.setOnEditCommit((CellEditEvent<Item, String> t) ->
{
((Item) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setCollectionName(t.getNewValue());
System.out.println("Set on edit commit");
if(isDuplicateName(t.getNewValue()))
{
t.getTableView().getColumns().get(t.getTablePosition().getColumn()).getStyleClass().add("duplicate-cell");
System.out.println("Duplicate");
}
else
{
t.getTableView().getColumns().get(t.getTablePosition().getColumn()).getStyleClass().remove("duplicate-cell");
System.out.println("Not duplicate");
}
});
This functions as intended but highlights the entire column. I need it to highlight only the specific cell. I wish there was a way to simply call myTable.getCell(x,y).getStyleClass().add("duplicate-cell") or something. I mean it is a table after all...
The solution to any problem involving changing the appearance of table cells based on certain state of the cell's item, and other data, is always to use a cell factory which returns a cell that updates its appearance accordingly.
The problem with the approach you are trying is that you are overlooking the fact that the table view reuses cells. For example, if the table contains a large amount of data and the user scrolls, new cells will not be created but cells that are scrolled out of view will be reused for the new items that scroll into view. Since you don't update the style of the cell when this happens, scrolling will make the wrong cells highlighted.
Here the logic is a little tricky as each cell essentially has to observe all values in the column (whether they are currently displayed or not). I think the easiest solution here is to independently maintain an ObservableSet that keeps a list of duplicate entries, and have the cell observe that. Here's an implementation. You can probably factor this out into a separate class for the cell factory (or something convenient) to make it more elegant and reusable.
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class HighlightDuplicateTableCells extends Application {
// create an observable list that fires events if the dataProperty of any elements change:
private final ObservableList<Item> items =
FXCollections.observableArrayList(item -> new Observable[]{item.dataProperty()});
// collection of strings that are duplicated in the data properties of all the items:
private final ObservableSet<String> duplicateData = FXCollections.observableSet();
private static final PseudoClass DUPLICATE_PC = PseudoClass.getPseudoClass("duplicate");
private final StringConverter<String> identityStringConverter = new StringConverter<String>() {
#Override
public String toString(String object) {
return object;
}
#Override
public String fromString(String string) {
return string;
}
};
#Override
public void start(Stage primaryStage) {
// listener to maintain collection of duplicates:
items.addListener((Change<? extends Item> change) -> updateDuplicateData());
TableView<Item> table = new TableView<>();
table.setEditable(true);
table.setItems(items);
TableColumn<Item, Number> idColumn = new TableColumn<>("Id");
idColumn.setCellValueFactory(cellData -> new SimpleIntegerProperty(cellData.getValue().getId()));
TableColumn<Item, String> dataColumn = new TableColumn<>("Data");
dataColumn.setCellValueFactory(cellData -> cellData.getValue().dataProperty());
dataColumn.setCellFactory(tc -> {
TextFieldTableCell<Item, String> cell = new TextFieldTableCell<Item, String>(identityStringConverter) {
// boolean binding that indicates if the current item is contained in the duplicateData set:
private BooleanBinding duplicate = Bindings.createBooleanBinding(
() -> duplicateData.contains(getItem()),
duplicateData, itemProperty());
// anonymous constructor just updates CSS pseudoclass if above binding changes:
{
duplicate.addListener((obs, wasDuplicate, isNowDuplicate) ->
pseudoClassStateChanged(DUPLICATE_PC, isNowDuplicate));
}
};
return cell ;
});
table.getColumns().add(idColumn);
table.getColumns().add(dataColumn);
// note best to minimize changes to items.
// creating a temp list and using items.setAll(...) achieves this:
List<Item> tmp = new ArrayList<>();
for (int i = 1 ; i <= 70; i++) {
char c = (char)('#' + (i % 60));
String data = Character.toString(c) ;
tmp.add(new Item(i, data));
}
items.setAll(tmp);
Scene scene = new Scene(table, 600, 600);
scene.getStylesheets().add("duplicate-cell-example.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private void updateDuplicateData() {
// TODO: may not be most efficient implementation
// all data:
List<String> data = items.stream().map(Item::getData).collect(Collectors.toList());
// unique data:
Set<String> uniqueData = new HashSet<>(data);
// remove unique values from data:
uniqueData.forEach(data::remove);
// remaining values are duplicates: replace contents of duplicateData with these:
duplicateData.clear();
duplicateData.addAll(data);
}
public static class Item {
private final int id ;
private final StringProperty data = new SimpleStringProperty();
public Item(int id, String data) {
this.id = id ;
setData(data);
}
public final StringProperty dataProperty() {
return this.data;
}
public final String getData() {
return this.dataProperty().get();
}
public final void setData(final String data) {
this.dataProperty().set(data);
}
public int getId() {
return id ;
}
}
public static void main(String[] args) {
launch(args);
}
}
and the duplicate-cell-example.css:
.table-cell:duplicate {
-fx-background-color: -fx-background ;
-fx-background: red ;
}
This is basically James_D's approach, but it improves the time required for updates from Ω(n²) worst case (n = list size) to O(m) where m is the number of changes (1 for updates of a property; the number of elements added/removed on a list update).
This performance is achieved by storing the number of occurances in a ObservableMap<String, Integer>:
private final ObservableMap<String, Integer> valueOccuranceCounts = FXCollections.observableHashMap();
private final ChangeListener<String> changeListener = (observable, oldValue, newValue) -> {
valueOccuranceCounts.computeIfPresent(oldValue, REMOVE_UPDATER);
valueOccuranceCounts.merge(newValue, 1, ADD_MERGER);
};
private static final BiFunction<Integer, Integer, Integer> ADD_MERGER = (oldValue, newValue) -> oldValue + 1;
private static final BiFunction<String, Integer, Integer> REMOVE_UPDATER = (key, value) -> {
int newCount = value - 1;
// remove mapping, if the value would become 0
return newCount == 0 ? null : newCount;
};
private final ListChangeListener<Item> listChangeListener = (ListChangeListener.Change<? extends Item> c) -> {
while (c.next()) {
if (c.wasRemoved()) {
for (Item r : c.getRemoved()) {
// decrease count and remove listener
this.valueOccuranceCounts.computeIfPresent(r.getData(), REMOVE_UPDATER);
r.dataProperty().removeListener(this.changeListener);
}
}
if (c.wasAdded()) {
for (Item a : c.getAddedSubList()) {
// increase count and add listener
this.valueOccuranceCounts.merge(a.getData(), 1, ADD_MERGER);
a.dataProperty().addListener(this.changeListener);
}
}
}
};
private final ObservableList<Item> items;
{
items = FXCollections.observableArrayList();
items.addListener(listChangeListener);
}
private static final PseudoClass DUPLICATE = PseudoClass.getPseudoClass("duplicate");
private static final String FIRST_COLUMN_CLASS = "first-column";
#Override
public void start(Stage primaryStage) throws Exception {
TableView<Item> tableView = new TableView<>(items);
// tableView.getSelectionModel().setCellSelectionEnabled(true);
tableView.setEditable(true);
TableColumn<Item, String> column = new TableColumn<>("data");
column.setCellValueFactory(cellData -> cellData.getValue().dataProperty());
column.setCellFactory(col -> new TextFieldTableCell<Item, String>() {
// boolean binding that indicates if the current item is contained in the duplicateData set:
private final BooleanBinding duplicate = Bindings.createBooleanBinding(
() -> valueOccuranceCounts.getOrDefault(getItem(), 1) >= 2,
valueOccuranceCounts, itemProperty());
// anonymous constructor just updates CSS pseudoclass if above binding changes:
{
duplicate.addListener((observable, oldValue, newValue)
-> pseudoClassStateChanged(DUPLICATE, newValue));
}
});
TableColumn<Item, Number> idColumn = new TableColumn<>("id");
idColumn.setCellValueFactory(cellData -> new SimpleIntegerProperty(cellData.getValue().getId()));
tableView.getColumns().addAll(idColumn, column);
tableView.getColumns().addListener((Observable observable) -> {
// keep style class marking the cells of the column as
// belonging to the first column up to date
if (tableView.getColumns().get(0) == column) {
if (!column.getStyleClass().contains(FIRST_COLUMN_CLASS)) {
column.getStyleClass().add(FIRST_COLUMN_CLASS);
}
} else {
column.getStyleClass().remove(FIRST_COLUMN_CLASS);
}
});
// note best to minimize changes to items.
// creating a temp list and using items.setAll(...) achieves this:
final int count = 70;
List<Item> tmp = Arrays.asList(new Item[count]);
for (int i = 0; i < count; i++) {
tmp.set(i, new Item(Integer.toString(i % 60)));
}
items.setAll(tmp);
Scene scene = new Scene(tableView);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Item {
private static int counter = 0;
private final StringProperty data;
private final int id = counter++;
public Item(String data) {
this.data = new SimpleStringProperty(data);
}
public final StringProperty dataProperty() {
return this.data;
}
public final String getData() {
return this.dataProperty().get();
}
public final void setData(final String data) {
this.dataProperty().set(data);
}
public int getId() {
return id ;
}
}
style.css
.table-row-cell:filled .table-cell:duplicate {
-fx-background: yellow;
-fx-background-color: -fx-table-cell-border-color, -fx-background;
}
.table-view:focused .table-row-cell:filled .table-cell:duplicate:focused {
-fx-background-color: -fx-background, -fx-cell-focus-inner-border, -fx-background;
}
/* keep use the same background colors normally used for focused table rows */
.table-view:focused .table-row-cell:filled:focused .table-cell:duplicate {
-fx-background-color: -fx-background, -fx-cell-focus-inner-border, -fx-background;
/* frame only at top & bottom sides */
-fx-background-insets: 0, 1 0 1 0, 2 0 2 0;
}
.table-view:focused .table-row-cell:filled:focused .table-cell.first-column:duplicate {
/* frame only for top, left and bottom sides*/
-fx-background-insets: 0, 1 0 1 1, 2 0 2 2;
}
.table-row-cell:filled .table-cell:duplicate:selected,
.table-row-cell:filled:selected .table-cell:duplicate {
-fx-background: turquoise;
}
Note that some parts (creating & filling the table, creating the column) are copied from #James_D's answer, since it's simply best practice to do it this way.
Related
We try to achieve the following:
When a node gets selected in a JavaFX TreeTableView, also "the path to the root", i.e., the parent, the grandparent, and so on should get selected. Selected in this case means highlighted with a different background color, see the image (in the example, the node on Level 2 has been clicked by the user).
Is there a built-in function how to achieve this?
We tried using CSS but did not succeed.
There's no "built-in function" to do this. Use a row factory on the tree table view to create rows that observe the selected item, and set a pseudoclass on the row accordingly.
For example:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import javafx.stage.Stage;
public class TreeTableViewHighlightSelectionPath extends Application {
#Override
public void start(Stage primaryStage) {
TreeTableView<Item> table = new TreeTableView<Item>();
PseudoClass ancestorOfSelection = PseudoClass.getPseudoClass("ancestor-of-selection");
table.setRowFactory(ttv -> new TreeTableRow<Item>() {
{
table.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSelection, newSelection) -> updateStyleClass());
}
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
updateStyleClass();
}
private void updateStyleClass() {
pseudoClassStateChanged(ancestorOfSelection, false);
TreeItem<Item> treeItem = table.getSelectionModel().getSelectedItem();
if (treeItem != null) {
for (TreeItem<Item> parent = treeItem.getParent() ; parent != null ; parent = parent.getParent()) {
if (parent == getTreeItem()) {
pseudoClassStateChanged(ancestorOfSelection, true);
break ;
}
}
}
}
});
TreeTableColumn<Item, String> itemCol = new TreeTableColumn<>("Item");
itemCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getValue().getName()));
table.getColumns().add(itemCol);
TreeTableColumn<Item, Number> valueCol = new TreeTableColumn<>("Value");
valueCol.setCellValueFactory(cellData -> cellData.getValue().getValue().valueProperty());
table.getColumns().add(valueCol);
table.setRoot(createRandomTree());
Scene scene = new Scene(table);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeItem<Item> createRandomTree() {
TreeItem<Item> root = new TreeItem<>(new Item("Item 1", 0));
Random rng = new Random();
List<TreeItem<Item>> items = new ArrayList<>();
items.add(root);
for (int i = 2 ; i <= 20 ; i++) {
TreeItem<Item> item = new TreeItem<>(new Item("Item "+i, rng.nextInt(1000)));
items.get(rng.nextInt(items.size())).getChildren().add(item);
items.add(item);
}
return root ;
}
public static class Item {
private final String name ;
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
this.name = name ;
setValue(value);
}
public String getName() {
return name ;
}
public IntegerProperty valueProperty() {
return value ;
}
public final int getValue() {
return valueProperty().get();
}
public final void setValue(int value) {
valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
Now you can just style the "ancestor of a selected node" in CSS:
File style.css:
.tree-table-row-cell:ancestor-of-selection {
-fx-background: -fx-selection-bar;
-fx-table-cell-border-color: derive(-fx-selection-bar, 20%);
}
(You may want to modify the CSS to get better control, e.g. set different colors for selected rows in a non-focused table, etc. See the default stylesheet for details on the default style.)
Here's a screenshot of the above test app:
I would like to add a handler to the ComboBox inside a ComboBoxTableCell in a TableView in JavaFX8. I can see that there is a private ComboBox value in the ComboBoxTableCell class, yet I have no idea how to access it. I tell the column to use a ComboBoxTableCell via the setCellFactory method. Is there any way to get the ComboBox?
EDIT: I want to add a listener to the ComboBox which enables choosing items by entering keys. I already have one for a normal ComboBoxand I would like to reuse the same for the ComboBox in the TableCell.
To my knowledge there's no way to get the reference to the ComboBox of the ComboBoxTableCell. If that's true, it's not possible to add a listener to it.
An alternate approach would be to create your own custom cell containing a ComboBox. With this approach, you can manipulate the ComboBox in any way you'd like.
import java.util.function.BiConsumer;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class ComboBoxTable extends Application {
#Override
public void start(Stage stage) {
int numOfCols = 2;
ObservableList<ObservableList<String>> tableData = FXCollections.observableArrayList();
// Generate dummy data.
for (int i = 0; i < 10; i++) {
ObservableList<String> row = FXCollections.observableArrayList();
for (int j = 0; j < numOfCols; j++)
row.add("Row" + i + "Col" + j);
tableData.add(row);
}
TableView<ObservableList<String>> table = new TableView<ObservableList<String>>();
// Add columns to the table.
for (int i = 0; i < numOfCols; i++) {
final int j = i;
// The fourth argument in the method, the BiConsumer, might require
// an explanation. Basically we are saying that when the BiConsumer
// are given an ObservableList<String> and a String, we set the
// value of the String as the value of the element at position "j"
// of the row, where "j" will be the column index.
table.getColumns().add(addComboBoxColumn(i, "Column " + i, row -> new SimpleStringProperty(row.get(j)),
(row, newText) -> row.set(j, newText)));
}
table.getItems().addAll(tableData);
Scene scene = new Scene(table);
stage.setScene(scene);
stage.show();
}
/**
* Returns a TableColumn with ComboBoxCells.
*/
private TableColumn<ObservableList<String>, String> addComboBoxColumn(int index, String name,
Function<ObservableList<String>, ObservableValue<String>> property,
BiConsumer<ObservableList<String>, String> updater) {
TableColumn<ObservableList<String>, String> col = new TableColumn<ObservableList<String>, String>(name);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(e -> new ComboBoxCell(updater, index));
return col;
}
/**
* A TableCell with a ComboBox in it.
*/
public class ComboBoxCell extends TableCell<ObservableList<String>, String> {
private ComboBox<String> comboBox = new ComboBox<String>();
/**
* #param updater
* The updater makes sure that the cell value corresponds
* with the value in the ComboBox.
* #param colIndex
* The index of this column.
*/
public ComboBoxCell(BiConsumer<ObservableList<String>, String> updater, int colIndex) {
comboBox.setEditable(true);
comboBox.getEditor().textProperty().addListener((old, oldValue, newValue) -> {
if (getIndex() >= 0) {
// We provide the BiConsumer.accept() with an
// ObservableList<String> and a String. The BiConsumer will
// do the operation specified in the definition we provided
// in addColumn() using these two objects.
updater.accept(getTableView().getItems().get(getIndex()), (String) newValue);
}
});
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
// If we don't check if this value is the same as the old one,
// the cursor is moved to the beginning of the editor every time
// anything is typed.
if (!item.equals(comboBox.getEditor().getText())) {
comboBox.getEditor().setText(item);
}
setGraphic(comboBox);
}
}
}
public static void main(String[] args) {
launch();
}
}
an other possible solution is to add a StringConverter<>() to your ComboBoxTableCell<R, T>() for correct String display of an object and add a changeListener to your ObjectProperty
TableView<ParentObject> table = new TableView<>();
table.setEditable(true);
TableColumn<ParentObject, Item> col = new TableColumn<>("name");
col.setEditable(true);
col.setCellValueFactory(cellData -> {
ObjectProperty prop = new SimpleObjectProperty<Item>(cellData.getValue().getItem());
prop.addListener((observable, oldVal, newVal) -> {
//do something for example set the Object in getValue()
cellData.getValue().setItem(newVal);
});
return prop;
});
final StringConverter<Item> converter = new StringConverter<>() {
#Override
public Item fromString(final String string) {
return null;
}
#Override
public String toString(final Item object) {
return object.getLabel();
}
};
// get your list of possible Objects to swap
List<Item> itemList = Database.getAllItems();
col.setCellFactory(c -> new ComboBoxTableCell(converter, FXCollections.observableArrayList(itemList)));
table.getColumns().add(col);
The Object SimpleObjectProperty<> can be replaced with other javafx.beans.properties for Example StringProperty, IntegerProperty, BooleanProperty or ListProperty
I'm aware that this isn't the 100% matching answer to the question but this helped me in my issue searching for the right answer to the problem
I have a TableView in SelectionMode.MULTIPLE. Using a ListChangeListener I'm able to catch the selection of multiple rows (by pressing Shift).
However my solution only works if the items are being selected in the same column OR in the area without columns. Gif for illustration with 4 examples:
OK: Selecting 3 items using Shift in State column
OK: Selecting 4 items using Shift in Idx column
OK: Selecting 4 items using Shift starting from State column to area without columns
Error: Trying to select 4 items using Shift starting from State column to Data Item column
The problem seems to be that the SelectedItems-list is apparently empty in the last example. I'd really appreciate your help regarding this issue.
Here is my approach:
ObservableList<DataRowModel> dataRows = FXCollections.observableArrayList();
dataRows.addAll(dataSetModel.getRows());
tableDataRow.setItems(dataRows);
tableDataRowStateColumn.setCellValueFactory(f -> f.getValue().getState());
tableDataRow.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableDataRow.getSelectionModel().getSelectedItems()
.addListener((ListChangeListener.Change<? extends DataRowModel> c) -> {
while (c.next()) {
c.getRemoved().stream().forEach(remitem -> remitem.setSelected(false));
c.getAddedSubList().stream().forEach(additem -> additem.setSelected(true));
System.out.println(c.getList()); //Empty [] when selected using different columns
}
});
Just for a better understanding of my code: setSelected(...) sets a BooleanProperty on my DataRowModel which is bound to the State-Column.
Without context the reason for using this selected-property seems to be quite silly. However, there are various other fragments of code with ChangeListeners bound to the selected-property.
SSCCE ready to run:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class TableViewSample extends Application {
private TableView<DataRowModel> tableDataRow = new TableView<DataRowModel>();
private TableColumn<DataRowModel, String> tableDataRowNameColumn = new TableColumn<>("Data Item");
private TableColumn<DataRowModel, String> tableDataRowStateColumn = new TableColumn<>("State");
private final ObservableList<DataRowModel> dataRows =
FXCollections.observableArrayList(
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false)
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(500);
stage.setHeight(500);
tableDataRow.setItems(dataRows);
tableDataRowNameColumn.setCellValueFactory(f -> f.getValue().getName());
tableDataRowStateColumn.setCellValueFactory(f -> f.getValue().getState());
tableDataRow.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableDataRow.getSelectionModel().getSelectedItems()
.addListener((ListChangeListener.Change<? extends DataRowModel> c) -> {
while (c.next()) {
c.getRemoved().stream().forEach(remitem -> remitem.setSelected(false));
c.getAddedSubList().stream().forEach(additem -> additem.setSelected(true));
}
});
tableDataRow.getColumns().addAll(tableDataRowNameColumn, tableDataRowStateColumn);
((Group) scene.getRoot()).getChildren().addAll(tableDataRow);
stage.setScene(scene);
stage.show();
}
public static class DataRowModel {
private StringProperty name = new SimpleStringProperty(this, "name", "");
private BooleanProperty selected = new SimpleBooleanProperty(this, "selected", true);
private StringProperty state = new SimpleStringProperty(this, "state", "");
public DataRowModel(String name, boolean selected) {
this.name.setValue(name);
this.selected.setValue(selected);
this.selected.addListener((observable, oldVal, newVal) -> {
getState(); // Refresh State value
});
}
public StringProperty getName() {
return name;
}
public BooleanProperty isSelected() {
return selected;
}
public void setSelected(boolean selected) {
if (this.selected.getValue() != selected)
this.selected.setValue(selected);
}
public StringProperty getState() {
String stateStr = "";
if (selected.getValue())
stateStr += "Selected";
state.setValue(stateStr);
return state;
}
}
}
I was able to generate this by editing the Oracle's Person tableview example.
This is a bug, filed as https://bugs.openjdk.java.net/browse/JDK-8096787, and fixed in version 8u60 which is expected to be released in August 2015.
I'm trying to select all child check boxes from a parent root. The action is invoked when the parent check box is selected.
Here's the pseudo/modified/shortened set up with scenebuilder:
#FXML
private TreeTableView<Info> testTable;
#FXML
private TreeTableColumn<Info, Boolean> checkBoxCol;
Model:
public class Info{
private final BooleanProperty onHold;
public Info(){
this.onHold = new SimpleBooleanProperty(false);
}
public Boolean getOnHold(){
return onHold.get();
}
public void setOnHold(Boolean onHold) {
this.onHold.set(onHold);
}
public BooleanProperty onHoldProperty(){
return onHold;
}
}
Controller:
checkBoxCol.setCellValueFactory(new TreeItemPropertyValueFactory("onHold"));
checkBoxCol.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(checkBoxCol));
End Result would look like this (when parent nodes are clicked):
I tried onEditCommit/start/cancel, but those seem to only affect the cell and not the checkboxes. I am not exactly sure how to get a listener for only the parent nodes so that it can check all values underneath (if they have children). If it's too difficult to only allow just the parent nodes to have the listener, then all the checkbox can have listeners and I can simply check if there are children with:node.getChildren().size()
You should be able to manage this entirely in the model.
The TreeTableView consists of a TreeItem<Info> root with a bunch of descendent nodes. Just arrange that whenever you create the tree items, you add a listener to the properties:
private TreeItem<Info> createTreeItem(Info info) {
TreeItem<Info> item = new TreeItem<>(info);
info.onHoldProperty().addListener((obs, wasOnHold, isNowOnHold) -> {
if (isNowOnHold) {
item.getChildren().forEach(child -> child.getValue().setOnHold(true));
}
});
return item ;
}
Complete example:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.CheckBoxTreeTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TreeTableViewInheritableCheckBoxes extends Application {
#Override
public void start(Stage primaryStage) {
TreeTableView<Info> table = new TreeTableView<>();
table.setEditable(true);
TreeTableColumn<Info, Boolean> infoCol = new TreeTableColumn<>("Info");
infoCol.setPrefWidth(200);
infoCol.setCellValueFactory(cellData -> cellData.getValue().getValue().onHoldProperty());
infoCol.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(infoCol));
table.getColumns().add(infoCol);
TreeItem<Info> root = createTreeItem(new Info());
buildTree(root, 0);
table.setRoot(root);
primaryStage.setScene(new Scene(new BorderPane(table), 250, 400));
primaryStage.show();
}
private void buildTree(TreeItem<Info> parent, int depth) {
if (depth > 2) return ;
for (int i = 0; i < 5; i++) {
TreeItem<Info> item = createTreeItem(new Info());
parent.getChildren().add(item);
buildTree(item, depth + 1);
}
}
private TreeItem<Info> createTreeItem(Info info) {
TreeItem<Info> item = new TreeItem<>(info);
info.onHoldProperty().addListener((obs, wasOnHold, isNowOnHold) -> {
if (isNowOnHold) {
item.getChildren().forEach(child -> child.getValue().setOnHold(true));
}
});
return item ;
}
public static class Info {
private final BooleanProperty onHold;
public Info(){
this.onHold = new SimpleBooleanProperty(false);
}
public Boolean getOnHold(){
return onHold.get();
}
public void setOnHold(Boolean onHold) {
this.onHold.set(onHold);
}
public BooleanProperty onHoldProperty(){
return onHold;
}
}
public static void main(String[] args) {
launch(args);
}
}
I'm using GXT 3 Grid with InlineEdit mode following (more or less) the example code on their site. I don't think there is a way to get the check box cell to fire the 'EditComplete' event and if so, I'm not sure how I would, upon receiving it, disable the date cell on that same row. Just look for the comment: "// not firing for checkbox:" in the code below.
The following code works in an Eclipse web application project - you just need to use it in your 'onModuleLoad' method as demonstrated here:
public void onModuleLoad() {
GridInlineEditingTest j = new GridInlineEditingTest();
}
Here's the code:
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.google.gwt.cell.client.DateCell;
import com.google.gwt.core.client.GWT;
import com.google.gwt.editor.client.Editor.Path;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.sencha.gxt.cell.core.client.form.CheckBoxCell;
import com.sencha.gxt.core.client.ValueProvider;
import com.sencha.gxt.data.shared.ListStore;
import com.sencha.gxt.data.shared.ModelKeyProvider;
import com.sencha.gxt.data.shared.PropertyAccess;
import com.sencha.gxt.data.shared.Store;
import com.sencha.gxt.widget.core.client.FramedPanel;
import com.sencha.gxt.widget.core.client.button.TextButton;
import com.sencha.gxt.widget.core.client.container.BoxLayoutContainer.BoxLayoutPack;
import com.sencha.gxt.widget.core.client.container.VerticalLayoutContainer;
import com.sencha.gxt.widget.core.client.container.VerticalLayoutContainer.VerticalLayoutData;
import com.sencha.gxt.widget.core.client.container.Viewport;
import com.sencha.gxt.widget.core.client.event.CompleteEditEvent;
import com.sencha.gxt.widget.core.client.event.CompleteEditEvent.CompleteEditHandler;
import com.sencha.gxt.widget.core.client.event.SelectEvent;
import com.sencha.gxt.widget.core.client.event.SelectEvent.SelectHandler;
import com.sencha.gxt.widget.core.client.form.CheckBox;
import com.sencha.gxt.widget.core.client.form.DateField;
import com.sencha.gxt.widget.core.client.form.DateTimePropertyEditor;
import com.sencha.gxt.widget.core.client.grid.ColumnConfig;
import com.sencha.gxt.widget.core.client.grid.ColumnModel;
import com.sencha.gxt.widget.core.client.grid.Grid;
import com.sencha.gxt.widget.core.client.grid.Grid.GridCell;
import com.sencha.gxt.widget.core.client.grid.GridView;
import com.sencha.gxt.widget.core.client.grid.editing.GridEditing;
import com.sencha.gxt.widget.core.client.grid.editing.GridInlineEditing;
public class GridInlineEditingTest {
public GridInlineEditingTest() {
VerticalLayoutContainer vlc = new VerticalLayoutContainer();
vlc.add(createGrid(), new VerticalLayoutData(1, 1));
Viewport vp = new Viewport();
vp.add(vlc);
RootPanel.get().add(vp);
}
interface PlaceProperties extends PropertyAccess<Plant> {
ValueProvider<Plant, Date> available();
#Path("id")
ModelKeyProvider<Plant> key();
ValueProvider<Plant, String> name();
ValueProvider<Plant, Boolean> indoor();
}
private static final PlaceProperties properties = GWT.create(PlaceProperties.class);
protected Grid<Plant> grid;
private FramedPanel panel;
private ListStore<Plant> store;
private DateField dateField;
public Widget createGrid() {
if (panel == null) {
ColumnConfig<Plant, String> nameCol = new ColumnConfig<Plant, String>( properties.name(), 220, "Name" );
ColumnConfig<Plant, Date> dateCol = new ColumnConfig<Plant, Date>( properties.available(), 95, "Date" );
ColumnConfig<Plant, Boolean> indorCol = new ColumnConfig<Plant, Boolean>( properties.indoor(), 55, "Indoor");
// display formatting
DateCell dateCell = new DateCell(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT));
dateCol.setCell(dateCell);
// display a checkbox in the gridview
indorCol.setCell(new CheckBoxCell());
List<ColumnConfig<Plant, ?>> l = new ArrayList<ColumnConfig<Plant, ?>>();
l.add(nameCol);
l.add(dateCol);
l.add(indorCol);
ColumnModel<Plant> columns = new ColumnModel<Plant>(l);
store = new ListStore<Plant>(properties.key());
store.setAutoCommit(false);
store.addAll(getPlants());
GridView<Plant> gridView = new GridView<Plant>();
grid = new Grid<Plant>(store, columns, gridView);
grid.getView().setAutoExpandColumn(nameCol);
// EDITING//
final GridEditing<Plant> editing = new GridInlineEditing<Plant>(grid);
dateField = new DateField(new DateTimePropertyEditor(DateTimeFormat.getFormat(PredefinedFormat.DATE_SHORT)));
dateField.setClearValueOnParseError(false);
editing.addEditor(dateCol, dateField);
CheckBox checkField = new CheckBox();
editing.addEditor(indorCol, checkField);
editing.addCompleteEditHandler( new CompleteEditHandler<Plant>(){
// not firing for checkbox:
#Override
public void onCompleteEdit(CompleteEditEvent<Plant> event) {
GridCell cell = event.getEditCell();
int row = cell.getRow();
int col = cell.getCol();
System.out.println("got here. row "+row+", col "+col);
}
});
panel = new FramedPanel();
panel.setHeadingText("Editable Grid Example");
panel.setPixelSize(600, 400);
panel.addStyleName("margin-10");
VerticalLayoutContainer con = new VerticalLayoutContainer();
con.setBorders(true);
con.add(grid, new VerticalLayoutData(1, 1));
panel.setWidget(con);
panel.setButtonAlign(BoxLayoutPack.CENTER);
panel.addButton(new TextButton("Reset", new SelectHandler() {
#Override
public void onSelect(SelectEvent event) {
store.rejectChanges();
}
}));
panel.addButton(new TextButton("Save", new SelectHandler() {
#Override
public void onSelect(SelectEvent event) {
store.commitChanges();
}
}));
}
return panel;
}
private static int AUTO_ID = 0;
public class Plant {
private DateTimeFormat df = DateTimeFormat.getFormat("MM/dd/y");
private int id;
private String name;
private String light;
private double price;
private Date available;
private boolean indoor;
private String color;
private int difficulty;
private double progress;
public Plant() {
id = AUTO_ID++;
difficulty = (int) (Math.random() * 100);
progress = Math.random();
}
public Plant(String name, String light, double price, String available, boolean indoor) {
this();
setName(name);
setLight(light);
setPrice(price);
setAvailable(df.parse(available));
setIndoor(indoor);
}
public int getId() { return id; }
public double getProgress() { return progress; }
public String getColor() { return color; }
public int getDifficulty() { return difficulty; }
public Date getAvailable() { return available; }
public String getLight() { return light; }
public String getName() { return name; }
public double getPrice() { return price; }
public boolean isIndoor() { return indoor; }
public void setId(int id) { this.id = id; }
public void setProgress(double progress) { this.progress = progress; }
public void setAvailable(Date available) { this.available = available; }
public void setDifficulty(int difficulty) { this.difficulty = difficulty; }
public void setColor(String color) { this.color = color; }
public void setIndoor(boolean indoor) { this.indoor = indoor; }
public void setLight(String light) { this.light = light; }
public void setName(String name) { this.name = name; }
public void setPrice(double price) { this.price = price; }
#Override
public String toString() {
return name != null ? name : super.toString();
}
}
public List<Plant> getPlants() {
List<Plant> plants = new ArrayList<Plant>();
plants.add(new Plant("Bloodroot", "Mostly Shady", 2.44, "03/15/2006", true));
plants.add(new Plant("Columbine", "Shade", 9.37, "03/15/2006", true));
plants.add(new Plant("Marsh Marigold", "Mostly Sunny", 6.81, "05/17/2006", false));
plants.add(new Plant("Cowslip", "Mostly Shady", 9.90, "03/06/2006", true));
plants.add(new Plant("Dutchman's-Breeches", "Mostly Shady", 6.44, "01/20/2006", true));
plants.add(new Plant("Ginger, Wild", "Mostly Shady", 9.03, "04/18/2006", true));
return plants;
}
}
thanks. and have a great day!!
You are setting a checkbox cell in the column, and then also attaching a field as an inline editor for the column. So if the user clicks the checkbox (cell), you are expecting that click to be ignored, but instead a checkbox (field) to show up over it, which the user may then click?
Instead what is happening is that the checkbox (cell) is reporting that it is using that click event to do something useful - it is changing its value. As a result, the grid editing mechanism ignores the click, so the checkbox (field) never goes into edit mode, and so of course it doesn't complete edit mode.
What are you trying to achieve by making it the purpose of two different checkboxes to be drawn in the same place, and function differently? If you are trying to use the CheckBoxCell instance as a way to always draw the checkbox symbol in the grid cell, there are two main choices:
Skip the CheckBox field in the inline editing, and just let the cell take care of it. It will not fire the editing events, but it will still directly interact with the store. You can listen to the cell's events if you need to, or just to the record change events from the store, or you can subclass the cell to modify behavior.
Removing the event handing guts of the CheckBoxCell to prevent it from handling the event - this may be as simple as overriding onBrowserEvent to do nothing, though I suspect that you actually will want to prevent its check changing behavior entirely so that the Inline Editing version takes care of it
Finally, remember that the purpose of inline editing is to keep the grid from being a mass of fields, and to make it only draw those fields when the user actually interacts with it. This means that the user must first click a field to get something like a checkbox to show up, then interface with the field to change it. Looking one more time at the CheckBox field in an inline editable grid (though this time with a custom cell) at http://www.sencha.com/examples/#ExamplePlace:inlineeditablegrid you'll see that this means two clicks to change a value and get the CompleteEditing event (as well as the various other field change events) that you are after - is this really what you have in mind?
As per the Source Code of CheckBoxCell#isEditing() that says:
A checkbox is never in "edit mode". There is no intermediate state between checked and unchecked.
Find the alternate solution here How to get the row index of selected checkbox on grid GXT.
Please have a look at GXT checkbox in grid