CheckBoxTreeItems always independent, not rendered properly. Why? - java

I am new to JavaFX and I am trying to create a simple TreeTableView, containing a
single Boolean column and is rendered with a CheckBoxTreeTableCell.
The problem I am having is that the two CheckBoxTreeItems seem independent (selecting
the root doesn't select the child and the other way around). I even try setting the
independancy manually (see commented code) but it makes no difference.
The documentation for CheckBoxTreeItems says that "By default, CheckBoxTreeItem instances are dependent", which doesn't seem to work for me.
Also, I am expecting toString() value of the Model class to be shown as checkboxes'
texts but no text is drawn, only empty checkboxes. Why is this?
And finally, it is possible to set a graphic node for a CheckBoxTreeItem, and this
node is then shown to the left of the CheckBoxTreeItem. Would it be possible to have
it drawn between the checkbox and the checkbox text instead? Something like:
[x][graphic_node]A simple checkbox text
I am using JDK 1.8.0_40
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.scene.Scene;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.CheckBoxTreeTableCell;
import javafx.stage.Stage;
public final class CheckBoxTreeItemTest extends Application {
private Stage stage;
public static void main(String[] args) {
launch(args);
}
#Override
public final void start(final Stage stage) throws Exception {
this.stage = stage;
final CheckBoxTreeItem<Model> root = new CheckBoxTreeItem<>(new Model("Root"));
final CheckBoxTreeItem<Model> parent = new CheckBoxTreeItem<>(new Model("Parent"));
final CheckBoxTreeItem<Model> child = new CheckBoxTreeItem<>(new Model("Child"));
//Manually setting independence makes no difference
/*parent.setIndependent(false);
child.setIndependent(false);
root.setIndependent(false);*/
parent.getChildren().add(child);
root.getChildren().add(parent);
final TreeTableColumn<Model, Boolean> selectedColumn =
new TreeTableColumn<>("Selection");
selectedColumn.setEditable(true);
selectedColumn.setCellValueFactory(param -> param.getValue().getValue().selectedProperty());
selectedColumn.setCellFactory(CheckBoxTreeTableCell.<Model>forTreeTableColumn(selectedColumn));
final TreeTableView<Model> table = new TreeTableView<>(root);
table.setShowRoot(false);
table.setEditable(true);
table.getColumns().add(selectedColumn);
final Scene scene = new Scene(table, 500, 350);
stage.setScene(scene);
stage.show();
}
private class Model {
private final BooleanProperty selected;
private final StringProperty name;
public Model(final String name) {
this.selected = new SimpleBooleanProperty(false);
this.name = new SimpleStringProperty(name);
}
public final void setSelected(final boolean selected) {
this.selected.set(selected);
}
public final boolean isSelected() {
return selected.get();
}
public final BooleanProperty selectedProperty() {
return selected;
}
public final StringProperty nameProperty() {
return name;
}
#Override
public String toString() {
return "Model [selected=" + selected + ", name=" + name + "]";
}
}
}

A CheckBoxTreeItem provides a selected property. It is this property that respects the independent state of the CheckBoxTreeItem (i.e. if the parent CheckBoxTreeItem is selected, then this CheckBoxTreeItem is automatically selected, etc).
However, in your application, the CheckBoxTreeItem's selected property is not the property represented by the item, because you set the cell value factory to map to the selected property of the Model instance represented by the item. So checking the check box sets Model.selected to true, but of course there is no logic managing that property in terms of parent and/or child selected properties.
Typically when you have your own boolean property representing the state of the checkbox, you would not use a CheckBoxTreeItem. However, if you want the functionality of the non-independent properties, you would have to implement that yourself. Since that logic is actually quite complicated, if you want your own Model class, I would just bidirectionally bind the property of interest to the CheckBoxTreeItem's selectedProperty:
Model rootModel = new Model("Root");
final CheckBoxTreeItem<Model> root = new CheckBoxTreeItem<>(rootModel);
root.selectedProperty().bindBidirectional(rootModel.selectedProperty());
Model parentModel = new Model("Parent");
final CheckBoxTreeItem<Model> parent = new CheckBoxTreeItem<>( parentModel);
parent.selectedProperty().bindBidirectional(parentModel.selectedProperty());
Model childModel = new Model("Child");
final CheckBoxTreeItem<Model> child = new CheckBoxTreeItem<>(childModel);
child.selectedProperty().bindBidirectional(childModel.selectedProperty());

Related

JavaFX Combobox show attribute of element

I am currently working on a game with Java and JavaFX. I am using a JavaFX ComboBox.
The following example should explain my problem.
Let's say I have a class "Animal" with the attributes "name", "age" and "color".
First file:
public class Animal {
private String name;
private int age;
private String color;
public Animal(String name, int age, String color) {
this.name = name;
this.age = age;
this.color = color;
}
}
Now I want to create a ComboBox with each animal I create.
Second file:
ComboBox<Animal> comboBoxAnimal = new ComboBox();
ObservableList<Animal> comboBoxItems = FXCollections.observableArrayList();
Animal dog = new Animal("Liam", 2, "Brown");
Animal cat = new Animal("Emily", 5, "Gray");
Animal bird = new Animal("Kian", 3, "Green");
comboBoxItems.addAll(dog, cat, bird);
comboBoxAnimal.setItems(comboBoxItems);
Currently I get only "Animal#xxxxxxxx" which is understandable because I have a ComboBox of Animals but want only the names (Strings) to be presented.
Just simply creating a ComboBox<String> won't solve the problem as I need a Combobox<Animal>.
How can I get a Combobox<Animal> but as elements show only the names of each Animal?
Thanks for your feedback :)
Two options:
Use a cell factory.
Use a string converter.
The cell factory and string converter examples used in this answer produce the identical output:
Cell Factory Implementation
Use a cell factory, like in this answer:
How can I Populate a ListView in JavaFX using Custom Objects?
The linked answer is for a ListView, but the ComboBox is similar, as is a TableView or other virtualized controls that rely on cell factories for display.
To configure cells for the ComboBox drop-down list and button, make calls to both setCellFactory and setButtonCell.
This is the most flexible solution and allows for customization beyond just strings of text. Graphic nodes can be created to completely customize the visual representation of each combo box cell.
Example Code
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.stage.Stage;
public class AnimalComboApp extends Application {
public record Animal(String name, int age, String color) {}
public static class AnimalCell extends ListCell<Animal> {
#Override
public void updateItem(Animal animal, boolean empty) {
super.updateItem(animal, empty);
if (animal == null || empty) {
setText(null);
} else {
setText(animal.name());
}
}
}
#Override
public void start(Stage stage) throws Exception {
ComboBox<Animal> comboBox = new ComboBox<>();
comboBox.getItems().setAll(
new Animal("Liam", 2, "Brown"),
new Animal("Emily", 5, "Gray"),
new Animal("Kian", 3, "Green")
);
comboBox.setCellFactory(listView -> new AnimalCell());
comboBox.setButtonCell(new AnimalCell());
comboBox.getSelectionModel().select(0);
stage.setScene(new Scene(comboBox));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
StringConverter Implementation
An alternative to a cell factory definition is to provide the ComboBox with a StringConverter which can convert to and from a String and an object.
The StringConverter requires fromString and toString to be implemented, but the fromString implementation can just return null unless you also want the user to be able to perform text edits to edit the combo box value.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class AnimalComboApp extends Application {
public record Animal(String name, int age, String color) {}
#Override
public void start(Stage stage) throws Exception {
ComboBox<Animal> comboBox = new ComboBox<>();
comboBox.getItems().setAll(
new Animal("Liam", 2, "Brown"),
new Animal("Emily", 5, "Gray"),
new Animal("Kian", 3, "Green")
);
comboBox.setConverter(new StringConverter<>() {
#Override
public String toString(Animal animal) {
return animal.name();
}
#Override
public Animal fromString(String string) {
return null;
}
});
comboBox.getSelectionModel().select(0);
Scene scene = new Scene(comboBox);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Options NOT to use
toString() implementation
Do not override toString() to customize the cells, that is an anti-pattern.
Use toString() for other purposes such as listing and debugging all the elements of the Animal class, and instead, use the appropriate methods, such as cell factories or string converters, for customizing the UI view of the Animal.
Placing nodes in the combo box list
You might also be tempted to try to put nodes directly in the ComboBox list (for example create Labels with the name of an object in each Label and then create a ComboBox<Label>).
Don't do this, as advised by the ComboBox API documentation section: "A warning about inserting Nodes into the ComboBox items list", it will create bugs in your application.

JavaFX ChoiceBox - How can you update the text of the popup items?

I have a ChoiceBox where I can select the language for my program. When I select another language, the label gets translated as desired (because it is recomputed using ChoiceBoxSkin#getDisplayText and my StringConverter takes the language into account), but the elements in the popup list stay the same.
Now, I could do something like
public void updateStrings() {
var converter = getConverter();
setConverter(null);
setConverter(converter);
var selected = valueProperty().getValue();
valueProperty().setValue(null);
valueProperty().setValue(selected);
}
in my ChoiceBox-subclass. This will re-populate the popup list with the correctly translated texts. Setting the value again is necessary beacause ChoiceBoxSkin#updatePopupItems (which is triggered when changing the converter) also resets the toggleGroup. That means that the selected item would no longer be marked as selected in the popup list.
Despite being kind of ugly, this actually works for my current use case. However, it breaks if any listener of the valueProperty does something problematic on either setting it to null or selecting the desired item a second time.
Am I missing a cleaner or just all-around better way to achieve this?
Another approach might be to use a custom ChoiceBoxSkin. Extending that, I'd have access to ChoiceBoxSkin#getChoiceBoxPopup (although that is commented with "Test only purpose") and could actually bind the text properties of the RadioMenuItems to the corresponding translated StringProperty. But that breaks as soon as ChoiceBoxSkin#updatePopupItems is triggered from anywhere else...
A MRP should be:
import javafx.scene.control.ChoiceBox;
import javafx.util.StringConverter;
public class LabelChangeChoiceBox extends ChoiceBox<String> {
private boolean duringUpdate = false;
public LabelChangeChoiceBox() {
getItems().addAll("A", "B", "C");
setConverter(new StringConverter<>() {
#Override
public String toString(String item) {
return item + " selected:" + valueProperty().getValue();
}
#Override
public String fromString(String unused) {
throw new UnsupportedOperationException();
}
});
valueProperty().addListener((observable, oldValue, newValue) -> {
if(duringUpdate) {
return;
}
duringUpdate = true;
updateStrings();
duringUpdate = false;
});
}
public void updateStrings() {
var converter = getConverter();
setConverter(null);
setConverter(converter);
var selected = valueProperty().getValue();
valueProperty().setValue(null);
valueProperty().setValue(selected);
}
}
And an Application-class like
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import ui.LabelChangeChoiceBox;
public class Launcher extends Application {
#Override
public void start(Stage stage) {
Scene scene = new Scene(new LabelChangeChoiceBox());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
This works but needs the duringUpdate variable and can break if there is another change listener.
I’m not sure if this meets your needs, as your description of the problem is unclear in a few places.
Here’s a ChoiceBox which updates its converter using its own chosen language, and also retains its value when that change occurs:
import java.util.Locale;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.layout.BorderPane;
import javafx.util.StringConverter;
public class FXLocaleSelector
extends Application {
#Override
public void start(Stage stage) {
ChoiceBox<Locale> choiceBox = new ChoiceBox<>();
choiceBox.getItems().addAll(
Locale.ENGLISH,
Locale.FRENCH,
Locale.GERMAN,
Locale.ITALIAN,
Locale.CHINESE,
Locale.JAPANESE,
Locale.KOREAN
);
choiceBox.converterProperty().bind(
Bindings.createObjectBinding(
() -> createConverter(choiceBox.getValue()),
choiceBox.valueProperty()));
BorderPane pane = new BorderPane(choiceBox);
pane.setPadding(new Insets(40));
stage.setScene(new Scene(pane));
stage.setTitle("Locale Selector");
stage.show();
}
private StringConverter<Locale> createConverter(Locale locale) {
Locale conversionLocale =
(locale != null ? locale : Locale.getDefault());
return new StringConverter<Locale>() {
#Override
public String toString(Locale value) {
if (value != null) {
return value.getDisplayName(conversionLocale);
} else {
return "";
}
}
#Override
public Locale fromString(String s) {
return null;
}
};
}
public static void main(String[] args) {
launch(FXLocaleSelector.class, args);
}
}
Not entirely certain whether or not I understand your requirement correctly, my assumptions:
there's a ChoiceBox which contains the "language" for your ui, including the itself: lets say it contains the items Locale.ENGLISH and Locale.GERMAN, the visual representation of its items should be "English", "German" if its value is Locale.ENGLISH and "Englisch", "Deutsch" if its value is Locale.GERMAN
the visual representation is done by a StringConverter configurable with the value
If so, the solution is in separating out concerns - actually, it's not: the problem described (and hacked!) in the question is JDK-8088507: setting the converter doesn't update the selection of the menu items in the drop down. One hack is as bad or good as another, my personal preferenced would go for a custom skin which
adds a change listener to the converter property
reflectively calls updateSelection
Something like:
public static class MyChoiceBoxSkin<T> extends ChoiceBoxSkin<T> {
public MyChoiceBoxSkin(ChoiceBox<T> control) {
super(control);
registerChangeListener(control.converterProperty(), e -> {
// my local reflection helper, use your own
FXUtils.invokeMethod(ChoiceBoxSkin.class, this, "updateSelection");
});
}
}
Note: the hacks - this nor the OP's solution - do not solve the missing offset of the popup on first opening (initially or after selecting an item in the popup).
Not a solution to the question, just one way to have a value-dependent converter ;)
have a StringConverter with a fixed value (for simplicity) for conversion
have a converter controller having that a property with that value and a second property with a converter configured with the value: make sure the converter is replaced on change of the value
bind the controller's value to the box' value and the box' converter to the controller's converter
In (very raw) code:
public static class LanguageConverter<T> extends StringConverter<T> {
private T currentLanguage;
public LanguageConverter(T language) {
currentLanguage = language;
}
#Override
public String toString(T object) {
Object value = currentLanguage;
return "" + object + (value != null ? value : "");
}
#Override
public T fromString(String string) {
return null;
}
}
public static class LanguageController<T> {
private ObjectProperty<StringConverter<T>> currentConverter = new SimpleObjectProperty<>();
private ObjectProperty<T> currentValue = new SimpleObjectProperty<>() {
#Override
protected void invalidated() {
currentConverter.set(new LanguageConverter<>(get()));
}
};
}
Usage:
ChoiceBox<String> box = new ChoiceBox<>();
box.getItems().addAll("A", "B", "C");
box.getSelectionModel().selectFirst();
LanguageController<String> controller = new LanguageController<>();
controller.currentValue.bind(box.valueProperty());
box.converterProperty().bind(controller.currentConverter);

Refreshing a column of comboboxes in a table view, javafx

so i have a table view with 3 columns and one of them is a column of comboboxes, the way i create the column of combobox is as so
Source = new TableColumn<>("Configure Interface as..");
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
Source.setCellFactory(col -> {
TableCell<TableViewTest, StringProperty> c = new TableCell<>();
ComboBox<String> comboBox = new ComboBox<>(options);
c.itemProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
comboBox.valueProperty().unbindBidirectional(oldValue);
}
if (newValue != null) {
comboBox.valueProperty().bindBidirectional(newValue);
}
});
c.graphicProperty().bind(Bindings.when(c.emptyProperty()).then((Node) null).otherwise(comboBox));
return c;
});
the column gets its values from the getter method optionProperty() which resides within my TableViewTest class.
So the problem i'm having is I have another combobox (comboBoxA) that is above my tableview table in my gui, and when ever i change the value of comboBoxA i want to change the values of the comboboxes with the column.
I can do this by calling the following code within the method that is listening for the selection change of comboboxA
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionTwoProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
but the values don't change unless is start scrolling down to near the bottom of the table. is there a way to force the comboboxes to change to the new values within the getter method optionTwoProperty() without me having to scroll down?.
EDIT
Okay so the line
final StringProperty value = i.getValue().optionTwoProperty();
doesnt actaully get called until i start scrolling down.
So, with help from fabian, I think I understand that you want the combo box above the table to change the property in your model class that is represented in the cells in the table column.
One way to do this is to make the type of the combo box function that maps the model class to a property, and populate it with functions mapping to each of the properties you want.
Then you can represent the cell value factory for the table column with a binding that observes all the possible properties that could be represented, along with the selected value in the combo box, and returns the value computed by applying the function from the combo box to the model instance (and retrieving its wrapped value).
For the cell factory for the column, you can observe the selected value in the cell's combo box. When it changes, use the selected item in the combo box above the table to figure out which property to update.
Here's a SSCCE:
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableWithSetAllComboBox extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemCol = new TableColumn<>("Item");
itemCol.setCellValueFactory(cellData -> Bindings.createStringBinding(() -> cellData.getValue().getName()));
table.getColumns().add(itemCol);
TableColumn<Item, String> choiceCol = new TableColumn<>("Choice");
ComboBox<Function<Item, StringProperty>> option = new ComboBox<>();
option.getItems().add(Item::choiceProperty);
option.getItems().add(Item::choice2Property);
option.setCellFactory(lv -> createListCell());
option.setButtonCell(createListCell());
option.getSelectionModel().select(0);
ObservableList<String> choices = FXCollections.observableArrayList("First choice", "Second choice", "Third choice");
choiceCol.setCellFactory(col -> {
TableCell<Item, String> cell = new TableCell<>();
ComboBox<String> combo = new ComboBox<>(choices);
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty()).then((Node)null).otherwise(combo));
combo.valueProperty().addListener((obs, oldValue, newValue) -> {
if (! cell.isEmpty() && newValue != null) {
Item item = table.getItems().get(cell.getIndex()) ;
StringProperty property = option.getValue().apply(item);
property.set(newValue);
}
});
cell.itemProperty().addListener((obs, oldItem, newItem) -> combo.setValue(newItem));
return cell ;
});
choiceCol.setPrefWidth(150);
table.getColumns().add(choiceCol);
choiceCol.setCellValueFactory(cellData -> Bindings.createStringBinding(
() -> option.getValue().apply(cellData.getValue()).get(),
cellData.getValue().choiceProperty(),
cellData.getValue().choice2Property(),
option.valueProperty()));
choiceCol.setGraphic(option);
choiceCol.setPrefWidth(200);
for (int i = 1; i <= 30 ; i++) table.getItems().add(new Item("Item "+i ,choices.get(0)));
Button debug = new Button("Debug");
debug.setOnAction(e -> table.getItems().stream().
map(item -> String.format("%s (%s, %s)", item.getName(), item.getChoice(), item.getChoice2())).
forEach(System.out::println));
BorderPane root = new BorderPane(table);
BorderPane.setMargin(debug, new Insets(5));
root.setBottom(debug);
primaryStage.setScene(new Scene(root, 600, 600));
primaryStage.show();
}
private ListCell<Function<Item, StringProperty>> createListCell() {
return new ListCell<Function<Item, StringProperty>>() {
#Override
public void updateItem(Function<Item, StringProperty> item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? null : item.apply(new Item("", "")).getName());
}
};
}
public static class Item {
private final String name ;
private final StringProperty choice ;
private final StringProperty choice2 ;
public Item(String name, String choice) {
this.choice = new SimpleStringProperty(this, "Choice", choice);
this.choice2 = new SimpleStringProperty(this, "Choice 2", "Second choice");
this.name = name ;
}
public final StringProperty choiceProperty() {
return this.choice;
}
public final java.lang.String getChoice() {
return this.choiceProperty().get();
}
public final void setChoice(final java.lang.String choice) {
this.choiceProperty().set(choice);
}
public String getName() {
return name;
}
public final StringProperty choice2Property() {
return this.choice2;
}
public final java.lang.String getChoice2() {
return this.choice2Property().get();
}
public final void setChoice2(final java.lang.String choice2) {
this.choice2Property().set(choice2);
}
}
public static void main(String[] args) {
launch(args);
}
}
The issue is the TableView not listening to modifications of the cellValueFactory property of the elements of it's columns. Therefore the TableView doesn't know it should redraw it's cells. In JavaFX 8u60 the refresh() method was added for this purpose (for some reason I can't find it in the online javadoc though), which allows you to change the code of your method changing the cellValueFactory like this:
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionTwoProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
tableview.refresh();
In older versions you have to use the workaround of setting the column value to trigger a change in the list:
List<TableColumn<TableViewTest, ?>> columns = tableview.getColumns();
columns.set(columns.indexOf(Source), Source);
But this workaround could cease to work in future versions, since the list is not actually modified with this operation and triggering a list change event is not required by the contract of ObservableList (but replacing the TableColumn with a new instance (and copying the properties) should always work).
Hard to say given the code snippets. Maybe you're not on the javaFX thread when doing the update? In that case use Platform.runLater(...), or share some minimal amout of code to reproduce the problem.

