I would like to customize a ListView cell's display. Assuming that there's a ListView containing objects of a Person class and two buttons:
To add (generate) a new Person
To remove selected Person
I'd like to achieve the following goals:
When a cell is not selected, it has to show getBasicView() of a Person which is a name field.
When a cell is selected, it has to show getExpandedView() of a Person which is a multiline text name + "/n" + surname.
What the problem is?
The code I wrote fills requirements given above but additional bugs appeared:
When adding a new Person for a blink of an eye the display of cells changes to a toString() method which is not implemented (so the user sees sample.Person#c4f324 alike trash).
When removing a Person from the last cell weird things start to happen. The removed Person name remains in the cell which moves two cells down and because it no longer contains a Person object - it cannot be cleared.
I tried to add listeners to the cells' itemProperty which could check if item is null, after what I could set text to "" but unfortunately it does not work. Does anyone have an idea how can I make my code fully functional?
Providing an SSCCE (assuming that all the files are in the sample package):
Main.java:
package sample;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class Main extends Application {
public void start(Stage stage) {
FXMLLoader fxmlLoader = new FXMLLoader();
fxmlLoader.setController(Controller.class);
try {
Parent parent = FXMLLoader.load(getClass().getResource("/sample/sample.fxml"));
Scene scene = new Scene(parent);
stage.setScene(scene);
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch();
}
}
sample.fxml:
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.VBox?>
<HBox xmlns:fx="http://javafx.com/fxml" fx:controller="sample.Controller">
<VBox>
<Button text="Add Person" onAction="#addPerson"/>
<Button text="Delete Person" onAction="#deletePerson"/>
</VBox>
<ListView prefHeight="400" prefWidth="400" fx:id="listView"/>
</HBox>
Controller.java:
package sample;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.cell.TextFieldListCell;
import java.util.concurrent.ThreadLocalRandom;
public class Controller {
#FXML
private ListView<Person> listView;
private ObservableList<Person> personList = FXCollections.observableArrayList();
private String [] names = {"John", "Katherine", "Michael", "August", "Peter"};
private String [] surnames = {"Jones", "Mayer", "Stevens", "Wayne", "Milton"};
#FXML
private void initialize() {
initializeListCells();
initializeList();
}
private void initializeList() {
for (int i = 0 ; i < 5 ; i++) {
personList.add(generatePerson());
}
listView.setItems(personList);
}
private void initializeListCells() {
listView.setCellFactory(param -> {
TextFieldListCell<Person> cell = new TextFieldListCell<Person>();
cell.selectedProperty().addListener((observable, oldValue, newValue) -> handleCellDisplaying(cell));
cell.itemProperty().addListener((observable, oldValue, newValue) -> handleCellDisplaying(cell));
return cell;
});
}
private void handleCellDisplaying(TextFieldListCell<Person> cell) {
Person person = cell.getItem();
if (person != null) {
Platform.runLater(() -> {
if (!cell.isSelected()) {
cell.setText(person.getBasicView());
} else {
cell.setText(person.getExpandedView());
}
});
} else {
cell.setText("");
}
}
#FXML
private void addPerson() {
personList.add(generatePerson());
}
#FXML
private void deletePerson() {
MultipleSelectionModel<Person> selectionModel = listView.getSelectionModel();
if (!selectionModel.isEmpty()) {
int selectedIndex = selectionModel.getSelectedIndex();
personList.remove(selectedIndex);
}
}
private Person generatePerson() {
int nameRandom = ThreadLocalRandom.current().nextInt(1,5);
int surnameRandom = ThreadLocalRandom.current().nextInt(1,5);
return new Person(names[nameRandom],surnames[surnameRandom]);
}
}
Person.java:
package sample;
public class Person {
private String name;
private String surname;
public Person(String name, String surname) {
this.name = name;
this.surname = surname;
}
public String getBasicView() {
return name;
}
public String getExpandedView() {
return name + "\n" + surname;
}
}
The problem is that TextFieldListCell implements the cell lifecycle methods (updateItem, etc) and effectively calls setText(item.toString()) at various points. This is clearly interfering with the behavior you are trying to implement. (For example, the issue with deleting the last cell seems to happen because setText("") is called before the TextFieldListCell resets the text to its previous value. If you use your Platform.runLater(...) hack so that it surrounds the complete if-else clause, then this problem goes away. However...)
If you don't need the cells to be editable, there is no need to use a TextFieldListCell: just use a plain ListCell. (Also note there is no need for the Platform.runLater(...) in the handler for changed item/selected status.)
package sample;
import java.util.concurrent.ThreadLocalRandom;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MultipleSelectionModel;
public class Controller {
#FXML
private ListView<Person> listView;
private ObservableList<Person> personList = FXCollections.observableArrayList();
private String[] names = { "John", "Katherine", "Michael", "August", "Peter" };
private String[] surnames = { "Jones", "Mayer", "Stevens", "Wayne", "Milton" };
#FXML
private void initialize() {
initializeListCells();
initializeList();
}
private void initializeList() {
for (int i = 0; i < 5; i++) {
personList.add(generatePerson());
}
listView.setItems(personList);
}
private void initializeListCells() {
listView.setCellFactory(param -> {
ListCell<Person> cell = new ListCell<Person>();
cell.selectedProperty().addListener((observable, oldValue, newValue) -> handleCellDisplaying(cell));
cell.itemProperty().addListener((observable, oldValue, newValue) -> handleCellDisplaying(cell));
return cell;
});
}
private void handleCellDisplaying(ListCell<Person> cell) {
Person person = cell.getItem();
if (person != null) {
if (!cell.isSelected()) {
cell.setText(person.getBasicView());
} else {
cell.setText(person.getExpandedView());
}
} else {
cell.setText("");
}
}
#FXML
private void addPerson() {
personList.add(generatePerson());
}
#FXML
private void deletePerson() {
MultipleSelectionModel<Person> selectionModel = listView.getSelectionModel();
if (!selectionModel.isEmpty()) {
int selectedIndex = selectionModel.getSelectedIndex();
personList.remove(selectedIndex);
}
}
private Person generatePerson() {
int nameRandom = ThreadLocalRandom.current().nextInt(1, 5);
int surnameRandom = ThreadLocalRandom.current().nextInt(1, 5);
return new Person(names[nameRandom], surnames[surnameRandom]);
}
}
Related
Is there a way to bind an object's field to a TreeItem so that when the field is changed, the TreeItem is automatically updated in the TreeView?
I have objects with several fields that are set in TreeItems in a TreeView. The objects fields are updated sometimes at 5 to 10 times per second via their setters and I would like the TreeItems to update on the fly as the fields change. Any help on this would be greatly appreciated. I can't seem to find a way to do this other than reloading the entire TreeView.
Edit: Here is the existing code...
The list listener sets up the treeview for the player list. If the number of objects in the list changes, the tree is redrawn. That is all fine. What I want is, if you were to place code in the handle button click method that continuously changes player attributes(in a loop, maybe 2-10 times per second), that should be reflected in the treeview as those attributes change.(Just a simulation, I cant get into how those attributes are really being changed at that rate). An example of how to do this would be great.
package application;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import viewcontroller.PlayerViewController;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
public class Main extends Application {
private Stage mainStage;
#FXML
private AnchorPane mainView;
#Override
public void start(Stage mainStage) {
this.mainStage = mainStage;
PlayerList.playerListListener();
this.showMainLayout();
}
/**
* shows the main screen
*/
public void showMainLayout() {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(Main.class.getResource("/viewcontroller/playerView.fxml"));
try {
mainView = (AnchorPane) loader.load();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Scene scene = new Scene(mainView);
mainStage.sizeToScene();
mainStage.setScene(scene);
mainStage.show();
PlayerViewController controller = loader.getController();
controller.setMainApp(this);
}
public static void main(String[] args) {
launch(args);
}
}
package model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Player {
private StringProperty str1 = new SimpleStringProperty();
private StringProperty str2 = new SimpleStringProperty();
private StringProperty str3 = new SimpleStringProperty();
public Player() {
}
public Player(String str1, String str2, String str3) {
super();
this.str1 = new SimpleStringProperty(str1);
this.str2 = new SimpleStringProperty(str2);
this.str3 = new SimpleStringProperty(str3);
}
public StringProperty str1() {
return str1;
}
public String getStr1() {
return str1.get();
}
public void setStr1(String str1) {
this.str1.set(str1);
}
public StringProperty str2() {
return str2;
}
public String getStr2() {
return str2.get();
}
public void setStr2(String str2) {
this.str2.set(str2);
}
public StringProperty str3() {
return str3;
}
public String getStr3() {
return str3.get();
}
public void setStr3(String str3) {
this.str3.set(str3);
}
}
package application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.control.TreeItem;
import model.Player;
public class PlayerList {
private final ObservableList<Player> playerList = FXCollections.observableArrayList();
private static PlayerList instance = new PlayerList();
public static TreeItem<String> rootNode = new TreeItem<String>("Root");
private PlayerList() {}
public ObservableList<Player> getPlayerList() {
return playerList;
}
public static PlayerList getInstance() {
return instance;
}
public static void playerListListener() {
//adds a list listener which reloads tree if objects are added or removed
PlayerList.getInstance().getPlayerList().addListener((ListChangeListener<? super Player>) new ListChangeListener<Player>() {
#SuppressWarnings("unchecked")
public void onChanged(Change<? extends Player> change) {
//re-sort list
//System.out.println("List changed");
rootNode.getChildren().clear();//clear tree before reloading on list change
for(Player p: PlayerList.getInstance().getPlayerList()) {
//Parent Node
TreeItem<String> playerName = new TreeItem<String>(p.getStr1());
//Items
TreeItem<String> attr1 = new TreeItem<String>(p.getStr2());
TreeItem<String> attr2 = new TreeItem<String>(p.getStr3());
playerName.getChildren().addAll(attr1, attr2);
playerName.setExpanded(true);
rootNode.getChildren().add(playerName);
rootNode.setExpanded(true);
}
}
});
}
}
package viewcontroller;
import java.net.URL;
import java.util.ResourceBundle;
import application.Main;
import application.PlayerList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TreeView;
import javafx.scene.layout.VBox;
import model.Player;
public class PlayerViewController implements Initializable {
#FXML
private VBox vbox;
private TreeView<String> treeView;
public void setMainApp(Main main) {
// TODO Auto-generated method stub
}
#Override
public void initialize(URL location, ResourceBundle resources) {
// TODO Auto-generated method stub
Player p1 = new Player("Name1", "Attr1", "Attr2");
Player p2 = new Player("Name2", "Attr1", "Attr2");
Player p3 = new Player("Name3", "Attr1", "Attr2");
PlayerList.getInstance().getPlayerList().addAll(p1, p2, p3);//add players to list
treeView = new TreeView<String>(PlayerList.rootNode);
treeView.setShowRoot(false);
vbox.getChildren().add(treeView);//add tree view to GUI
}
#FXML
private void handleButtonClick() {
/*this method can hold code that would run in a loop, continuously setting new values
on p1, p2, and p3 attributes and would show the tree items updating automatically*/
}
}
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane fx:id="mainView" prefHeight="400.0" prefWidth="200.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="viewcontroller.PlayerViewController">
<children>
<VBox fx:id="vbox" layoutX="14.0" layoutY="14.0" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Button mnemonicParsing="false" onAction="#handleButtonClick" text="Button" />
</children>
</VBox>
</children>
</AnchorPane>
We try to achieve the following:
When a node gets selected in a JavaFX TreeTableView, also "the path to the root", i.e., the parent, the grandparent, and so on should get selected. Selected in this case means highlighted with a different background color, see the image (in the example, the node on Level 2 has been clicked by the user).
Is there a built-in function how to achieve this?
We tried using CSS but did not succeed.
There's no "built-in function" to do this. Use a row factory on the tree table view to create rows that observe the selected item, and set a pseudoclass on the row accordingly.
For example:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import javafx.stage.Stage;
public class TreeTableViewHighlightSelectionPath extends Application {
#Override
public void start(Stage primaryStage) {
TreeTableView<Item> table = new TreeTableView<Item>();
PseudoClass ancestorOfSelection = PseudoClass.getPseudoClass("ancestor-of-selection");
table.setRowFactory(ttv -> new TreeTableRow<Item>() {
{
table.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSelection, newSelection) -> updateStyleClass());
}
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
updateStyleClass();
}
private void updateStyleClass() {
pseudoClassStateChanged(ancestorOfSelection, false);
TreeItem<Item> treeItem = table.getSelectionModel().getSelectedItem();
if (treeItem != null) {
for (TreeItem<Item> parent = treeItem.getParent() ; parent != null ; parent = parent.getParent()) {
if (parent == getTreeItem()) {
pseudoClassStateChanged(ancestorOfSelection, true);
break ;
}
}
}
}
});
TreeTableColumn<Item, String> itemCol = new TreeTableColumn<>("Item");
itemCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getValue().getName()));
table.getColumns().add(itemCol);
TreeTableColumn<Item, Number> valueCol = new TreeTableColumn<>("Value");
valueCol.setCellValueFactory(cellData -> cellData.getValue().getValue().valueProperty());
table.getColumns().add(valueCol);
table.setRoot(createRandomTree());
Scene scene = new Scene(table);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeItem<Item> createRandomTree() {
TreeItem<Item> root = new TreeItem<>(new Item("Item 1", 0));
Random rng = new Random();
List<TreeItem<Item>> items = new ArrayList<>();
items.add(root);
for (int i = 2 ; i <= 20 ; i++) {
TreeItem<Item> item = new TreeItem<>(new Item("Item "+i, rng.nextInt(1000)));
items.get(rng.nextInt(items.size())).getChildren().add(item);
items.add(item);
}
return root ;
}
public static class Item {
private final String name ;
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
this.name = name ;
setValue(value);
}
public String getName() {
return name ;
}
public IntegerProperty valueProperty() {
return value ;
}
public final int getValue() {
return valueProperty().get();
}
public final void setValue(int value) {
valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
Now you can just style the "ancestor of a selected node" in CSS:
File style.css:
.tree-table-row-cell:ancestor-of-selection {
-fx-background: -fx-selection-bar;
-fx-table-cell-border-color: derive(-fx-selection-bar, 20%);
}
(You may want to modify the CSS to get better control, e.g. set different colors for selected rows in a non-focused table, etc. See the default stylesheet for details on the default style.)
Here's a screenshot of the above test app:
Scenario: Im passing in comma separated values to a table. 2 columns, one with the original value, one with a text field with the values populated inside. They are lined up so I can add/change values, and copy the changes to a string.
I cannot figure out how to capture the TextField data after they are put in the table. Code below:
Main:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Parent;
import javafx.scene.Scene;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
try {
Parent root = FXMLLoader.load(getClass().getResource("/application/MainFxml.fxml")); //this is the file that
Scene scene = new Scene(root,800,800); ////100,100 is width and height of window
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
}
Controller:
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.ResourceBundle;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.HBox;
public class MainFxmlController implements Initializable {
public static int count=-1;
#FXML public TableView<tableClass> table = new TableView<tableClass>();
#FXML private TableColumn<tableClass, String>col1;
#SuppressWarnings({ "unchecked", "rawtypes" })
#FXML public TableColumn<tableClass, Row> col2 = new TableColumn("Row");
#FXML public TextField txt;
#FXML public Button btn, btn2;
#FXML public ListView<String> listView = new ListView<String>();
public static ArrayList<String> input = new ArrayList<String>();
final HBox hb = new HBox();
public ObservableList<tableClass> obList = FXCollections.observableArrayList(); // each column contains an observable list object
public ObservableList<tableClass> loadTable(){
return obList; //return data object
}////END loadData
#Override
public void initialize(URL url, ResourceBundle rb) {
table.setEditable(true);
col1.setCellValueFactory(cellData -> cellData.getValue().getCol1());
col2.setCellFactory((param) -> new TextFieldCell<tableClass, Row>(EnumSet.allOf(Row.class)));
col2.setCellValueFactory(new PropertyValueFactory<tableClass, Row>("Row"));
col2.setOnEditCommit(
new EventHandler<CellEditEvent<tableClass, Row>>() {
#Override
public void handle(CellEditEvent<tableClass, Row> t) {
((tableClass) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setCol2(t.getNewValue());
}
}
);
tableClass Entry = new tableClass(" ", Row.Row1); //create the table using getters/setters from Table Class
table.getItems().addAll(Entry);
table.setItems(loadTable());
col1.setCellFactory(TextFieldTableCell.<tableClass>forTableColumn()); //Makes the columns themselves editable
col1.setOnEditCommit(
new EventHandler<CellEditEvent<tableClass, String>>() {
#Override
public void handle(CellEditEvent<tableClass, String> t) {
((tableClass) t.getTableView().getItems().get( t.getTablePosition().getRow())).setCol1(t.getNewValue());
}
}
);
col1.setStyle( "-fx-alignment: BOTTOM-RIGHT;"); //to alight text next to textArea
txt.setText("fsad,0,0,gfds,43,4,4,fdsg,rtewrtwe,0,67,3,4,4,,4,44,,4"); //TO BE ROMOVED UPON COMPLETION
}//end initialize
public void buttonAction(ActionEvent e){
if(txt.getText() != ""){
System.out.println(txt.getText());
ArrayList<String> myList = new ArrayList<String>(Arrays.asList(txt.getText().split(",")));
input = myList;
}
for(int i =0; i< input.size(); i++){
Row.Row1.equals(input.get(i).toString());
obList.add(new tableClass(input.get(i).toString(), Row.Row1));
}//end for
}//end buttonAction
public void captureText(ActionEvent e){
/*
* HERE I NEED TO CAPTURE VALUES FROM THE TEXT FIELDS
* IN COL2
*/
}
public static enum Row { //enum for the dxTable radio button if you want any other options, add another value and another radio buttton will be populated
Row1;
}
public static class TextFieldCell<S,T extends Enum<T>> extends TableCell<S,T>{
private EnumSet<T> enumeration;
public TextFieldCell(EnumSet<T> enumeration) {
this.enumeration = enumeration;
}
#Override
protected void updateItem(T item, boolean empty)
{
super.updateItem(item, empty);
if (!empty)
{
// gui setup
HBox hb = new HBox(7);
hb.setAlignment(Pos.CENTER);
// create a radio button for each 'element' of the enumeration
for (Enum<T> enumElement : enumeration) {
try{
TextField textField = new TextField(input.get(count));
textField.setUserData(enumElement);
hb.getChildren().add(textField);
}catch(IndexOutOfBoundsException e){}
}
// issue events on change of the selected radio button
setGraphic(hb);
count++;
} //end if
else
setGraphic(null);
}//updateItem
}//END TextFieldCell class
public static class tableClass{ ///table object with getters and setters.
public final SimpleStringProperty col1;
public final SimpleObjectProperty<Row> col2 = new SimpleObjectProperty<Row>();
public tableClass(String col1, Row t) { //uses an enum for the second type
this.col1 = new SimpleStringProperty(col1);
this.col2.setValue(t);
}
public StringProperty getCol1() {
return col1;
}
public void setCol1(String i) {
col1.set(i);
}
public void setCol2(Row t) {
col2.set(t);
}
public Row getCol2() {
return col2.get();
}
public String getCol2(int index) {
return "";
}
}//end table class
}//end controller
FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="800.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MainFxmlController">
<children>
<Button fx:id="btn" layoutX="723.0" layoutY="26.0" mnemonicParsing="false" onAction="#buttonAction" text="btn" />
<TextField fx:id="txt" layoutX="41.0" layoutY="26.0" prefHeight="25.0" prefWidth="647.0" />
<TableView fx:id="table" editable="true" layoutX="41.0" layoutY="106.0" prefHeight="588.0" prefWidth="451.0">
<columns>
<TableColumn fx:id="col1" prefWidth="75.0" text="C1" />
<TableColumn fx:id="col2" prefWidth="114.0" text="C2" />
</columns>
</TableView>
<Button fx:id="btn2" layoutX="507.0" layoutY="669.0" mnemonicParsing="false" onAction="#captureText" text="Button" />
</children>
</AnchorPane>
Any help would be greatly appreciated.
I'm not sure exactly what are trying to do, but I have found a way to get the TextField when they press enter.
In your TextFieldCell class, I added a setOnAction to grab what the user input in the TextField.
TextField textField = new TextField(input.get(count));
textField.setUserData(enumElement);
textField.setOnAction(event -> {
System.out.println("Gotcha");
});
You can use that, instead of the button, to do whatever you want to the text entered.
I have no idea how to use your button to programmatically grab all of the TextField in col2 and grab their text.
But, with the setOnAction you can add whatever you like in there and hopefully do what you need to do.
Edit 1
With some heavy editing of your work, I have done it!
I will leave in the old answer, since it applies directly to your source code.
First, I made col2 Look like this.
#FXML public TableColumn<tableClass, TextField> col2;
Then, I used that to my advantage and setCellValueFactory like so:
col2.setCellValueFactory(new PropertyValueFactory<>("col2"));
Everything to do with col2 I had to update/remove to get it to work with the TextField, and I ended up with a much shorter source code, that can do what you want. Example is in the captureText method.
I edited the tableClass to make use of the TextField, I removed your cellFactory class altogether. Hope this helps.
#FXML public TableView<tableClass> table;
#FXML private TableColumn<tableClass, String>col1;
#SuppressWarnings({ "unchecked", "rawtypes" })
#FXML public TableColumn<tableClass, TextField> col2;
#FXML public TextField txt;
#FXML public Button btn, btn2;
public static ArrayList<String> input = new ArrayList<String>();
public static Group hb = new Group();
public ObservableList<tableClass> obList = FXCollections.observableArrayList(); // each column contains an observable list object
public ObservableList<tableClass> loadTable(){
return obList; //return data object
}////END loadData
#Override
public void initialize(URL url, ResourceBundle rb) {
table.setEditable(true);
col1.setCellValueFactory(cellData -> cellData.getValue().col1Property());
col2.setCellValueFactory(new PropertyValueFactory<>("col2"));
table.setItems(loadTable());
col1.setCellFactory(TextFieldTableCell.<tableClass>forTableColumn()); //Makes the columns themselves editable
col1.setOnEditCommit(
new EventHandler<CellEditEvent<tableClass, String>>() {
#Override
public void handle(CellEditEvent<tableClass, String> t) {
((tableClass) t.getTableView().getItems().get( t.getTablePosition().getRow())).setCol1(t.getNewValue());
}
}
);
col1.setStyle( "-fx-alignment: BOTTOM-RIGHT;"); //to alight text next to textArea
txt.setText("fsad,0,0,gfds,43,4,4,fdsg,rtewrtwe,0,67,3,4,4,,4,44,,4"); //TO BE ROMOVED UPON COMPLETION
}//end initialize
public void buttonAction(ActionEvent e){
if(txt.getText() != ""){
System.out.println(txt.getText());
ArrayList<String> myList = new ArrayList<String>(Arrays.asList(txt.getText().split(",")));
input = myList;
}
for(int i =0; i< input.size(); i++){
obList.add(new tableClass(input.get(i),input.get(i)));
}//end for
}//end buttonAction
public void captureText(ActionEvent e) {
obList.forEach(event -> {
event.setCol1(event.getCol2().getText());
});
/*
* HERE I NEED TO CAPTURE VALUES FROM THE TEXT FIELDS
* IN COL2
*/
}
public static class tableClass{ ///table object with getters and setters.
public final SimpleStringProperty col1;
public final TextField col2;
public tableClass(String col1, String col2) { //uses an enum for the second type
this.col1 = new SimpleStringProperty(col1);
this.col2 = new TextField(col2);
}
public StringProperty col1Property() {
return col1;
}
public String getCol1(){
return col1.get();
}
public void setCol1(String i) {
col1.set(i);
}
public void setCol2(String tx) {
col2.setText(tx);
}
public TextField getCol2() {
return col2;
}
}//end table class
I currently have the following object data structure:
Item
String name
ArrayList of information
Character
String name
Collection of Item
Account
String name
Collection of Character (up to 8 max)
I want to make a TreeView that looks like the following:
Root(invisible)
======Jake(Account)
============JakesChar(Character)
==================Amazing Sword(Item)
==================Broken Bow(Item)
==================Junk Metal(Item)
======Mark(Account)
============myChar(Character)
==================Godly Axe(Item)
======FreshAcc(Account)
======MarksAltAcc(Account)
============IllLvlThisIPromise(Character)
======Jeffrey(Account)
============Jeff(Character)
==================Super Gun(Item)
==================Better Super Gun(Item)
==================Super Gun Scope(Item)
I made all those names up and such, obviously the real implementation would be a lot more complex. How can this be done? The TreeItem requires each TreeItem to be the same type as its' parent.
The ONLY solution I have is to do the following:
public class ObjectPointer
{
Object pointer;
String name;
}
My TreeView would be of type ObjectPointer and on each row I would cast the ObjectPointer to Account, Character, or Item. This is AWFUL but I think it would work.
Sub Questions:
How do I get TreeItem(s) to detect setOnMouseHover events?
How do I get TreeItem(s) to not use the toString method of their type and instead a custom way of displaying the String property that they need?
How do I get the TreeItem(s) to display colored text in the GUI instead of plain text?
Thank you!
If you look at your model and think generically, all the classes have a degree of similarity, which you could factor out into a superclass:
package model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public abstract class GameObject<T extends GameObject<?>> {
public GameObject(String name) {
setName(name);
}
private final StringProperty name = new SimpleStringProperty();
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
private final ObservableList<T> items = FXCollections.observableArrayList();
public ObservableList<T> getItems() {
return items ;
}
public abstract void createAndAddChild(String name);
}
The type parameter T here represents the type of "child" objects. So your Account class (whose child type is GameCharacter - don't name classes the same as anything in java.lang, btw...) looks like
package model;
public class Account extends GameObject<GameCharacter> {
public Account(String name) {
super(name);
}
#Override
public void createAndAddChild(String name) {
getItems().add(new GameCharacter(name));
}
}
and similarly all the way down the hierarchy. I'd define an Information class (even though it just has a name) to make everything fit the structure, so:
package model;
public class Item extends GameObject<Information> {
public Item(String name) {
super(name);
}
#Override
public void createAndAddChild(String name) {
getItems().add(new Information(name));
}
}
and, since Information has no children, its child list is just an empty list:
package model;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Information extends GameObject<GameObject<?>> {
public Information(String name) {
super(name);
}
#Override
public ObservableList<GameObject<?>> getItems() {
return FXCollections.emptyObservableList();
}
#Override
public void createAndAddChild(String name) {
throw new IllegalStateException("Information has no child items");
}
}
Now every item in your tree is a GameObject<?>, so you can basically build a TreeView<GameObject<?>>. The tricky part is that your tree items need to reflect the structure already built in the model. Since you have observable lists there, you can do this with listeners on the lists.
You can use a cell factory on the tree to customize the appearance of the cells displaying the TreeItems. If you want a different appearance for each type of item, I'd recommend defining the styles in an external CSS class, and setting a CSS PseudoClass on the cell corresponding to the type of item. If you use some naming convention (I have that the pseudo-class name is the lower case version of the class name), it can be quite slick to do that. Here's a fairly simple example:
package ui;
import static java.util.stream.Collectors.toList;
import java.util.Arrays;
import java.util.List;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item;
public class Tree {
private final TreeView<GameObject<?>> treeView ;
private final List<Class<? extends GameObject<?>>> itemTypes = Arrays.asList(
Account.class, GameCharacter.class, Item.class, Information.class
);
public Tree(ObservableList<Account> accounts) {
treeView = new TreeView<>();
GameObject<?> root = new GameObject<Account>("") {
#Override
public ObservableList<Account> getItems() {
return accounts ;
}
#Override
public void createAndAddChild(String name) {
getItems().add(new Account(name));
}
};
TreeItem<GameObject<?>> treeRoot = createItem(root);
treeView.setRoot(treeRoot);
treeView.setShowRoot(false);
treeView.setCellFactory(tv -> {
TreeCell<GameObject<?>> cell = new TreeCell<GameObject<?>>() {
#Override
protected void updateItem(GameObject<?> item, boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if (empty) {
setText(null);
itemTypes.stream().map(Tree.this::asPseudoClass)
.forEach(pc -> pseudoClassStateChanged(pc, false));
} else {
textProperty().bind(item.nameProperty());
PseudoClass itemPC = asPseudoClass(item.getClass());
itemTypes.stream().map(Tree.this::asPseudoClass)
.forEach(pc -> pseudoClassStateChanged(pc, itemPC.equals(pc)));
}
}
};
cell.hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
if (isNowHovered && (! cell.isEmpty())) {
System.out.println("Mouse hover on "+cell.getItem().getName());
}
});
return cell ;
}
}
public TreeView<GameObject<?>> getTreeView() {
return treeView ;
}
private TreeItem<GameObject<?>> createItem(GameObject<?> object) {
// create tree item with children from game object's list:
TreeItem<GameObject<?>> item = new TreeItem<>(object);
item.setExpanded(true);
item.getChildren().addAll(object.getItems().stream().map(this::createItem).collect(toList()));
// update tree item's children list if game object's list changes:
object.getItems().addListener((Change<? extends GameObject<?>> c) -> {
while (c.next()) {
if (c.wasAdded()) {
item.getChildren().addAll(c.getAddedSubList().stream().map(this::createItem).collect(toList()));
}
if (c.wasRemoved()) {
item.getChildren().removeIf(treeItem -> c.getRemoved().contains(treeItem.getValue()));
}
}
});
return item ;
}
private PseudoClass asPseudoClass(Class<?> clz) {
return PseudoClass.getPseudoClass(clz.getSimpleName().toLowerCase());
}
}
Quick test, which works, but note you probably need to test more of the functionality:
package application;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item;
import ui.Tree;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Tree tree = new Tree(createAccounts());
TreeView<GameObject<?>> treeView = tree.getTreeView();
TextField addField = new TextField();
Button addButton = new Button("Add");
EventHandler<ActionEvent> addHandler = e -> {
TreeItem<GameObject<?>> selected = treeView
.getSelectionModel()
.getSelectedItem();
if (selected != null) {
selected.getValue().createAndAddChild(addField.getText());
addField.clear();
}
};
addField.setOnAction(addHandler);
addButton.setOnAction(addHandler);
addButton.disableProperty().bind(Bindings.createBooleanBinding(() -> {
TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
return selected == null || selected.getValue() instanceof Information ;
}, treeView.getSelectionModel().selectedItemProperty()));
Button deleteButton = new Button("Delete");
deleteButton.setOnAction(e -> {
TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
TreeItem<GameObject<?>> parent = selected.getParent() ;
parent.getValue().getItems().remove(selected.getValue());
});
deleteButton.disableProperty().bind(treeView.getSelectionModel().selectedItemProperty().isNull());
HBox controls = new HBox(5, addField, addButton, deleteButton);
controls.setPadding(new Insets(5));
controls.setAlignment(Pos.CENTER);
BorderPane root = new BorderPane(treeView);
root.setBottom(controls);
Scene scene = new Scene(root, 600, 600);
scene.getStylesheets().add(getClass().getResource("/ui/style/style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private ObservableList<Account> createAccounts() {
Account jake = new Account("Jake");
Account mark = new Account("Mark");
Account freshAcc = new Account("Fresh Account");
Account marksAltAcc = new Account("Mark's alternative account");
Account jeffrey = new Account("Jeffrey");
GameCharacter jakesChar = new GameCharacter("Jakes character");
Item amazingSword = new Item("Amazing Sword");
Item brokenBow = new Item("Broken Bow");
Item junkMetal = new Item("Junk Metal");
GameCharacter myChar = new GameCharacter("Me");
Item godlyAxe = new Item("Godly Axe");
GameCharacter level = new GameCharacter("I'll level this I promise");
GameCharacter jeff = new GameCharacter("Jeff");
Item superGun = new Item("Super Gun");
Item superGunScope = new Item("Super Gun Scope");
jake.getItems().add(jakesChar);
mark.getItems().add(myChar);
marksAltAcc.getItems().add(level);
jeffrey.getItems().add(jeff);
jakesChar.getItems().addAll(amazingSword, brokenBow, junkMetal);
myChar.getItems().add(godlyAxe);
jeff.getItems().addAll(superGun, superGunScope);
return FXCollections.observableArrayList(jake, mark, freshAcc, marksAltAcc, jeffrey);
}
}
and the CSS as an example:
.tree-cell, .tree-cell:hover:empty {
-fx-background-color: -fx-background ;
-fx-background: -fx-control-inner-background ;
}
.tree-cell:hover {
-fx-background-color: crimson, -fx-background ;
-fx-background-insets: 0, 1;
}
.tree-cell:account {
-fx-background: lightsalmon ;
}
.tree-cell:gamecharacter {
-fx-background: bisque ;
}
.tree-cell:item {
-fx-background: antiquewhite ;
}
.tree-cell:selected {
-fx-background: crimson ;
}
I have a ListView with a TextField above it. If a user enters in a search query into the textfield, the listview will update and filter itself to show relevant results.
The ListView shows items from a FilteredList, which is filled with Employee objects. Each Employee has a first and last name.
package application.ctrl;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.transformation.FilteredList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import application.Main;
import application.objects.Employee;
import application.objects.EmployeeDatabase;
public class EmployeePickerWidget extends VBox implements Initializable {
#FXML
private TextField textField;
#FXML
private Button addNewEmployee;
#FXML
private ListView<Employee> employeeList;
private FilteredList<Employee> filteredList;
private ContextMenu cm;
private CustomMenuItem item;
private ClickedEmployeeInterface parent;
public EmployeePickerWidget(ClickedEmployeeInterface parent) {
FXMLLoader loader = new FXMLLoader(this.getClass().getResource(
Main.EMPLOYEE_PICKER));
loader.setRoot(this);
loader.setController(this);
try {
loader.load();
} catch (IOException e) {
e.printStackTrace();
}
this.parent = parent;
}
#Override
public void initialize(URL location, ResourceBundle resources) {
setupEmployeeListView();
setupTextField();
}
private void setupEmployeeListView() {
filteredList = new FilteredList<Employee>(EmployeeDatabase.getInstance()
.getObservableList());
employeeList = new ListView<Employee>();
employeeList.setItems(filteredList);
employeeList.setOnMouseClicked(arg0 -> {
if (employeeList.getSelectionModel().getSelectedItem() != null) {
cm.hide();
parent.handleClickedEmployee();
}
});
}
private void setupTextField() {
textField.textProperty().addListener(
(observable, oldValue, newValue) -> {
filteredList.setPredicate(employee -> {
return filterHelper(employee, newValue);
});
});
textField.setText(" ");
textField.setText("");
textField.setOnMouseClicked(event -> cm
.show(textField, Side.BOTTOM, 0, 0));
cm = new ContextMenu();
item = new CustomMenuItem();
VBox container = new VBox();
container.setAlignment(Pos.CENTER_RIGHT);
container.getChildren().add(employeeList);
Button defineEmployeeBtn = new Button("Define New Employee");
defineEmployeeBtn.setOnAction(event -> {
FXMLLoader loader = new FXMLLoader(getClass().getResource(
Main.DEFINE_NEW_EMPLOYEE));
Parent root = null;
try {
root = loader.load();
} catch (IOException e) {
e.printStackTrace();
}
Scene newScene = new Scene(root);
Stage newStage = new Stage();
newStage.setScene(newScene);
newStage.show();
});
container.getChildren().add(defineEmployeeBtn);
item.setContent(container);
cm.getItems().add(item);
}
private boolean filterHelper(Employee employee, String query) {
String first = employee.getFirst().toLowerCase(), last = employee
.getLast().toLowerCase();
String[] querySplit = query.replace(",", "\\s").split("\\s+");
int length = querySplit.length;
for (int i = 0; i < length; i++)
querySplit[i] = querySplit[i].toLowerCase();
if (length == 1) {
if (first.contains(querySplit[0]) || last.contains(querySplit[0]))
return true;
else
return false;
} else if (length == 2) {
if (first.contains(querySplit[0]) || last.contains(querySplit[0]))
if (first.contains(querySplit[1]) || last.contains(querySplit[1]))
return true;
return false;
} else if (length == 3) {
return false;
}
return false;
}
public Employee getEmployee() {
return employeeList.getSelectionModel().getSelectedItem();
}
#FXML
public void addNewEmployee() {
}
}
interface ClickedEmployeeInterface {
void handleClickedEmployee();
}
If there were 3 employees named "Donald Trump", "Donald Smith", and "Donald Jackson" in the database, then the following needs to happen:
Typing up to the word "Donald" will show all 3 results.
Typing a space after Donald (resulting in "Donald ") will still show 3 results.
Typing a T after the previous query (resulting in "Donald T") should only show 1 result.
The problem is, after I enter in a space, the ListView breaks, and all of my Employees disappear from the ListView. When I click outside of the textfield and click back in again, it triggers this:
textField.setOnMouseClicked(event -> cm
.show(textField, Side.BOTTOM, 0, 0));
And my ListView suddenly works again, showing that one Employee.
How do I make the ListView filter properly without having to click out and back in?
I do not have the FXML file, so I wasn't able to replicate your problem. There are multiple problems with your code and this is the not the optimum solution, still, I have edited your answer to give you hints and help you understand the areas where you might have committed logical errors
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class DemoList extends Application {
#Override
public void start(Stage stage) throws Exception {
GridPane gridPane = new GridPane();
Label label = new Label("Name");
final TextField textField = new TextField();
textField.setFocusTraversable(false);
textField.setPromptText("Please Type Here");
final ContextMenu cm = new ContextMenu();
final ObservableList<String> employeeList = FXCollections
.observableArrayList();
employeeList.addAll("Donald Duck", "Donald Mouse", "Donald Goofy");
textField.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> arg0,
String arg1, String arg2) {
// To clear the Context Menu so that same items are not added
// multiple times
cm.getItems().clear();
for (String employee : employeeList) {
if (filterHelper(employee, arg2)) {
cm.getItems().add(new MenuItem(employee));
}
}
}
});
textField.setOnMouseClicked(new EventHandler<Event>() {
#Override
public void handle(Event arg0) {
// To clear the Context Menu so that same items are not added
// multiple times
cm.getItems().clear();
//Adding the data for initial click
for (String employee : employeeList) {
if (filterHelper(employee, textField.getText())) {
cm.getItems().add(new MenuItem(employee));
}
}
cm.show(textField, Side.BOTTOM, 0, 0);
}
});
gridPane.add(label, 0, 0);
gridPane.add(textField, 0, 1);
Scene scene = new Scene(gridPane, 300, 300);
stage.setScene(scene);
stage.show();
}
private boolean filterHelper(String employee, String query) {
//Splitting Employee name to fetch first and last name
String first = employee.split(" ")[0].toLowerCase(), last = employee
.split(" ")[1].toLowerCase();
String[] querySplit = query.replace(",", "\\s").split("\\s+");
int length = querySplit.length;
for (int i = 0; i < length; i++)
querySplit[i] = querySplit[i].toLowerCase();
/**
* Avoid adding unnecessary return statement
* I have removed all the 'return false' statements
* The last return will take care of all the 'return false'
*/
//only single word
if (length == 1) {
if (first.startsWith(querySplit[0])
|| last.startsWith(querySplit[0]))
return true;
}
//two words, considering first word is first name
//and second word is last name
else if (length == 2) {
if (first.startsWith(querySplit[0])
&& last.startsWith(querySplit[1]))
return true;
}
return false;
}
public static void main(String[] args) {
launch(args);
}
}