Javafx how to get cell from list(not value) - javafx

I want get the cell(node,not data) from list by index.I need cast it to my custom cell and call its function.
I found a lot of question ask how to get value,but i find nothing about how to get cell itself.I try to use listView.getChildrenUnmodifiable().get(index),but it's not work.
The function i want to achieve is right clicking on the cell of list,then popup a context menu which have a 'rename' command,when i clicking the commond, startEdit function of cell need to be called.
How can i do that?

You should set the context menu on the individual cells, not on the list view itself. Here is a quick example:
Controller class with list view, and methods to invoke from context menu items:
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
public class Controller {
#FXML
private ListView<String> listView ;
public void initialize() {
for (int i = 1 ; i <= 20 ; i++) {
listView.getItems().add("Item "+i);
}
listView.setCellFactory(lv -> new ListCellWithContextMenu(this));
}
public void edit(int index) {
listView.edit(index);
}
public void delete(int index) {
listView.getItems().remove(index);
}
}
The cell implementation:
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.cell.TextFieldListCell;
public class ListCellWithContextMenu extends TextFieldListCell<String> {
private final ContextMenu contextMenu ;
public ListCellWithContextMenu(Controller controller) {
contextMenu = new ContextMenu();
MenuItem delete = new MenuItem("Delete");
MenuItem edit = new MenuItem("Edit");
contextMenu.getItems().addAll(edit, delete);
edit.setOnAction(e -> controller.edit(getIndex()));
delete.setOnAction(e -> controller.delete(getIndex()));
setConverter(TextFormatter.IDENTITY_STRING_CONVERTER);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setContextMenu(null);
} else {
setContextMenu(contextMenu);
}
}
}
For completeness, an FXML file:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.ListView?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="Controller">
<left>
<ListView fx:id="listView" editable="true" />
</left>
</BorderPane>
and an application class:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class ListViewWithContextMenu extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Scene scene = new Scene(FXMLLoader.load(getClass().getResource("ListViewWithContextMenu.fxml")));
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}

James_D is right,you can register the context menus on the individual cells.
But in the situation which i have a lot of code relative to context menu,rewrite then all may cause other problems.After some try i use a another way.
For simplicity,I write two reusable class to implement the function that get cell by index from list directly.
If you think helpful,you can use the code below.
//let your cell extend this class
public class GetAbleListCell<T> extends ListCell<T>{
private ListViewHelper<T> libraryListHelper;
public GetAbleListCell(ListViewHelper<T> libraryListHelper) {
this.libraryListHelper = libraryListHelper;
}
#Override
public void updateIndex(int i) {
// TODO Auto-generated method stub
super.updateIndex(i);
if(getIndex()>0)
{
libraryListHelper.setCell(getIndex(),this);
}
}
}
//helper class
public class ListViewHelper<T> {
private ObservableList<GetAbleListCell<T>> list=FXCollections.observableArrayList();
public GetAbleListCell<T> getCell(int index) {
return list.get(index);
}
public void setCell(int index, GetAbleListCell<T> getAbleListCell) {
while(list.size()<index+1)
{
list.add(null);
}
list.set(index, getAbleListCell);
}
}
example
this.libraryListHelper=new ListViewHelper();
libraryLV.setCellFactory(param -> new LibraryCell(libraryListHelper));
.........
private void renameCurrentSelectedItems() {
//get cell directly
LibraryCell cell=(LibraryCell) libraryListHelper.getCell(libraryLV.getSelectionModel().getSelectedIndex());
cell.startEdit();
}
may contain some bug but it work in my situation

Related

Interacting with custom CellFactory node adds row to TableView selection list?