Are bindings automatically removed in a TreeCell

I have TreeView that has a cell factory set on it. The TreeCells I'm returning are displayed below:
import javafx.beans.binding.StringBinding;
import javafx.collections.ObservableMap;
import javafx.scene.control.TreeCell;
public class TreeCellTest extends TreeCell<String> {
private ObservableMap<String, StringBinding> lookup;
public TreeCellTest(ObservableMap<String, StringBinding> lookup) {
this.lookup = lookup;
}
#Override
protected void updateItem(String id, boolean empty) {
super.updateItem(id, empty);
if (empty) {
setText(null);
} else {
StringBinding stringBinding = lookup.get(id);
textProperty().bind(stringBinding);
}
}
}
Notice that I'm not setting the text but I'm binding the textProperty to a StringBinding. This works fine in normal situations but I'm wondering if it is OK to use it inside a TreeCell.
The TreeCell gets recycled as and when needed so I would like to know whether when this happens the binding gets automatically removed or whether I need to remove it manually?
I don't want the case where each TreeCell has 100's of bindings attached to it.
While it's not documented, it appears that calling bind(...) will remove any existing bindings before creating the new binding.
For example:
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class RebindingTest {
public static void main(String[] args) {
StringProperty text = new SimpleStringProperty();
StringProperty value1 = new SimpleStringProperty();
StringProperty value2 = new SimpleStringProperty();
text.addListener((obs, oldValue, newValue) -> System.out.printf("text changed from %s to %s%n", oldValue, newValue));
text.bind(value1);
value1.set("Set value 1");
text.bind(value2);
value2.set("Set value 2");
value1.set("Reset value 1");
}
}
So I think all you need to do to make your code work correctly is add
textProperty().unbind();
to the if (empty) { ... } block.
Of course, calling that unconditionally in your updateItem(...) method would mean you're not relying on undocumented behavior, and any loss of efficiency is probably minimal.

