I have two types of Objects in a table, for simplicity, I will call them Dog and Cat, both of these objects extend from Animal. I am attempting to have a running count of the objects. For the moment I will focus on the Dog object.
I have a TableColumn that I've created a value factory for, and it looks like this:
column.setCellFactory(callback -> new TableCell<>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(!empty) {
if (getTableView().getItems().get(getIndex()) instanceof Dog) {
int count = setDogCount(getTableView(), getIndex(), 0);
setText(String.valueOf(count));
} else {
setText("");
}
} else {
setText("");
}
}
});
The recursive method I use is here:
private int setDogCount(TableView<Animal> table, int index, int count){
if(index == 0){
if(table.getItems().get(index) instanceof Dog) {
return count + 1;
} else {
return count;
}
}
if(table.getItems().get(index) instanceof Dog){
return setDogCount(table, --index, ++count);
}else {
return setDogCount(table, --index, count);
}
}
This actually works about 95% of the time. The only times it breaks is when some sorts occur. It breaks because cell factories are only called when they are sorted, so if a sort does not occur, then it won't update the count. This leads to some multiple counts of Dog depending on the circumstances.
QUESTION:
So, is there a way to have it update only one column on sort? I would like to try to avoid refreshing the entire table, and I was hoping there is a better way.
Thanks!
Edit:
By "if a sort does not occur" I mean that if that object was not moved from its current index, it does not call the cell factory.
Edit 2:
Here is a picture to see what I am facing.
Before Sorting -> After Sorting:
As you can see, index 0 switched with index 3, thus changing the count column appropriately, but now I have no number 1, and two number 3s. This is because it only updates the rows that were switched.
Edit 3:
Here is the small application to see the changes. When you run it, please click on the Commands Known column once to see what I am up against.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
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 Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
BorderPane root = new BorderPane();
TableView<Animal> table = new TableView<>();
TableColumn<Animal, String> count = new TableColumn<>("Count");
TableColumn<Animal, String> name = new TableColumn<>("Name");
TableColumn<Animal, String> sound = new TableColumn<>("Sound");
TableColumn<Animal, String> commandsKnown = new TableColumn<>("Commands Known");
table.getColumns().addAll(count, name, sound, commandsKnown);
root.setCenter(table);
count.setCellFactory(callback -> new TableCell<>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
if (getTableView().getItems().get(getIndex()) instanceof Dog) {
int count = setDogCount(getTableView(), getIndex(), 0);
setText(String.valueOf(count));
} else {
setText("");
}
} else {
setText("");
}
}
});
name.setCellValueFactory(data -> data.getValue().nameProperty());
sound.setCellValueFactory(data -> data.getValue().soundProperty());
commandsKnown.setCellValueFactory(data -> {
if(data.getValue() instanceof Dog){
return ((Dog) data.getValue()).commandsKnownProperty();
}
return new SimpleStringProperty("");
});
ObservableList<Animal> animals = FXCollections.observableArrayList();
animals.add(new Dog("Tweeter", "Woof", "Sit, rollover, shake, drop"));
animals.add(new Dog("Sub Woofer", "Woof", "Sit, rollover, shake"));
animals.add(new Cat("Kitter Cat", "Meow"));
animals.add(new Dog("Bass", "Woof", "Sit, rollover, shake, fetch"));
table.setItems(animals);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
private int setDogCount(TableView<Animal> table, int index, int count){
if(index == 0){
if(table.getItems().get(index) instanceof Dog) {
return count + 1;
} else {
return count;
}
}
if(table.getItems().get(index) instanceof Dog){
return setDogCount(table, --index, ++count);
}else {
return setDogCount(table, --index, count);
}
}
public static void main(String[] args) {
launch(args);
}
public class Animal{
StringProperty name = new SimpleStringProperty();
StringProperty sound = new SimpleStringProperty();
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public String getSound() {
return sound.get();
}
public StringProperty soundProperty() {
return sound;
}
public void setSound(String sound) {
this.sound.set(sound);
}
}
public class Dog extends Animal{
StringProperty commandsKnown = new SimpleStringProperty();
public Dog(String name, String sound, String commandsKnown){
setName(name);
setSound(sound);
setCommandsKnown(commandsKnown);
}
public String getCommandsKnown() {
return commandsKnown.get();
}
public StringProperty commandsKnownProperty() {
return commandsKnown;
}
public void setCommandsKnown(String commandsKnown) {
this.commandsKnown.set(commandsKnown);
}
}
public class Cat extends Animal{
public Cat(String name, String sound){
setName(name);
setSound(sound);
}
}
}
To expand a bit on my comment:
a cell definitely is the wrong place to change the data (even if it's
meta) ... do it in the model ;)
As model I meant not only your data object/list but also all state that is related to the data, even if it's also related to the view, as f.i. a running counter. You must model that relation somehow outside of the view.
Below is an example that models the relation by an additional list for the counters, one property per owner. It's the task of the app to keep it in sync with the owners as shown in the table whenever anything that effects the counters changes (f.i. when the list is sorted or the pet changed or anything).
The code:
public class TableWithExternalCounterSO extends Application {
/**
* Updates the counter data from the given source list, assuming that
* both have the same size (if that's not true, adjust counter size
* as needed)
*/
private void updateDogCounterFrom(ObservableList<ObjectProperty<Integer>> dogCounter,
ObservableList<? extends PetOwner> owners) {
int count = 0;
for (int i = 0; i < owners.size(); i++) {
PetOwner owner = owners.get(i);
if (owner.petProperty().get() == Pet.DOG) {
dogCounter.get(i).set(++count);
} else {
dogCounter.get(i).set(-1);
}
}
}
private Parent createContent() {
// the base data
ObservableList<PetOwner> owners = PetOwner.owners();
// a list for the counters, that must be kept in sync with changes in the table
ObservableList<ObjectProperty<Integer>> dogCounter = FXCollections.observableArrayList();
owners.forEach(owner -> dogCounter.add(new SimpleObjectProperty<Integer>(-1)));
// initial sync
updateDogCounterFrom(dogCounter, owners);
SortedList<PetOwner> sorted = new SortedList<>(owners);
sorted.addListener((ListChangeListener<? super PetOwner>) c -> {
// sync after change
updateDogCounterFrom(dogCounter, c.getList());
});
TableView<PetOwner> table = new TableView<>(sorted);
sorted.comparatorProperty().bind(table.comparatorProperty());
TableColumn<PetOwner, String> name = new TableColumn<>("Name");
name.setCellValueFactory(new PropertyValueFactory<>("name"));
TableColumn<PetOwner, Pet> pet = new TableColumn<>("Pet");
pet.setCellValueFactory(new PropertyValueFactory<>("pet"));
TableColumn<PetOwner, Integer> dogIndex = new TableColumn<>("Running Dog#");
dogIndex.setSortable(false);
dogIndex.setCellValueFactory(cd -> {
// astonishingly, this is called for every cell after sorting,
// that is all cells are newly created
int index = cd.getTableView().getItems().indexOf(cd.getValue());
return dogCounter.get(index);
});
dogIndex.setCellFactory(cb -> {
return new TableCell<PetOwner, Integer>() {
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
if (empty || item != null && item.intValue() < 0) {
setText("");
} else {
setText(String.valueOf(item));
}
}
};
});
table.getColumns().addAll(name, pet, dogIndex);
BorderPane pane = new BorderPane(table);
return pane;
}
private enum Pet {
CAT, DOG
}
public static class PetOwner {
ObjectProperty<Pet> pet;
StringProperty name;
PetOwner(String name, Pet pet) {
this.pet = new SimpleObjectProperty<>(this, "pet", pet);
this.name = new SimpleStringProperty(this, "name", name);
}
public ObjectProperty<Pet> petProperty() {
return pet;
}
public StringProperty nameProperty() {
return name;
}
public static ObservableList<PetOwner> owners() {
ObservableList<PetOwner> owners = FXCollections.observableArrayList();
for (int i = 0; i < 20; i++) {
owners.add(new PetOwner("O " + i, i % 3 == 0 ? Pet.CAT : Pet.DOG) );
}
return owners;
}
#Override
public String toString() {
return name.get( ) + " " + pet.get();
}
}
I suggest you use a static variable to keep up with the Dog's ID.
Example code below:
Full Code:
Dog Class
/**
*
* #author blj0011
*/
public class Dog extends Animal
{
static int dogCounter = 0;
private String commnads;
private final int dogId;
public Dog(String name, String sound, String commands)
{
super(name, sound);
this.commnads = commands;
dogId = ++dogCounter;
}
public String getCommnads()
{
return commnads;
}
public void setCommnads(String commnads)
{
this.commnads = commnads;
}
public int getDogId()
{
return dogId;
}
}
Cat Class
/**
*
* #author blj0011
*/
public class Cat extends Animal
{
public Cat(String name, String sound)
{
super(name, sound);
}
}
Main
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application
{
#Override
public void start(Stage primaryStage) throws Exception
{
BorderPane root = new BorderPane();
TableView<Animal> table = new TableView<>();
TableColumn<Animal, String> count = new TableColumn<>("Count");
TableColumn<Animal, String> name = new TableColumn<>("Name");
TableColumn<Animal, String> sound = new TableColumn<>("Sound");
TableColumn<Animal, String> commandsKnown = new TableColumn<>("Commands Known");
table.getColumns().addAll(count, name, sound, commandsKnown);
root.setCenter(table);
count.setCellValueFactory(new PropertyValueFactory("dogId"));
name.setCellValueFactory(new PropertyValueFactory("name"));
sound.setCellValueFactory(new PropertyValueFactory("sound"));
commandsKnown.setCellValueFactory(new PropertyValueFactory("commands"));
ObservableList<Animal> animals = FXCollections.observableArrayList();
animals.add(new Dog("Tweeter", "Woof", "Sit, rollover, shake, drop"));
animals.add(new Dog("Sub Woofer", "Woof", "Sit, rollover, shake"));
animals.add(new Cat("Kitter Cat", "Meow"));
animals.add(new Dog("Bass", "Woof", "Sit, rollover, shake, fetch"));
table.setItems(animals);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
private int setDogCount(TableView<Animal> table, int index, int count)
{
if (index == 0) {
if (table.getItems().get(index) instanceof Dog) {
return count + 1;
}
else {
return count;
}
}
if (table.getItems().get(index) instanceof Dog) {
return setDogCount(table, --index, ++count);
}
else {
return setDogCount(table, --index, count);
}
}
public static void main(String[] args)
{
launch(args);
}
}
SortedList code:
import java.util.Comparator;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application
{
#Override
public void start(Stage primaryStage) throws Exception
{
BorderPane root = new BorderPane();
TableView<Animal> table = new TableView<>();
TableColumn<Animal, String> count = new TableColumn<>("Count");
TableColumn<Animal, String> name = new TableColumn<>("Name");
TableColumn<Animal, String> sound = new TableColumn<>("Sound");
TableColumn<Animal, String> commandsKnown = new TableColumn<>("Commands Known");
table.getColumns().addAll(count, name, sound, commandsKnown);
root.setCenter(table);
count.setCellValueFactory(new PropertyValueFactory("dogId"));
name.setCellValueFactory(new PropertyValueFactory("name"));
sound.setCellValueFactory(new PropertyValueFactory("sound"));
commandsKnown.setCellValueFactory(new PropertyValueFactory("commands"));
ObservableList<Animal> animals = FXCollections.observableArrayList();
animals.add(new Dog("Tweeter", "Woof", "Sit, rollover, shake, drop"));
animals.add(new Dog("Sub Woofer", "Woof", "Sit, rollover, shake"));
animals.add(new Cat("Kitter Cat", "Meow"));
animals.add(new Cat("Kitter Cat 2", "Meow"));
animals.add(new Dog("Bass", "Woof", "Sit, rollover, shake, fetch"));
SortedList<Animal> sortedList = new SortedList(animals);
Comparator<Animal> comparator = (c, d) -> {
if (c instanceof Cat && d instanceof Dog) {
return -1;
}
else if (c instanceof Dog && d instanceof Cat) {
return 1;
}
return 0;
};
comparator.thenComparing((c, d) -> {
if (c instanceof Cat && d instanceof Dog) {
return -1;
}
else if (c instanceof Dog && d instanceof Cat) {
return 1;
}
else {
if (c instanceof Cat && d instanceof Cat) {
return c.getName().compareTo(d.getName());
}
else {
Dog tempDog1 = (Dog) c;
Dog tempDog2 = (Dog) d;
if (tempDog1.getDogId() > tempDog2.getDogId()) {
return 1;
}
}
return 0;
}
});
sortedList.setComparator(comparator);
table.setItems(sortedList);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
public static void main(String[] args)
{
launch(args);
}
}
Related
I have a TableView where every row has a ContextMenu like in the image below.
When I click on the first MenuItem called ("Contrassegna riga come analizzata"), I want all selected rows of the TableView (in the example above the ones starting with 22002649 and 22016572) to change color.
If they are already coloured, I want them to remove it.
I tried with the following code but it obviously works only with the last selected row and not with others
tableView.setRowFactory(
new Callback<TableView, TableRow>() {
#Override
public TableRow call(TableView tableView0) {
final TableRow row = new TableRow<>();
final ContextMenu rowMenu = new ContextMenu();
final PseudoClass checkedPC = PseudoClass.getPseudoClass("checked");
MenuItem doneRiga = new MenuItem("Contrassegna riga come analizzata");
doneRiga.setOnAction(j -> {
if (!row.getPseudoClassStates().contains(checkedPC))
row.pseudoClassStateChanged(checkedPC, true);
else
row.pseudoClassStateChanged(checkedPC, false);
});
MenuItem doneArticolo = new MenuItem("Contrassegna articolo come analizzato");
rowMenu.getItems().addAll(doneRiga, doneArticolo);
return row;
}
});
Consequently I obtain the following result
Any suggestions? Thank you
This is really a duplicate of Programmatically change the TableView row appearance, but since that question is quite old, here is a solution using more modern Java idioms.
Typically your model class should contain observable properties for all data that is required to view it. In this case, your table items can be either "analyzed" or "not analyzed", so they would usually have a boolean property to represent that. For example:
public class Item {
private final StringProperty name = new SimpleStringProperty();
private final BooleanProperty analyzed = new SimpleBooleanProperty();
public Item(String name) {
setName(name);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public boolean getAnalyzed() {
return analyzed.get();
}
public BooleanProperty analyzedProperty() {
return analyzed;
}
public void setAnalyzed(boolean analyzed) {
this.analyzed.set(analyzed);
}
}
Your table row needs to do two things:
Observe the analyzedProperty() of the current item it is displaying, so it updates the state if that property changes. Note this mean if the item changes, it needs to remove a listener from the old item (i.e. stop observing the property in the old item) and add a listener to the new item (start observing the property in the new item).
If the item changes, update the state of the row to reflect the analyzed state of the new item.
A table row implementation that does this looks like:
TableRow<Item> row = new TableRow<>(){
private final ChangeListener<Boolean> analyzedListener = (obs, wasAnalyzed, isNowAnalyzed) ->
updateState(isNowAnalyzed);
{
// Make sure we are observing the analyzedProperty on the current item
itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.analyzedProperty().removeListener(analyzedListener);
}
if (newItem != null) {
newItem.analyzedProperty().addListener(analyzedListener);
}
});
}
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
updateState(false);
} else {
updateState(item.getAnalyzed());
}
}
private void updateState(boolean analyzed) {
pseudoClassStateChanged(analyzedPC, analyzed);
}
};
Note that in JavaFX 19 you can use the flatMap() API to simplify this code considerably:
TableRow<Item> row = new TableRow<>();
row.itemProperty()
.flatMap(Item::analyzedProperty)
.orElse(false)
.addListener((obs, wasAnalyzed, isNowAnalyzed) -> {
row.pseudoClassStateChanged(analyzedPC, isNowAnalyzed);
});
Now to change the state of the selected items, you just need to iterate through them and toggle the analyzed state:
ContextMenu menu = new ContextMenu();
MenuItem analyzedMI = new MenuItem("Analyzed");
analyzedMI.setOnAction(e -> {
// Toggle analyzed state of selected items
List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
for (Item item : selectedItems) {
item.setAnalyzed(! item.getAnalyzed());
}
});
menu.getItems().add(analyzedMI);
row.setContextMenu(menu);
Putting it all together in a complete example:
package org.jamesd.examples.highlightrows;
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.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
public class HelloApplication extends Application {
private static final PseudoClass analyzedPC = PseudoClass.getPseudoClass("analyzed");
#Override
public void start(Stage stage) throws IOException {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> column = new TableColumn<>("Item");
column.setCellValueFactory(data -> data.getValue().nameProperty());
table.getColumns().add(column);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.setRowFactory(tc -> {
TableRow<Item> row = new TableRow<>();
row.itemProperty()
.flatMap(Item::analyzedProperty)
.orElse(false)
.addListener((obs, wasAnalyzed, isNowAnalyzed) -> {
row.pseudoClassStateChanged(analyzedPC, isNowAnalyzed);
});
// Prior to JavaFX 19 you need something like the following (which is probably less robust):
// TableRow<Item> row = new TableRow<>(){
// private final ChangeListener<Boolean> analyzedListener = (obs, wasAnalyzed, isNowAnalyzed) ->
// updateState(isNowAnalyzed);
//
// {
// // Make sure we are observing the analyzedProperty on the current item
// itemProperty().addListener((obs, oldItem, newItem) -> {
// if (oldItem != null) {
// oldItem.analyzedProperty().removeListener(analyzedListener);
// }
// if (newItem != null) {
// newItem.analyzedProperty().addListener(analyzedListener);
// }
// });
// }
// #Override
// protected void updateItem(Item item, boolean empty) {
// super.updateItem(item, empty);
// if (empty || item == null) {
// updateState(false);
// } else {
// updateState(item.getAnalyzed());
// }
// }
//
// private void updateState(boolean analyzed) {
// pseudoClassStateChanged(analyzedPC, analyzed);
// }
// };
ContextMenu menu = new ContextMenu();
MenuItem analyzedMI = new MenuItem("Analyzed");
analyzedMI.setOnAction(e -> {
// Toggle analyzed state of selected items
List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
for (Item item : selectedItems) {
item.setAnalyzed(! item.getAnalyzed());
}
});
menu.getItems().add(analyzedMI);
row.setContextMenu(menu);
return row;
});
for (int i = 1 ; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i));
}
BorderPane root = new BorderPane(table);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final BooleanProperty analyzed = new SimpleBooleanProperty();
public Item(String name) {
setName(name);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public boolean getAnalyzed() {
return analyzed.get();
}
public BooleanProperty analyzedProperty() {
return analyzed;
}
public void setAnalyzed(boolean analyzed) {
this.analyzed.set(analyzed);
}
}
public static void main(String[] args) {
launch();
}
}
with the stylesheet style.css (in the same package as the application class):
.table-row-cell:analyzed {
-fx-control-inner-background: derive(green, 20%);
-fx-control-inner-background-alt: green;
-fx-selection-bar: #00b140;
}
If for some reason you cannot change the model class (Item in the code above), you need to track which items are "analyzed" separately in a way that can be observed. An ObservableList could be used for this:
final ObservableList<Item> analyzedItems = FXCollections.observableArrayList();
Now the table row can observe that list, and update the CSS pseudoclass if the list changes:
TableRow<Item> row = new TableRow<>(){
{
// If the list of analyzed items changes, make sure the state is correct:
analyzedItems.addListener((ListChangeListener.Change<? extends Item> change) -> {
updateState(analyzedItems.contains(getItem()));
});
}
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
updateState(false);
} else {
updateState(analyzedItems.contains(item));
}
}
private void updateState(boolean analyzed) {
pseudoClassStateChanged(analyzedPC, analyzed);
}
};
and you can toggle the state by adding or removing items from the list of analyzed items accordingly:
ContextMenu menu = new ContextMenu();
MenuItem analyzedMI = new MenuItem("Analyze");
analyzedMI.setOnAction(e -> {
// Toggle analyzed state of selected items
List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
for (Item item : selectedItems) {
if (analyzedItems.contains(item)) {
analyzedItems.remove(item);
} else {
analyzedItems.add(item);
}
}
});
menu.getItems().add(analyzedMI);
row.setContextMenu(menu);
The complete example in this case looks like;
package org.jamesd.examples.highlightrows;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
public class HelloApplication extends Application {
private static final PseudoClass analyzedPC = PseudoClass.getPseudoClass("analyzed");
#Override
public void start(Stage stage) throws IOException {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> column = new TableColumn<>("Item");
column.setCellValueFactory(data -> data.getValue().nameProperty());
table.getColumns().add(column);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
final ObservableList<Item> analyzedItems = FXCollections.observableArrayList();
table.setRowFactory(tc -> {
TableRow<Item> row = new TableRow<>(){
{
// If the list of analyzed items changes, make sure the state is correct:
analyzedItems.addListener((ListChangeListener.Change<? extends Item> change) -> {
updateState(analyzedItems.contains(getItem()));
});
}
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
updateState(false);
} else {
updateState(analyzedItems.contains(item));
}
}
private void updateState(boolean analyzed) {
pseudoClassStateChanged(analyzedPC, analyzed);
}
};
ContextMenu menu = new ContextMenu();
MenuItem analyzedMI = new MenuItem("Analyze");
analyzedMI.setOnAction(e -> {
// Toggle analyzed state of selected items
List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
for (Item item : selectedItems) {
if (analyzedItems.contains(item)) {
analyzedItems.remove(item);
} else {
analyzedItems.add(item);
}
}
});
menu.getItems().add(analyzedMI);
row.setContextMenu(menu);
return row;
});
for (int i = 1 ; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i));
}
BorderPane root = new BorderPane(table);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
public Item(String name) {
setName(name);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
}
public static void main(String[] args) {
launch();
}
}
I'm currently trying to implement the following:
A TableView with an ObservableList as dataset, with two columns, each of which contains Strings (names of the players). This part is easy enough.
Once a Player(name) is clicked, a custom FlowPane should be injected below the selected player. If another player is clicked, the flowpane should disappear and be injected below the currently clicked player.
The below code implements the TableView (minus the mouse listener part). Please help me let the FlowPane span the entire row. I'm guessing I need a RowFactory but have no clue how to make it work for my purposes :)
Also, apparently both my columns now show the same data. Confusing :) Is there a way to tell one column to use half the data set and the other column the other half? I obviously don't want my data shown twice.
public class main extends Application
{
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage stage) throws Exception
{
try
{
FlowPane f = new FlowPane();
Scene scene = new Scene(f, 300, 200);
Player p1 = new Player("player 1 ");
Player p2 = new Player("player 2 ");
Player p3 = new Player("player 3 ");
ArrayList<Object> players = new ArrayList<>();
players.add(p1);
players.add(p2);
players.add(p3);
ObservableList<Object> observableList = FXCollections.observableArrayList(players);
TableView<Object> table = createTableView(observableList, 300, 200);
f.getChildren().add(table);
injectFlowPane(table);
stage.setScene(scene);
stage.show();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public TableView<Object> createTableView(ObservableList<Object> items, double width, double height)
{
TableView<Object> table = new TableView<>();
table.setItems(items);
table.getColumns().add(createTableColumn(width / 2));
table.getColumns().add(createTableColumn(width / 2));
table.setMinSize(width, height);
table.setPrefSize(width, height);
table.setMaxSize(width, height);
return table;
}
private TableColumn<Object, Object> createTableColumn(double width)
{
TableColumn<Object, Object> tableColumn = new TableColumn<>();
tableColumn.setCellFactory(
new Callback<TableColumn<Object, Object>, TableCell<Object, Object>>() {
#Override
public TableCell<Object, Object> call(TableColumn<Object, Object> arg0)
{
return new PlayerCell();
}
});
tableColumn.setCellValueFactory(cellDataFeatures -> {
Object item = cellDataFeatures.getValue();
return new SimpleObjectProperty<>(item);
});
tableColumn.setMinWidth(width);
return tableColumn;
}
private void injectFlowPane(TableView<Object> table)
{
FlowPane f = new FlowPane();
f.setMinSize(50, 50);
f.setBackground(new Background(new BackgroundFill(Color.DARKGREEN, CornerRadii.EMPTY, Insets.EMPTY)));
table.getItems().add(1, f);
}
}
public class PlayerCell extends TableCell<Object, Object>
{
#Override
protected void updateItem(Object item, boolean empty)
{
super.updateItem(item, false);
// if (empty)
if (item != null)
{
if (item instanceof Player)
{
setText(((Player) item).getName());
setGraphic(null);
}
else if (item instanceof FlowPane)
{
setGraphic((FlowPane) item);
}
else
{
setText("N/A");
setGraphic(null);
}
}
else
{
setText(null);
setGraphic(null);
}
}
}
public class Player
{
private String name;
public Player(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
EDIT:
I have now implemented James_D's ExpandingTableRow, which works neatly as far as showing the FlowPane below the selected TableRow is concerned. I have also managed to change my datastructures so that each column now shows different players instead of the same ones in each column.
However, the FlowPane that is created should actually depend on the actual player(cell) that is clicked within the row. In James' example: a different FlowPane would be created if the FirstName or LastName was selected (even for the same row). The FlowPane should be shown the same way - below the selected row - but it's a different, new FlowPane depending on if FirstName was clicked, or if LastName was clicked. How can I manage to do this?
I've looked at using:
table.getSelectionModel().setCellSelectionEnabled(true);
But this actually seems to disable James_d's solution.
This solution works only in Java 9 and later.
The display of a row is managed by a TableRow, and the actual layout of that row is performed by its skin (a TableRowSkin). So to manage this, you need a subclass of TableRow that installs a custom skin.
The row implementation is pretty straightforward: in this example I added a property for the "additional content" to be displayed when the row is selected. It also overrides the createDefaultSkin() method to specify a custom skin implementation.
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.TableRow;
public class ExpandingTableRow<T> extends TableRow<T> {
private final ObjectProperty<Node> selectedRowContent = new SimpleObjectProperty<>();
public final ObjectProperty<Node> selectedRowContentProperty() {
return this.selectedRowContent;
}
public final Node getSelectedRowContent() {
return this.selectedRowContentProperty().get();
}
public final void setSelectedRowContent(final Node selectedRowContent) {
this.selectedRowContentProperty().set(selectedRowContent);
}
public ExpandingTableRow(Node selectedRowContent) {
super();
setSelectedRowContent(selectedRowContent);
}
public ExpandingTableRow() {
this(null);
}
#Override
protected Skin<?> createDefaultSkin() {
return new ExpandingTableRowSkin<T>(this);
}
}
The skin implementation has to do the layout work. It needs to override the methods that compute the height, accounting for the height of the extra content if needed, and it needs to override the layoutChildren() method, to position the additional content, if needed. Finally, it must manage the additional content, adding or removing the additional content if the selected state of the row changes (or if the additional content itself is changed).
import javafx.scene.control.skin.TableRowSkin;
public class ExpandingTableRowSkin<T> extends TableRowSkin<T> {
private ExpandingTableRow<T> row;
public ExpandingTableRowSkin(ExpandingTableRow<T> row) {
super(row);
this.row = row;
row.selectedRowContentProperty().addListener((obs, oldContent, newContent) -> {
if (oldContent != null) {
getChildren().remove(oldContent);
}
if (newContent != null && row.isSelected()) {
getChildren().add(newContent);
}
if (row.getTableView() != null) {
row.getTableView().requestLayout();
}
});
row.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected && row.getSelectedRowContent() != null
&& !getChildren().contains(row.getSelectedRowContent())) {
getChildren().add(row.getSelectedRowContent());
} else {
getChildren().remove(row.getSelectedRowContent());
}
});
}
#Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().maxHeight(width);
}
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().minHeight(width);
}
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().prefHeight(width);
}
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected void layoutChildren(double x, double y, double w, double h) {
if (row.isSelected()) {
double rowHeight = super.computePrefHeight(w, snappedTopInset(), snappedRightInset(), snappedBottomInset(),
snappedLeftInset());
super.layoutChildren(x, y, w, rowHeight);
row.getSelectedRowContent().resizeRelocate(x, y + rowHeight, w, h - rowHeight);
} else {
super.layoutChildren(x, y, w, h);
}
}
}
Finally, a test (using the usual example from Oracle, or a version of it):
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class ExpandingTableRowTest extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.getColumns().add(column("First Name", Person::firstNameProperty));
table.getColumns().add(column("Last Name", Person::lastNameProperty));
table.setRowFactory(tv -> {
Label label = new Label();
FlowPane flowPane = new FlowPane(label);
TableRow<Person> row = new ExpandingTableRow<>(flowPane) {
#Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty) {
label.setText(null);
} else {
label.setText(String.format("Some additional information about %s %s here",
person.getFirstName(), person.getLastName()));
}
}
};
return row;
});
table.getItems().addAll(
new Person("Jacob", "Smith"),
new Person("Isabella", "Johnson"),
new Person("Ethan", "Williams"),
new Person("Emma", "Jones"),
new Person("Michael", "Brown")
);
Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
private static <S, T> TableColumn<S, T> column(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S, T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
public Person(String firstName, String lastName) {
setFirstName(firstName);
setLastName(lastName);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final String lastName) {
this.lastNameProperty().set(lastName);
}
}
public static void main(String[] args) {
launch(args);
}
}
As you can see, a little refinement of the style and sizing may be needed to get this production-ready, but this shows the approach that will work.
I would like to add multiple combo boxes to JavaFX that after the user has selected an item the cost of that item will be displayed under the combo box. Also that the total cost of all the selected items will be displayed at the bottom. I know how to make one combo box that will display the cost of one item selected but can't figure out how to make multiple ones and to display the cost of everything selected
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.collections.FXCollections;
public class Animals extends Application {
Stage window;
Scene scene;
Button button;
ComboBox<Animal> comboBox = new ComboBox<Animal>();
Text textNamePrice = new Text();
static public TextField[] tfLetters = new TextField[37];
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
window = primaryStage;
window.setTitle("ComboBox ");
button = new Button("Submit");
comboBox = new ComboBox<Animal>();
comboBox.setConverter(new StringConverter<Animal>() {
#Override
public String toString(Animal object) {
return object.getName();
}
#Override
public Animal fromString(String string) {
return null;
}
});
comboBox.setItems(FXCollections.observableArrayList(new Animal("Dog", 30.12), new Animal("Cat", 23.23),
new Animal("Bird", 15.0)));
comboBox.valueProperty().addListener((obs, oldVal, newVal) -> {
String selectionText = "Price of the " + newVal.getName() + " is : " + newVal.getPrice();
System.out.println(selectionText);
textNamePrice.setText(selectionText);
});
VBox layout = new VBox(10);
layout.setPadding(new Insets(60, 60, 60, 60));
layout.getChildren().addAll(comboBox, textNamePrice, button);
scene = new Scene(layout, 500, 350);
window.setScene(scene);
window.show();
}
public class Animal {
private String name;
private Double price;
public Double getPrice() {
return price;
}
public String getName() {
return name;
}
public Animal(String name, Double price) {
this.name = name;
this.price = price;
}
}
}
It's probabls easiest to use a custom Node type AnimalChooser for displaying the ComboBox + price. This way the functionality for one selection+price display can be handled in one place. Also you can provide a price property based on the selection to sum them up from you application class.
The following example places all those AnimalChoosers in an VBox and adds a listener to the child list to add and remove listeners to/from the child list, should it be modified, which would allow you to dynamically add/remove those AnimalChooser to/from the VBox and still get a properly updated sum.
public class Animal {
private final String name;
// primitive type should be prefered here
private final double price;
public double getPrice() {
return price;
}
public String getName() {
return name;
}
public Animal(String name, double price) {
this.name = name;
this.price = price;
}
}
public class AnimalChooser extends VBox {
private final ComboBox<Animal> animalCombo;
private final ReadOnlyDoubleWrapper price;
private final Text text;
public AnimalChooser(ObservableList<Animal> items) {
setSpacing(5);
animalCombo = new ComboBox<>(items);
// converter for using a custom string representation of Animal in the
// combobox
animalCombo.setConverter(new StringConverter<Animal>() {
#Override
public String toString(Animal object) {
return object == null ? "" : object.getName();
}
#Override
public Animal fromString(String string) {
if (string == null || string.isEmpty()) {
return null;
} else {
// find suitable animal from list
Animal animal = null;
for (Animal item : items) {
if (string.equals(item.getName())) {
animal = item;
break;
}
}
return animal;
}
}
});
text = new Text();
price = new ReadOnlyDoubleWrapper();
getChildren().addAll(animalCombo, text);
// bind price value to price property
price.bind(Bindings.createDoubleBinding(new Callable<Double>() {
#Override
public Double call() throws Exception {
Animal animal = animalCombo.getValue();
return animal == null ? 0d : animal.getPrice();
}
}, animalCombo.valueProperty()));
// bind text to content of Text node
text.textProperty().bind(Bindings.when(animalCombo.valueProperty().isNull()).then("").otherwise(price.asString("%.2f $")));
}
public final double getPrice() {
return this.price.get();
}
public final ReadOnlyDoubleProperty priceProperty() {
return this.price.getReadOnlyProperty();
}
}
#Override
public void start(Stage primaryStage) {
VBox animalChoosers = new VBox(20);
ObservableList<Animal> animals = FXCollections.observableArrayList(
new Animal("cat", 1000.99),
new Animal("dog", 20.50),
new Animal("goldfish", 15.22)
);
final DoubleProperty total = new SimpleDoubleProperty();
InvalidationListener listener = new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
double sum = 0d;
for (Node n : animalChoosers.getChildren()) {
AnimalChooser chooser = (AnimalChooser) n;
sum += chooser.getPrice();
}
total.set(sum);
}
};
// just in case you want to add AnimalChooser s dynamially to animalChoosers
animalChoosers.getChildren().addListener(new ListChangeListener<Node>() {
#Override
public void onChanged(ListChangeListener.Change<? extends Node> c) {
while (c.next()) {
// add remove listeners updating the total
for (Node n : c.getRemoved()) {
AnimalChooser chooser = (AnimalChooser) n;
chooser.priceProperty().removeListener(listener);
}
for (Node n : c.getAddedSubList()) {
AnimalChooser chooser = (AnimalChooser) n;
chooser.priceProperty().addListener(listener);
}
}
listener.invalidated(null);
}
});
for (int i = 0; i < 10; i++) {
animalChoosers.getChildren().add(new AnimalChooser(animals));
}
BorderPane root = new BorderPane(animalChoosers);
Text totalText = new Text();
totalText.textProperty().bind(total.asString("total: %.2f $"));
root.setBottom(totalText);
BorderPane.setMargin(totalText, new Insets(20));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
I have a JavaFX TableColumn. The column has a ComboBoxTableCell populated by ObservableList I pass to it.
I have an "active" list and a "deactive" list for the combobox. After a selection is made, I wish to remove the selected item from the active, "real", list (and add it to the deactivated items list).
After a selection has been made, a CellEditEvent is being fired and sets up the row object by the selected one (from the combobox).
The thing is, when I remove the select item from the list, in the event handler, my CellEditEvent event handler got fired again - this time with a wrong "new value"!
Of course this behavior breaks my flow logic completely.
Any ideas about how to solve this situation? Thank you
An SSCCE of the situation:
package tableviewexample;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.*;
import javafx.event.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class TableViewExample extends Application {
#Override
public void start(Stage primaryStage) {
TableView<MappingItem> table = new TableView<>();
// FIRST COLUMN
TableColumn<MappingItem, String> colA = new TableColumn<>("Excel Column");
colA.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<MappingItem, String>, ObservableValue<String>> () {
#Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<MappingItem, String> param) {
return new ReadOnlyObjectWrapper(param.getValue().getExcelColumnName());
}
});
//SECOND COLUMN
TableColumn<MappingItem, GoldplusField> colB = new TableColumn<>("Database Field Column");
colB.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<MappingItem, GoldplusField>, ObservableValue<GoldplusField>> () {
#Override
public ObservableValue<GoldplusField> call(TableColumn.CellDataFeatures<MappingItem, GoldplusField> param) {
return new ReadOnlyObjectWrapper(param.getValue().getGpField());
}
});
GoldplusField gp1 = new GoldplusField("T1", "fName", "First Name");
GoldplusField gp2 = new GoldplusField("T1", "phn", "Phone");
GoldplusField gp3 = new GoldplusField("T2", "lName", "Last Name");
GoldplusField gp4 = new GoldplusField("T2", "adrs", "Address");
ObservableList<GoldplusField> deactiveFieldsList = FXCollections.observableArrayList();
ObservableList<GoldplusField> activeFieldsList = FXCollections.observableArrayList(gp1, gp2, gp3, gp4);
colB.setCellFactory(ComboBoxTableCell.forTableColumn(new FieldToStringConvertor(), activeFieldsList));
colB.setOnEditCommit(
new EventHandler<TableColumn.CellEditEvent<MappingItem, GoldplusField>>() {
#Override
public void handle(TableColumn.CellEditEvent<MappingItem, GoldplusField> t) {
if (t.getNewValue() != null) {
deactiveFieldsList.add(t.getNewValue());
((MappingItem) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setGpField(t.getNewValue());
// ******************************************************************************************** //
// This creates a new instance of the EventHandler in which I get the "next" item on the List.
// ******************************************************************************************** //
activeFieldsList.remove(t.getNewValue());
}
}
}
);
//THIRD COLUMN
TableColumn<MappingItem, String> colC = new TableColumn<>("Test Column");
PropertyValueFactory<MappingItem, String> nameFac = new PropertyValueFactory<>("name");
colC.setCellValueFactory(nameFac);
colC.setCellFactory(TextFieldTableCell.forTableColumn());
table.setEditable(true);
table.getColumns().addAll(colA, colB, colC);
GoldplusField gp5 = new GoldplusField("T1", "other", "Other");
MappingItem mi1 = new MappingItem("name", gp5);
mi1.excelColumnName.set("name1");
MappingItem mi2 = new MappingItem("phone", gp5);
mi2.excelColumnName.set("nam2");
ObservableList<MappingItem> miList = FXCollections.observableArrayList(mi1, mi2);
table.setItems(miList);
StackPane root = new StackPane();
root.getChildren().add(table);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
class FieldToStringConvertor extends StringConverter<GoldplusField> {
#Override
public String toString(GoldplusField object) {
if (object != null)
return object.getGpName();
else
return "";
}
#Override
public GoldplusField fromString(String string) {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
}
public class MappingItem {
private StringProperty excelColumnName = new SimpleStringProperty(this, "excelColumnName");
private ObjectProperty<GoldplusField> gpField = new SimpleObjectProperty<GoldplusField>(this, "gpField");
public String getExcelColumnName() { return excelColumnName.get(); }
public void setExcelColumnName(String excelColumnName) { this.excelColumnName.set(excelColumnName); }
public StringProperty excelColumnNameProperty() { return excelColumnName; }
public GoldplusField getGpField() { return gpField.get(); }
public void setGpField(GoldplusField gpField) { this.gpField.set(gpField); }
public ObjectProperty gpFieldProperty() { return this.gpField; }
public MappingItem(String columnName) { this.excelColumnName.set(columnName); }
public MappingItem(GoldplusField gpField) { this.gpField.set(gpField); }
public MappingItem(String columnName, GoldplusField gpField) {
this.excelColumnName.set(columnName);
this.gpField.set(gpField);
}
}
public class GoldplusField {
private StringProperty table = new SimpleStringProperty(this, "table");
private StringProperty dbName = new SimpleStringProperty(this, "dbName");
private StringProperty gpName = new SimpleStringProperty(this, "gpName");
public String getDbName() { return dbName.get(); }
public String getGpName() { return gpName.get(); }
public String getTable() { return table.get(); }
public void setDbName(String dbName) { this.dbName.set(dbName); }
public void setGpName(String gpName) { this.gpName.set(gpName); }
public void setTable(String table) { this.table.set(table); }
public StringProperty tableProperty() { return this.table; }
public StringProperty gpNameProperty() { return this.gpName; }
public StringProperty dbNameProperty() { return this.dbName; }
public GoldplusField(String table, String dbName, String gpName) {
this.dbName.set(dbName);
this.gpName.set(gpName);
this.table.set(table);
}
}
}
I have used the sample Tree User interface on the JAVA FX 2 site and changed the type from String to PayString and all seems to work well. My next step is in the setCellFactory 'TextFieldTreeCellImpl' I need to assign a different context menu to the cells depending upon the value of PayString.level an integer.
How do I reference the value of the data fields associated with the current cell.
Below are the two source files code in packages treeviewsample. The position I need the data is marked with +++++++++++++++++++++++++ for ease of finding.
`/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package treeviewsample;
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.VBox;
public class TreeViewSample extends Application {
private final Node rootIcon;
private final Image depIcon;
List<Employee> employees = Arrays.<Employee>asList(
new Employee(1, "Ethan Williams", "Sales Department"),
new Employee(2, "Emma Jones", "Sales Department"),
new Employee(3, "Michael Brown", "Sales Department"),
new Employee(4, "Anna Black", "Sales Department"),
new Employee(5, "Rodger York", "Sales Department"),
new Employee(6, "Susan Collins", "Sales Department"),
new Employee(7, "Mike Graham", "IT Support"),
new Employee(8, "Judy Mayer", "IT Support"),
new Employee(9, "Gregory Smith", "IT Support"),
new Employee(10, "Jacob Smith", "Accounts Department"),
new Employee(11, "Isabella Johnson", "Accounts Department"));
TreeItem<PayString> rootNode;
Integer next;
public static void main(String[] args) {
Application.launch(args);
}
public TreeViewSample() {
this.next = 12;
this.depIcon = new Image(getClass().getResourceAsStream("department.png"));
this.rootIcon = new ImageView(new Image(getClass().getResourceAsStream("root.png")));
this.rootNode = new TreeItem<>(new PayString ("MyCompany Human Resources", 0,0), rootIcon);
}
#Override
public void start(Stage stage) {
rootNode.setExpanded(true);
for (Employee employee : employees) {
TreeItem<PayString> empLeaf = new TreeItem<>(new PayString(employee.getName(),2,employee.getId()));
boolean found = false;
for (TreeItem<PayString> depNode : rootNode.getChildren()) {
if (depNode.getValue().toString().contentEquals(employee.getDepartment())){
depNode.getChildren().add(empLeaf);
found = true;
break;
}
}
if (!found) {
TreeItem depNode = new TreeItem<>(new PayString(employee.getDepartment(),1,employee.getId()),
new ImageView(depIcon)
);
rootNode.getChildren().add(depNode);
depNode.getChildren().add(empLeaf);
}
}
stage.setTitle("Tree View Sample");
VBox box = new VBox();
final Scene scene = new Scene(box, 400, 300);
scene.setFill(Color.LIGHTGRAY);
TreeView<PayString> treeView = new TreeView<>(rootNode);
treeView.setEditable(true);
treeView.setCellFactory(new Callback<TreeView<PayString>,TreeCell<PayString>>(){
#Override
public TreeCell<PayString> call(TreeView<PayString> p) {
return new TextFieldTreeCellImpl();
}
});
box.getChildren().add(treeView);
stage.setScene(scene);
stage.show();
}
private final class TextFieldTreeCellImpl extends TreeCell<PayString> {
private TextField textField;
private ContextMenu addMenu = new ContextMenu();
/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
* This is where I need to be able to extract the values in the current TreeCell<PayString>
* to be able to create the appropriate context menu.
* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*/
private String curr = getString();
public TextFieldTreeCellImpl() {
TreeItem<PayString> paystring = treeItemProperty().getValue();
MenuItem addMenuItem = new MenuItem("Add Employee");
MenuItem addMenuItem2 = new MenuItem("Add Address");
MenuItem addMenuItem3 = new MenuItem(curr);
addMenu.getItems().add(addMenuItem2);
addMenu.getItems().add(addMenuItem3);
addMenuItem.setOnAction(new EventHandler() {
#Override
public void handle(Event t) {
TreeItem newEmployee =
new TreeItem<>(new PayString ("New Employee",3,next));
next ++;
getTreeItem().getChildren().add(newEmployee);
}
});
}
#Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setText(null);
setGraphic(textField);
textField.selectAll();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(getString());
setGraphic(getTreeItem().getGraphic());
}
#Override
public void updateItem(PayString item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(getTreeItem().getGraphic());
if (
!getTreeItem().isLeaf()&&getTreeItem().getParent()!= null
){
setContextMenu(addMenu);
}
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(new PayString (textField.getText(),3,next));
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
public static class Employee {
private final SimpleStringProperty name;
private final SimpleStringProperty department;
private final Integer id;
private Employee(Integer id, String name, String department) {
this.name = new SimpleStringProperty(name);
this.department = new SimpleStringProperty(department);
this.id = new Integer(id);
}
public Integer getId() {
return id;
}
public String getName() {
return name.get();
}
public void setName(String fName) {
name.set(fName);
}
public String getDepartment() {
return department.get();
}
public void setDepartment(String fName) {
department.set(fName);
}
}
}
package treeviewsample;
import java.util.Objects;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
*
* #author Len
*/
public class PayString {
private final String description;
private final Integer level;
private final Integer id;
public PayString(String description, Integer level, Integer id) {
this.id = id;
this.level = level;
this.description = description;
}
public int getId() {
return id;
}
public int getLevel() {
return level;
}
public String getDescription() {
return description;
}
#Override
public int hashCode() {
int hash = 7;
return hash;
}
public boolean contentEquals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final PayString other = (PayString) obj;
if (!Objects.equals(this.description, other.description)) {
return false;
}
return true;
}
#Override
public String toString() {
return description;
}
}
`
You can always access the current data associated with your cell by calling
PayString value = getTreeItem().getValue()
However there is no 1:1 relationship between a cell and its value. JavaFX creates as many TreeCells it needs to display. It then dynamically assigns them to the underlying data items by calling the method
public void updateItem(PayString item, boolean empty) { ... }
This is where you can set a context menu that depends on the current data item. And there you get the data item passed in as parameter anyway.
At the code position you marked current data item will be null.