I have a TableView with a CellFactory that places a ComboBox into one of the columns. The TableView has SelectionMode.MULTIPLE enabled but it is acting odd with the ComboBox cell.
When the users clicks on the ComboBox to select a value, that row is added to the list of selected rows. Instead, clicking on the ComboBox should either select that row and deselect all others (unless CTRL is being held), or it should not select the row at all, but only allow for interaction with the ComboBox.
I am not sure how to achieve this.
Here is a complete example to demonstrate the issue:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
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.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
enum Manufacturer {
HP, DELL, LENOVO, ASUS, ACER;
}
public class TableViewSelectionIssue extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Simple TableView
TableView<ComputerPart> tableView = new TableView<>();
TableColumn<ComputerPart, Manufacturer> colManufacturer = new TableColumn<>("Manufacturer");
TableColumn<ComputerPart, String> colItem = new TableColumn<>("Item");
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
colManufacturer.setCellValueFactory(t -> t.getValue().manufacturerProperty());
colItem.setCellValueFactory(t -> t.getValue().itemNameProperty());
tableView.getColumns().addAll(colManufacturer, colItem);
// CellFactory to display ComboBox in colManufacturer
colManufacturer.setCellFactory(param -> new ManufacturerTableCell(colManufacturer, FXCollections.observableArrayList(Manufacturer.values())));
// Add sample items
tableView.getItems().addAll(
new ComputerPart("Keyboard"),
new ComputerPart("Mouse"),
new ComputerPart("Monitor"),
new ComputerPart("Motherboard"),
new ComputerPart("Hard Drive")
);
root.getChildren().add(tableView);
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
}
class ComputerPart {
private final ObjectProperty<Manufacturer> manufacturer = new SimpleObjectProperty<>();
private final StringProperty itemName = new SimpleStringProperty();
public ComputerPart(String itemName) {
this.itemName.set(itemName);
}
public Manufacturer getManufacturer() {
return manufacturer.get();
}
public void setManufacturer(Manufacturer manufacturer) {
this.manufacturer.set(manufacturer);
}
public ObjectProperty<Manufacturer> manufacturerProperty() {
return manufacturer;
}
public String getItemName() {
return itemName.get();
}
public void setItemName(String itemName) {
this.itemName.set(itemName);
}
public StringProperty itemNameProperty() {
return itemName;
}
}
class ManufacturerTableCell extends TableCell<ComputerPart, Manufacturer> {
private final ComboBox<Manufacturer> cboStatus;
ManufacturerTableCell(TableColumn<ComputerPart, Manufacturer> column, ObservableList<Manufacturer> items) {
this.cboStatus = new ComboBox<>();
this.cboStatus.setItems(items);
this.cboStatus.setConverter(new StringConverter<Manufacturer>() {
#Override
public String toString(Manufacturer object) {
return object.name();
}
#Override
public Manufacturer fromString(String string) {
return null;
}
});
this.cboStatus.disableProperty().bind(column.editableProperty().not());
this.cboStatus.setOnShowing(event -> {
final TableView<ComputerPart> tableView = getTableView();
tableView.getSelectionModel().select(getTableRow().getIndex());
tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
});
this.cboStatus.valueProperty().addListener((observable, oldValue, newValue) -> {
if (isEditing()) {
commitEdit(newValue);
column.getTableView().refresh();
}
});
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Manufacturer item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if (empty) {
setGraphic(null);
} else {
this.cboStatus.setValue(item);
this.setGraphic(this.cboStatus);
}
}
}
The example begins with a predictable UI:
However, when interacting with the ComboBox in the Manufacturer column, the corresponding row is selected. This is expected for the first row, but it does not get deselected when interacting with another ComboBox.
How can I prevent subsequent interactions with a ComboBox from adding to the selected rows? It should behave like any other click on a TableRow, should it not?
I am using JDK 8u161.
Note: I understand there is a ComboBoxTableCell class available, but I've not been able to find any examples of how to use one properly; that is irrelevant to my question, though, unless the ComboBoxTableCell behaves differently.
Since you want an "always editing" cell, your implementation should behave more like CheckBoxTableCell than ComboBoxTableCell. The former bypasses the normal editing mechanism of the TableView. As a guess, I think it's your use of the normal editing mechanism that causes the selection issues—why exactly, I'm not sure.
Modifying your ManufactureTableCell to be more like CheckBoxTableCell, it'd look something like:
class ManufacturerTableCell extends TableCell<ComputerPart, Manufacturer> {
private final ComboBox<Manufacturer> cboStatus;
private final IntFunction<Property<Manufacturer>> extractor;
private Property<Manufacturer> property;
ManufacturerTableCell(IntFunction<Property<Manufacturer>> extractor, ObservableList<Manufacturer> items) {
this.extractor = extractor;
this.cboStatus = new ComboBox<>();
this.cboStatus.setItems(items);
// removed StringConverter for brevity (accidentally)
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
cboStatus.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (event.isShortcutDown()) {
getTableView().getSelectionModel().select(getIndex(), getTableColumn());
} else {
getTableView().getSelectionModel().clearAndSelect(getIndex(), getTableColumn());
}
event.consume();
});
}
#Override
protected void updateItem(Manufacturer item, boolean empty) {
super.updateItem(item, empty);
setText(null);
clearProperty();
if (empty) {
setGraphic(null);
} else {
property = extractor.apply(getIndex());
Bindings.bindBidirectional(cboStatus.valueProperty(), property);
setGraphic(cboStatus);
}
}
private void clearProperty() {
setGraphic(null);
if (property != null) {
Bindings.unbindBidirectional(cboStatus.valueProperty(), property);
}
}
}
And you'd install it like so:
// note you could probably share the same ObservableList between all cells
colManufacturer.setCellFactory(param ->
new ManufacturerTableCell(i -> tableView.getItems().get(i).manufacturerProperty(),
FXCollections.observableArrayList(Manufacturer.values())));
As already mentioned, the above implementation bypasses the normal editing mechanism; it ties the value of the ComboBox directly to the model item's property. The implementation also adds a MOUSE_PRESSED handler to the ComboBox that selects the row (or cell if using cell selection) as appropriate. Unfortunately, I'm not quite understanding how to implement selection when Shift is down so only "Press" and "Shortcut+Press" is handled.
The above works how I believe you want it to, but I could only test it out using JavaFX 12.