JavaFX 2: Get TableCell Row Index

I have a Table with checkboxes. I want to change the selection of the checkbox in the first column when I click on the checkbox in the third or fourth column. I want to be able to change the other cells on the same row. I already have the columns so I want to know what row the cell is in. I am also very uncertain whether I have it right so far or not.
What I have done so far I figured mostly from
http://download.oracle.com/javafx/2.0/ui_controls/list-view.htm
http://download.oracle.com/javafx/2.0/ui_controls/table-view.htm
http://download.oracle.com/javafx/2.0/api/index.html?javafx/scene/control/Cell.html
Here is my SSCCE (Short Self Contained Compilable Example)
Please correct me if there is something wrong with the code below.
package javafxapplication5;
import javafx.application.Application;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
public class JavaFXApplication extends Application {
private static final ObservableList<ContactOptions> addContactOption = FXCollections.observableArrayList(
new ContactOptions("Yes", "John Doe", "No", "Yes"),
new ContactOptions("Yes", "Jane Doe", "No", null),
new ContactOptions("Yes", "John Smith", "Yes", "Yes"),
new ContactOptions("Yes", "Patty Smith", "Yes", "No"),
new ContactOptions("Yes", "Jo Johnson", "Yes", "Yes"),
new ContactOptions("No", "Mary Johnson", "No", "No"),
new ContactOptions("Yes", "Clint Doe", "No", null),
new ContactOptions("Yes", "Sally Sue", "No", "Yes"),
new ContactOptions("Yes", "Bob Ryan", null, "Yes"),
new ContactOptions("No", "Mary Sue", "No", "No"),
new ContactOptions("Yes", "Bob Smith", "No", "Yes"));
private static TableView<ContactOptions> contactOptions = new TableView<ContactOptions>();
public static void main(String[] args) {
Application.launch(JavaFXApplication.class, args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World");
Group root = new Group();
Scene scene = new Scene(root, 400, 200, Color.LIGHTGREEN);
Callback<TableColumn, TableCell> cellFactory = new Callback<TableColumn, TableCell>() {
#Override
public TableCell call(final TableColumn param) {
final CheckBox checkBox = new CheckBox();
final TableCell cell = new TableCell() {
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (item == null) {
checkBox.setDisable(true);
checkBox.setSelected(false);
} else {
checkBox.setDisable(false);
checkBox.setSelected(item.toString().equals("Yes") ? true : false);
commitEdit(checkBox.isSelected() ? "Yes" : "No");
}
}
};
cell.setNode(checkBox);
return cell;
}
};
TableColumn firstCol = new TableColumn("Contact?");
firstCol.setPrefWidth(60);
firstCol.setProperty("one");
firstCol.setCellFactory(cellFactory);
TableColumn secondCol = new TableColumn("Name");
secondCol.setPrefWidth(200);
secondCol.setSortAscending(true);
secondCol.setProperty("two");
TableColumn thirdCol = new TableColumn("Call");
thirdCol.setPrefWidth(60);
thirdCol.setProperty("three");
thirdCol.setCellFactory(cellFactory);
TableColumn fourthCol = new TableColumn("Email");
fourthCol.setPrefWidth(60);
fourthCol.setProperty("four");
fourthCol.setCellFactory(cellFactory);
contactOptions.setItems(addContactOption);
contactOptions.getColumns().addAll(firstCol, secondCol, thirdCol, fourthCol);
contactOptions.setPrefSize(400, 200);
root.getChildren().add(contactOptions);
primaryStage.setScene(scene);
primaryStage.setVisible(true);
}
public static class ContactOptions {
private final StringProperty one;
private final StringProperty two;
private final StringProperty three;
private final StringProperty four;
ContactOptions(String col1, String col2, String col3, String col4) {
this.one = new StringProperty(col1);
this.two = new StringProperty(col2);
this.three = new StringProperty(col3);
this.four = new StringProperty(col4);
}
public String getOne() {
return one.get();
}
public String getTwo() {
return two.get();
}
public String getThree() {
return three.get();
}
public String getFour() {
return four.get();
}
}
}
Almost There
Before calling commitEdit, it is necessary to call getTableView().edit(getTableRow().getIndex(), param). This puts the cell into "editing mode". Since there is no startEdit method, there is very little involved in entering edit mode, but it is still required.
After that, as described here: http://download.oracle.com/javafx/2.0/ui_controls/table-view.htm
It is necessary to call
firstCol.setOnEditCommit(new EventHandler<EditEvent<String>>() {
#Override
public void handle(EditEvent<String> event) {
String newValue = event.getNewValue();
ContactOptions data = (ContactOptions) event.getTableView().getItems().get(event.getTablePosition().getRow());
data.one.set(newValue)
if(newValue.equals("No")) {
data.three.set("No");
data.four.set("No");
}
}
}
Now all I need to know is how to update the table's display once the data is updated.
An advantage of using Observables is that the JavaFX UI elements can perform the bindings for you "behind the scenes." In other words, if you implement your data model class as a JavaFX Bean, your UI will update itself automatically whenever it changes. It does this because bindings for the observable data in your model are automatically assigned and change notification events automatically generated.
But you have to define your data model according to the JavaFX bean paradigm in order for this to happen, otherwise your UI won't update as changes occur.
Your data model is defined like this:
public static class ContactOptions {
private final StringProperty one;
private final StringProperty two;
private final StringProperty three;
private final StringProperty four;
ContactOptions(String col1, String col2, String col3, String col4) {
this.one = new StringProperty(col1);
this.two = new StringProperty(col2);
this.three = new StringProperty(col3);
this.four = new StringProperty(col4);
}
public String getOne() {
return one.get();
}
public String getTwo() {
return two.get();
}
public String getThree() {
return three.get();
}
public String getFour() {
return four.get();
}
}
For this reply, I will focus only on your 1st instance field, one. To transform this so that it is compliant with the JavaFX bean paradigm for a JavaFX Property, write your code this way, for example:
public static class ContactOptions {
private final StringProperty one = new SimpleStringProperty();
public final String getOne() { return this.one.get(); }
public final void setOne(String v) { this.one.set(v); }
public final StringProperty oneProperty() { return this.one; }
It is possible to write property definitions for a JavaFX bean that provide for a lazier initialization, but this will work. The difference between a Java bean and a JavaFX bean is that you must also provide an accessor for the property (the last line above).
If you make all your fields into properties similar to the above, you will find that your UI updates to reflect changes.

Categories