javafx checkBoxTableCell callback

I would like to ask what is the most elegant way to capture value change of CheckBoxTableCell in my TableView.
My goal is to save new value in DB which my example shows:
printedColumn.setCellValueFactory(f -> f.getValue().getPrintedProperty());
printedColumn.setCellFactory(CheckBoxTableCell.forTableColumn(new Callback<Integer, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(Integer param) {
ProductFx productFx = addProductModel.getProductFxObservableList().get(param);
updateInDatabase(productFx);
return productFx.getPrintedProperty();
}
}));
This works fine, but I don't feel like it's the best way to achieve that. For other columns I follow this way:
#FXML
public void onEditPrice(TableColumn.CellEditEvent<ProductFx, Number> e) {
ProductFx productFx = e.getRowValue();
productFx.setPrice(e.getNewValue().doubleValue());
updateInDatabase(productFx);
}
fxml:
<TableColumn fx:id="priceColumn" onEditCommit="#onEditPrice" prefWidth="75.0" text="%addProductTable.price" />
Is it possible to do it in similar way with #FXML annotated method and fxml configuration? Maybe some other ideas?
I really find it hard to get the idea behind your question. I publish the code how I think it should be done. If it does not meet your requirements please elaborate on what you are exactly trying to achive.
import javafx.application.Application;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
public class TableViewApp extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
ObservableList<Product> products = FXCollections.observableArrayList(product -> new ObservableValue[] {product.nameProperty()});
products.add(new Product(1l, "Machine1"));
products.add(new Product(2l, "Machine2"));
products.addListener((ListChangeListener<Product>) change -> {
while (change.next()) {
if (change.wasUpdated()) {
for (int i = change.getFrom(); i < change.getTo(); i++) {
updateInDb(change.getList().get(i));
}
}
}
});
TableView<Product> tableView = new TableView<>(products);
tableView.setEditable(true);
TableColumn<Product, Long> idColumn = new TableColumn<>("Id");
idColumn.setCellValueFactory(cellData -> cellData.getValue().idProperty().asObject());
TableColumn<Product, String> nameColumn = new TableColumn<>("Name");
nameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
nameColumn.setEditable(true);
tableView.getColumns().add(idColumn);
tableView.getColumns().add(nameColumn);
stage.setScene(new Scene(tableView));
stage.show();
}
private void updateInDb(Product product) {
System.out.println("Update " + product + " in db");
}
}
class Product {
private LongProperty id = new SimpleLongProperty();
private StringProperty name = new SimpleStringProperty();
public Product(long id, String name) {
this.id.set(id);
this.name.set(name);
}
public LongProperty idProperty() {
return id;
}
public long getId() {
return id.get();
}
public StringProperty nameProperty() {
return name;
}
public String getName() {
return name.get();
}
#Override
public String toString() {
return "Product[id=" + getId() + ", name=" + getName() + "]";
}
}
I do not understand in detail how you got your code working. I guess it is not working as intended by the design of the API. I can definitely answer if it is possible to do it with a simple FXML attribute of CheckBoxTableCell: No.
In case of CheckBoxTableCell
... it is not necessary that the cell enter its editing state (...). A side-effect of this is that the usual editing callbacks (such as on edit commit) will not be called. If you want to be notified of changes,
it is recommended to directly observe the boolean properties that are
manipulated by the CheckBox.
as stated by the javadoc.
If the class of printedProperty implements ObservableValue<Boolean>(as stated by your code) you should follow the cited doc and add a ChangeListener to it like
printedColumn.setCellValueFactory(f -> f.getValue().getPrintedProperty());
printedColumn.setCellFactory(CheckBoxTableCell.forTableColumn(new Callback<Integer, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(Integer param) {
ProductFx productFx = addProductModel.getProductFxObservableList().get(param);
return productFx.getPrintedProperty();
}
}));
ObservableList<ProductFx> obs = addProductModel.getProductFxObservableList();
obs.addListener(new ListChangeListener<ProductFx>(){
#Override
public void onChanged(Change<? extends ProductFx> c) {
if(c.wasAdded()) {
for (ProductFx s:c.getAddedSubList()) {
s.getPrintedProperty().addListener(new ChangeListener<Boolean>() {
ProductFx localProductFx=s;
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
updateInDatabase(localProductFx);
}
});
}
}
}
});
But this is not elegant at all.
To your approach to solve the problem:
The Callback<Integer, ObservableValue<Boolean>>() you used is called each time when the displayed cell is updated. This happens especially when you are scrolling through a huge list, because TableView only keeps as many Cell instances as necessary to fill its view-port (doc). They are simply updated during scrolling and your code updates the database each time this happens, so you might run into performance problems for large datasets or slow databases.
PS: As far as I understand your code you do not follow the usual naming conventions for properties. This might lead to problems using reflecting classes like PropertyValueFactory.

How to open fxml in Scene Builder that has custom elements? [duplicate]

I have the need to have a selection listener and select method on a pane to be able to monitor and present a highlight when a node is clicked on.
I did the following:
public class PaneWithSelectionListener extends Pane {
private ObjectProperty<Annotation> selectedAnnotation = new SimpleObjectProperty<>();
public PaneWithSelectionListener() {
super();
selectedAnnotation.addListener((obs, oldAnno, newAnno) -> {
if (oldAnno != null) {
oldAnno.setStyle("");
}
if (newAnno != null) {
newAnno.setStyle("-fx-border-color: blue;-fx-border-insets: 5;-fx-border-width: 1;-fx-border-style: dashed;");
}
});
setOnMouseClicked(e->selectAnnotation(null));
}
public void selectAnnotation(Annotation ann){
selectedAnnotation.set(ann);
}
}
And this works great - however I am not able to work with SceneBuilder anymore since my FXML references this PaneWithSelectionListener rather than Pane. I am not sure how to get my custom pane into SceneBuilder. I have looked at other questions and they are all a combination of FXML and Controllers - where this is just a Pane.
Does anyone know of a way to do this, or perhaps swap the Pane for a PaneWithSelectionListener at initialization time?
Thanks
If the issue is just to make your custom class available in SceneBuilder, you can do so with the following steps:
Bundle your custom class (and any supporting classes, such as Annotation) as a jar file
In SceneBuilder, activate the drop-down button next to "Library" in the top of the left pane:
Choose "Import JAR/FXML File..."
Select the Jar file created from step 1
Make sure the class you need access to in SceneBuilder (PaneWithSelectionListener) is checked
Press "Import Component"
PaneWithSelectionListener will now appear in SceneBuilder under "Custom" in the left pane:
You'll notice the drop-down in SceneBuilder has a "Custom Library Folder" option, from which you can open the folder where the jar files are stored. For a quick option, you can just copy jar files to this folder and (after a short delay), the contained classes will appear in the "Custom" list.
I created a CustomCB a combo box which is of type a userObject. In my case I have used <APerson> as userObject. Entire project and the TESTER are here. First one is CustomCB, the components and the JAR file generated out of this code is added to the Library. This can be loaded in SCENE BUILDER. Thereafter it is available for scene design.
Then of course, you have a tester CustomCB2, which also can use a FXML instead of the way I have done.
I actually want the items starting with the text I type in the ComboBox to appear in the list. But I don't know how to do it. Because the FIRST NAME or LAST NAME of the PERSON class can start with 'Pe'. If I find a solution, I will post it here.
This is the Custom Component.
package customCB;
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
*
* #author Hornigold Arthur
*/
public class APerson {
private final StringProperty firstName;
private final StringProperty lastName;
private final IntegerProperty familyID;
private final IntegerProperty personID;
public APerson() {
this(null, null, 0,0);
}
/**
* Constructor with some initial data.
*
* #param familyID
* #param familyName
*/
public APerson (String firstName, String lastName, int familyID, int personID) {
this.firstName = new SimpleStringProperty(firstName);
this.lastName = new SimpleStringProperty(lastName);
this.familyID = new SimpleIntegerProperty(familyID);
this.personID = new SimpleIntegerProperty(personID);
}
public int getFamilyID() {
return familyID.get();
}
public void setFamilyID(int FamilyID) {
this.familyID.set(FamilyID);
}
public IntegerProperty familyIDProperty() {
return familyID;
}
public int getPersonID() {
return personID.get();
}
public void setPersonID(int PersonID) {
this.personID.set(PersonID);
}
public IntegerProperty personIDProperty() {
return personID;
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String FirstName) {
this.firstName.set(FirstName);
}
public StringProperty firstNameProperty() {
return firstName;
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String LastName) {
this.lastName.set(LastName);
}
public StringProperty lastNameProperty() {
return lastName;
}
public String toString() {
String name = getFirstName() + " " + getLastName()+ " [" + getFamilyID() +"]";
return name;
}
}
This is the FXML for the Custom Component.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.layout.VBox?>
<fx:root stylesheets="#application.css" type="javafx.scene.layout.VBox" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1">
<ComboBox fx:id="myCustomCombo" editable="true" onAction="#cbOnAction" prefWidth="300.0" style="-fx-background-color: white;" />
</fx:root>
This is the controller for this FXML but do not mention it in FXML file. It throws error.
package customCB;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import org.controlsfx.control.textfield.TextFields;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
public class CustomComboController extends VBox{
#FXML
private ResourceBundle resources;
#FXML
private URL location;
#FXML
private ComboBox<APerson> myCustomCombo;
#FXML
void cbOnAction(ActionEvent event) {
}
#FXML
void initialize() {
assert myCustomCombo != null : "fx:id=\"myCustomCombo\" was not injected: check your FXML file 'CustomLvFXML.fxml'.";
}
public CustomComboController() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("customCombo.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
public void setCBValues(javafx.collections.ObservableList<APerson> values) {
myCustomCombo.setItems(values);
myCustomCombo.setEditable(true);
TextFields.bindAutoCompletion(myCustomCombo.getEditor(), myCustomCombo.getItems());
}
}
Download controlsfx-8.40.12.jar from WEB and include this in the BUILD PATH as library.
Now create a jar file for this project. "CustomCB.jar".
This JAR file has to be included as custom control in Scene Builder. I use version 10.0.
Now that it is part of Scene builder you can use this component in designing, unless you can do it the way I have done in my TEST CODE. You need to include "CustomCB.jar" as part of the library for building.
This is the code for the tester.
package customCB2;
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
public class Main extends Application {
static private javafx.collections.ObservableList<APerson> fathers = javafx.collections.FXCollections.observableArrayList();
static private javafx.collections.ObservableList<APerson> mothers = javafx.collections.FXCollections.observableArrayList();
#Override
public void start(Stage stage) throws Exception {
CustomComboController customControl2 = new CustomComboController();
CustomComboController customControl3 = new CustomComboController();
loadFathers();
loadMothers();
customControl2.setCBValues(fathers);
customControl3.setCBValues(mothers);
VBox root = new VBox();
root.getChildren().addAll(customControl2, customControl3);
stage.setScene(new Scene(root));
stage.setTitle("Custom Control Combo box");
stage.setWidth(300);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
private void loadFathers() {
fathers.clear();
fathers.add(new APerson("Hornigold","Arthur",1,63));
fathers.add(new APerson("Andrews","Sundareson",2,60));
fathers.add(new APerson("Christopher","Easweradoss",3,57));
fathers.add(new APerson("Arthur","Kennedy",4,55));
}
private void loadMothers() {
mothers.clear();
mothers.add(new APerson("Victoria","Arthur",1,95));
mothers.add(new APerson("Eliza", "Daniel",1,60));
mothers.add(new APerson("Nesammal", "Rivington",2,57));
mothers.add(new APerson("Ratnammal","Andews",1,55));
}
}
It needs lot of improvements. If anyone can improvise, please add it here.

JavaFX : TableView ToogleButton Column

My TableView has a column with a ToggleButton. All the buttons belong to one group, you can only select one button (one row).
But my TableView has a lot of rows and the ToggleGroup seems to work.
That is until I scroll drown.
When I select one ToggleButton and scroll down no other button should be selected but there is always one that is selected per view.
Is this fixable?
Edit: Here is a SSCCE :
MainApp.java :
package p1;
import java.io.IOException;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class MainApp extends Application {
private Stage primaryStage;
private AnchorPane rootLayout;
private ObservableList<Person> personData = FXCollections.observableArrayList();
public MainApp(){
for(int i=0;i<40;i++){
personData.add(new Person("person " +i));
}
}
public ObservableList<Person> getPersonData(){
return personData;
}
#Override
public void start(Stage primaryStage) throws Exception {
this.primaryStage = primaryStage;
try{
FXMLLoader loader =new FXMLLoader();
loader.setLocation(MainApp.class.getResource("People.fxml"));
rootLayout = (AnchorPane)loader.load();
Scene scene = new Scene(rootLayout);
primaryStage.setScene(scene);
primaryStage.show();
PeopleController controller = loader.getController();
controller.setMainApp(this);
} catch(IOException e){
e.printStackTrace();
}
}
public Stage getPrimaryStage(){
return primaryStage;
}
public static void main(String[] args){
launch(args);
}}
People.fxml :
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="p1.PeopleController">
<children>
<TableView fx:id="personTable" layoutX="160.0" layoutY="49.0" prefHeight="351.0" prefWidth="600.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="49.0">
<columns>
<TableColumn fx:id="nameColumn" prefWidth="75.0" text="Name" />
<TableColumn fx:id="previewColumn" prefWidth="75.0" text="Preview"/>
</columns>
</TableView>
</children>
</AnchorPane>
PeopleController.java :
package p1;
import com.sun.prism.impl.Disposer;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.util.Callback;
public class PeopleController{
#FXML private TableView<Person> personTable;
#FXML private TableColumn<Person, String> nameColumn;
#FXML private TableColumn previewColumn;
private MainApp mainApp;
final ToggleGroup group = new ToggleGroup();
#FXML
public void initialize() {
nameColumn.setCellValueFactory(cellData -> cellData.getValue().NameProperty());
previewColumn.setCellFactory(
new Callback<TableColumn<Disposer.Record, Boolean>, TableCell<Disposer.Record, Boolean>>() {
#Override
public TableCell<Disposer.Record, Boolean> call(TableColumn<Disposer.Record, Boolean> p) {
ButtonCell cell = new ButtonCell(group);
cell.setAlignment(Pos.CENTER);
return cell;
}
});
}
public void setMainApp(MainApp mainApp){
this.mainApp = mainApp;
personTable.setItems(mainApp.getPersonData());
}
public class ButtonCell extends TableCell<Disposer.Record, Boolean> {
final ToggleButton cellButton = new ToggleButton("click");
public ButtonCell(ToggleGroup group){
cellButton.setToggleGroup(group);
}
#Override
protected void updateItem(Boolean t, boolean empty) {
super.updateItem(t, empty);
if(!empty){
setGraphic(cellButton);
}
}}}
Person.java :
package p1;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Person {
private final StringProperty name;
public Person(){
this(null);
}
public Person(String name){
this.name = new SimpleStringProperty(name);
}
public String getName(){
return name.get();
}
public void setName(String name){
this.name.set(name);
}
public StringProperty NameProperty(){
return name;
} }
The reason that the toggle seems to "jump" when scrolling is the re-use of the cells: the selected state sticks to the button, not the item. Consequently, you can't use a toggleGroup (except in the not-so-common and not recommended case that the items in your data implement Toggle) to keep the toggle state. You need to implement the toggle-logic yourself.
One option is a custom SingleSelectionModel and a custom ButtonCell that talks to the model (as do all other collaborators). Unfortunately, FX doesn't have a publicly accessible concrete implementation of the model. As often, the heavy lifting - which in this case is to update itself on modifications to the items - is left to client code (and not done in this example as well ;-)
Something like:
public class ToggleButtonTableExample extends Application {
public static class DataSelectionModel<S> extends SingleSelectionModel<S> {
private ListProperty<S> listProperty;
public DataSelectionModel(Property<ObservableList<S>> items) {
//listProperty = BugPropertyAdapters.listProperty(items);
listProperty = new SimpleListProperty<>();
listProperty.bindBidirectional(items);
ListChangeListener<S> itemsContentObserver = c -> {
itemsChanged(c);
};
listProperty.addListener(itemsContentObserver);
}
protected void itemsChanged(Change<? extends S> c) {
// TODO need to implement update on modificatins to the underlying list
}
#Override
protected S getModelItem(int index) {
if (index < 0 || index >= getItemCount()) return null;
return listProperty.get(index);
}
#Override
protected int getItemCount() {
return listProperty.getSize();
}
}
public static class ButtonCellX<S, T> extends TableCell<S, T> {
private ToggleButton cellButton;
private SingleSelectionModel<S> model;
public ButtonCellX(SingleSelectionModel<S> group) {
this.model = group;
cellButton = new ToggleButton("click");
cellButton.setOnAction(e -> updateToggle());
updateToggle();
setAlignment(Pos.CENTER);
}
protected void updateToggle() {
model.select(cellButton.isSelected()? getIndex() : -1);
}
#Override
protected void updateItem(T t, boolean empty) {
super.updateItem(t, empty);
if (empty) {
setGraphic(null);
} else {
cellButton.setSelected(model.isSelected(getIndex()));
setGraphic(cellButton);
}
}
}
private Parent getContent() {
TableView<Person> table = new TableView<>();
table.setItems(Person.persons());
TableColumn<Person, String> name = new TableColumn<>("Name");
name.setCellValueFactory(new PropertyValueFactory<>("lastName"));
SingleSelectionModel<Person> model = new DataSelectionModel<>(table.itemsProperty());
TableColumn<Person, Boolean> toggle = new TableColumn<>("Preview");
toggle.setCellFactory(c -> new ButtonCellX<Person, Boolean>(model));
toggle.setCellValueFactory(f -> {
Object value = f.getValue();
return Bindings.equal(value, model.selectedItemProperty());
});
table.getColumns().addAll(name, toggle);
Button select = new Button("Select 0");
select.setOnAction(e -> {
model.select(0);
});
VBox content = new VBox(10, table, select);
return content;
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
//primaryStage.setTitle(FXUtils.version());
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(ChoiceBoxTableCellDynamic.class.getName());
}

Adding a custom component to SceneBuilder 2.0

I have the need to have a selection listener and select method on a pane to be able to monitor and present a highlight when a node is clicked on.
I did the following:
public class PaneWithSelectionListener extends Pane {
private ObjectProperty<Annotation> selectedAnnotation = new SimpleObjectProperty<>();
public PaneWithSelectionListener() {
super();
selectedAnnotation.addListener((obs, oldAnno, newAnno) -> {
if (oldAnno != null) {
oldAnno.setStyle("");
}
if (newAnno != null) {
newAnno.setStyle("-fx-border-color: blue;-fx-border-insets: 5;-fx-border-width: 1;-fx-border-style: dashed;");
}
});
setOnMouseClicked(e->selectAnnotation(null));
}
public void selectAnnotation(Annotation ann){
selectedAnnotation.set(ann);
}
}
And this works great - however I am not able to work with SceneBuilder anymore since my FXML references this PaneWithSelectionListener rather than Pane. I am not sure how to get my custom pane into SceneBuilder. I have looked at other questions and they are all a combination of FXML and Controllers - where this is just a Pane.
Does anyone know of a way to do this, or perhaps swap the Pane for a PaneWithSelectionListener at initialization time?
Thanks
If the issue is just to make your custom class available in SceneBuilder, you can do so with the following steps:
Bundle your custom class (and any supporting classes, such as Annotation) as a jar file
In SceneBuilder, activate the drop-down button next to "Library" in the top of the left pane:
Choose "Import JAR/FXML File..."
Select the Jar file created from step 1
Make sure the class you need access to in SceneBuilder (PaneWithSelectionListener) is checked
Press "Import Component"
PaneWithSelectionListener will now appear in SceneBuilder under "Custom" in the left pane:
You'll notice the drop-down in SceneBuilder has a "Custom Library Folder" option, from which you can open the folder where the jar files are stored. For a quick option, you can just copy jar files to this folder and (after a short delay), the contained classes will appear in the "Custom" list.
I created a CustomCB a combo box which is of type a userObject. In my case I have used <APerson> as userObject. Entire project and the TESTER are here. First one is CustomCB, the components and the JAR file generated out of this code is added to the Library. This can be loaded in SCENE BUILDER. Thereafter it is available for scene design.
Then of course, you have a tester CustomCB2, which also can use a FXML instead of the way I have done.
I actually want the items starting with the text I type in the ComboBox to appear in the list. But I don't know how to do it. Because the FIRST NAME or LAST NAME of the PERSON class can start with 'Pe'. If I find a solution, I will post it here.
This is the Custom Component.
package customCB;
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
*
* #author Hornigold Arthur
*/
public class APerson {
private final StringProperty firstName;
private final StringProperty lastName;
private final IntegerProperty familyID;
private final IntegerProperty personID;
public APerson() {
this(null, null, 0,0);
}
/**
* Constructor with some initial data.
*
* #param familyID
* #param familyName
*/
public APerson (String firstName, String lastName, int familyID, int personID) {
this.firstName = new SimpleStringProperty(firstName);
this.lastName = new SimpleStringProperty(lastName);
this.familyID = new SimpleIntegerProperty(familyID);
this.personID = new SimpleIntegerProperty(personID);
}
public int getFamilyID() {
return familyID.get();
}
public void setFamilyID(int FamilyID) {
this.familyID.set(FamilyID);
}
public IntegerProperty familyIDProperty() {
return familyID;
}
public int getPersonID() {
return personID.get();
}
public void setPersonID(int PersonID) {
this.personID.set(PersonID);
}
public IntegerProperty personIDProperty() {
return personID;
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String FirstName) {
this.firstName.set(FirstName);
}
public StringProperty firstNameProperty() {
return firstName;
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String LastName) {
this.lastName.set(LastName);
}
public StringProperty lastNameProperty() {
return lastName;
}
public String toString() {
String name = getFirstName() + " " + getLastName()+ " [" + getFamilyID() +"]";
return name;
}
}
This is the FXML for the Custom Component.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.layout.VBox?>
<fx:root stylesheets="#application.css" type="javafx.scene.layout.VBox" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1">
<ComboBox fx:id="myCustomCombo" editable="true" onAction="#cbOnAction" prefWidth="300.0" style="-fx-background-color: white;" />
</fx:root>
This is the controller for this FXML but do not mention it in FXML file. It throws error.
package customCB;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import org.controlsfx.control.textfield.TextFields;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
public class CustomComboController extends VBox{
#FXML
private ResourceBundle resources;
#FXML
private URL location;
#FXML
private ComboBox<APerson> myCustomCombo;
#FXML
void cbOnAction(ActionEvent event) {
}
#FXML
void initialize() {
assert myCustomCombo != null : "fx:id=\"myCustomCombo\" was not injected: check your FXML file 'CustomLvFXML.fxml'.";
}
public CustomComboController() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("customCombo.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
public void setCBValues(javafx.collections.ObservableList<APerson> values) {
myCustomCombo.setItems(values);
myCustomCombo.setEditable(true);
TextFields.bindAutoCompletion(myCustomCombo.getEditor(), myCustomCombo.getItems());
}
}
Download controlsfx-8.40.12.jar from WEB and include this in the BUILD PATH as library.
Now create a jar file for this project. "CustomCB.jar".
This JAR file has to be included as custom control in Scene Builder. I use version 10.0.
Now that it is part of Scene builder you can use this component in designing, unless you can do it the way I have done in my TEST CODE. You need to include "CustomCB.jar" as part of the library for building.
This is the code for the tester.
package customCB2;
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
public class Main extends Application {
static private javafx.collections.ObservableList<APerson> fathers = javafx.collections.FXCollections.observableArrayList();
static private javafx.collections.ObservableList<APerson> mothers = javafx.collections.FXCollections.observableArrayList();
#Override
public void start(Stage stage) throws Exception {
CustomComboController customControl2 = new CustomComboController();
CustomComboController customControl3 = new CustomComboController();
loadFathers();
loadMothers();
customControl2.setCBValues(fathers);
customControl3.setCBValues(mothers);
VBox root = new VBox();
root.getChildren().addAll(customControl2, customControl3);
stage.setScene(new Scene(root));
stage.setTitle("Custom Control Combo box");
stage.setWidth(300);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
private void loadFathers() {
fathers.clear();
fathers.add(new APerson("Hornigold","Arthur",1,63));
fathers.add(new APerson("Andrews","Sundareson",2,60));
fathers.add(new APerson("Christopher","Easweradoss",3,57));
fathers.add(new APerson("Arthur","Kennedy",4,55));
}
private void loadMothers() {
mothers.clear();
mothers.add(new APerson("Victoria","Arthur",1,95));
mothers.add(new APerson("Eliza", "Daniel",1,60));
mothers.add(new APerson("Nesammal", "Rivington",2,57));
mothers.add(new APerson("Ratnammal","Andews",1,55));
}
}
It needs lot of improvements. If anyone can improvise, please add it here.

Resources