I would like to create a ComboBox with the remove button like the picture below:
The picture uses Java Swing, and I don't know how to do this with JavaFX. I would like to create two ComboBoxes (a,b). When I click the "cross" in ComboBox a, I would like to remove a's item and add this item to ComboBox b, and ComboBox b so on.
ComboBox a:
(1)click item then remove it from a and add on b
ComboBox b:
(1)click item then do something(ex:print item)
(2)click cross then remove it from b and add on a
package UnitTest;
import Peer.Peer_Manager;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.chart.XYChart;
import javafx.geometry.Insets;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
public class temp extends Application {
final int height = 200;
final int weight = 300;
final int offset = 5;
Peer_Manager p_management;
XYChart.Series series_hop;
XYChart.Series series_gd;
#Override
public void start(Stage primaryStage) {
VBox vbox = new VBox();
vbox.setPadding(new Insets(5, 5, 5, 5));
vbox.setStyle("-fx-background-color: CORNSILK;");
Scene scene = new Scene(vbox, weight, height);
primaryStage.setScene(scene);
HBox hbBtn = new HBox();
Text t1=new Text(" A:");
Text t2=new Text(" B:");
String[] filename = {"A","B","C"};//conf.load_all();
ComboBox<String> cb = new ComboBox<String>();
cb.setItems(FXCollections.observableArrayList(filename));
cb.setVisibleRowCount(10);
ComboBox<String> cb2 = new ComboBox<String>();
cb.setVisibleRowCount(10);
vbox.getChildren().add(hbBtn);
hbBtn.getChildren().add(t1);
hbBtn.getChildren().add(cb);
hbBtn.getChildren().add(t2);
hbBtn.getChildren().add(cb2);
cb.setOnAction(e -> {
try {
Object object = cb.getValue();
if (object != null) {
cb2.getItems().add(object);
cb.getSelectionModel().clearSelection();
cb.getItems().remove(object);
}
} catch (Exception e1) {
e1.printStackTrace();
}
});
//would like to do something(ex:print item),but don't remove
//add the "cross" beside items,click "cross" to remove item and add on cb
cb2.setOnAction(e -> {
try {
Object object = cb2.getValue();
System.out.println(object);
if (object != null) {
cb1.getItems().add(object);
cb2.getSelectionModel().clearSelection();
cb2.getItems().remove(object);
}
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
});
primaryStage.setTitle("SimulatorFX");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The correct way is to use a CellFactory and create graphic nodes that contain the elements you wish to have. Here is an example:
public void start(Stage primaryStage) throws Exception {
ComboBox<String> cba = new ComboBox<>();
ComboBox<String> cbb = new ComboBox<>();
cba.getItems().addAll("A", "B", "C");
cbb.getItems().addAll("123", "456", "789");
// Set a cell factory for ComboBox A. A similar thing should be done for B.
cba.setCellFactory(lv ->
new ListCell<String>() {
// This is the node that will display the text and the cross.
// I chose a hyperlink, but you can change to button, image, etc.
private HBox graphic;
// this is the constructor for the anonymous class.
{
Label label = new Label();
// Bind the label text to the item property. If your ComboBox items are not Strings you should use a converter.
label.textProperty().bind(itemProperty());
// Set max width to infinity so the cross is all the way to the right.
label.setMaxWidth(Double.POSITIVE_INFINITY);
// We have to modify the hiding behavior of the ComboBox to allow clicking on the hyperlink,
// so we need to hide the ComboBox when the label is clicked (item selected).
label.setOnMouseClicked(event -> cba.hide());
Hyperlink cross = new Hyperlink("X");
cross.setVisited(true); // So it is black, and not blue.
cross.setOnAction(event ->
{
// Since the ListView reuses cells, we need to get the item first, before making changes.
String item = getItem();
System.out.println("Clicked cross on " + item);
if (isSelected()) {
// Not entirely sure if this is needed.
cba.getSelectionModel().select(null);
}
// Remove the item from A and add to B. You can add any additional logic in here.
cba.getItems().remove(item);
cbb.getItems().add(item);
}
);
// Arrange controls in a HBox, and set display to graphic only (the text is included in the graphic in this implementation).
graphic = new HBox(label, cross);
graphic.setHgrow(label, Priority.ALWAYS);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(graphic);
}
}
});
// We have to set a custom skin, otherwise the ComboBox disappears before the click on the Hyperlink is registered.
cba.setSkin(new ComboBoxListViewSkin<String>(cba) {
#Override
protected boolean isHideOnClickEnabled() {
return false;
}
});
VBox vb = new VBox(cba, cbb);
primaryStage.setScene(new Scene(vb));
primaryStage.show();
}
Related
My question is how to generate buttons, set with car pictures, based on the checkboxes and/or radio buttons selected by a user in javafx?
I'm simulating a car dealership website with car pictures. The user should be able to filter the pictures displayed by clicking checkboxes and/or radio buttons selection.
I'm first creating all the picture buttons with a for each loop. I could use if and if/else statements to filter through the pictures but there would be duplicates. I've heard of observablelist but I haven't learned those yet.
Can someone help me out with this one please? Thank you!
ArrayList<Car> cars;
for (Car r : cars)
{
for (int i = 0; i < SIZE; i++)
{
// create buttons and set car pictures
carButton[i] = new Button();
carButton[i].setId(String.format("%d", i));
carButton[i].setGraphic(cars.get(i).getCarPicture());
}
}
Instead of using an ArrayList for your cars, I recommend using an ObservableList:
ObservableList<Car> carsList = FXCollections.observableArrayList<>();
An ObservableList allows you to listen for changes and respond accordingly. For example, when a new Car is added to the list, you could trigger an event that automatically adds a new Button to your scene.
Here is a short demo application that shows how this would work. I did comment the code below as well and many of the concepts being used may be beyond your level, but it's one method, at least.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.ImageView;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
VBox root = new VBox(5);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
// Create an ObservableList to hold our Cars
ObservableList<Car> carsList = FXCollections.observableArrayList();
// For our sample, let's use a FlowPane to display all of our buttons. We will add new buttons to this FlowPane
// automatically as new Cars are added to carsList
FlowPane flowPane = new FlowPane();
flowPane.setHgap(10);
flowPane.setVgap(5);
flowPane.setAlignment(Pos.TOP_CENTER);
// Create a ListChangeListener for our carsList. This allows us to perform some actions whenever an item is added
// to or removed from the list. For our example, we will only do something when a new Car is added.
carsList.addListener(new ListChangeListener<Car>() {
#Override
public void onChanged(Change<? extends Car> c) {
System.out.println(carsList.size());
// Get the first change
c.next();
// If an item was added to the list...
if (c.wasAdded()) {
// Create a new button and add it to the FlowPane
// The Change (c) provides access to a List of items that were added during this change. Since we
// are only going to add one Car at a time, we only need to get the first item from the AddedSubList
Button button = new Button(c.getAddedSubList().get(0).getName());
button.setGraphic(c.getAddedSubList().get(0).getIcon());
button.setOnAction(event -> {
// The code to be executed when this button is clicked goes here
});
// Add the button to our FlowPane
flowPane.getChildren().add(button);
}
}
});
// Now we need a Button that will add a new car to the List
Button button = new Button("Add Car");
button.setOnAction(event -> {
// We'll just add a random car to the carsList
carsList.add(new Car("Car #" + (carsList.size() + 1), new ImageView("icon.png")));
});
// Add our FlowPane and Button to the root layout
root.getChildren().addAll(button, flowPane);
primaryStage.setScene(new Scene(root, 550, 250));
primaryStage.show();
}
}
class Car {
private final String name;
private final ImageView icon;
public Car(String name, ImageView icon) {
this.name = name;
this.icon = icon;
}
public String getName() {
return name;
}
public ImageView getIcon() {
return icon;
}
}
The Results: (after clicking the "Add Car" button a few times)
This is a terrible implementation but It will give you some ideas on how to do things. You need to research FilteredList, ListView, and Predicate. This implementation does not handle more than one CheckBox at a time. It will only display the last CheckBox action.
CarList
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
*
* #author Sedrick
*/
public class CarList extends Application {
#Override
public void start(Stage primaryStage) {
List<Car> cars = new ArrayList();
cars.add(new Car("Honda", "2004"));
cars.add(new Car("Ford", "2005"));
cars.add(new Car("Ford", "2004"));
cars.add(new Car("Honda", "2005"));
cars.add(new Car("Toyota", "2004"));
cars.add(new Car("Cadillac", "2005"));
ListView<Car> view = new ListView();
view.setCellFactory((ListView<Car> param) -> {
ListCell<Car> cell = new ListCell<Car>() {
CarView carView = new CarView();
#Override
protected void updateItem(Car item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setText("");
carView.setMake(item.getMake());
carView.setModel(item.getModel());
carView.setImageView(item.getUrl());
setGraphic(carView);
} else {
setText("");
setGraphic(null);
}
}
};
return cell;
});
ObservableList<Car> data = FXCollections.observableArrayList(cars);
FilteredList<Car> filteredList = new FilteredList(data);
view.setItems(filteredList);
HBox.setHgrow(view, Priority.ALWAYS);
CheckBox checkBox = new CheckBox("Honda");
checkBox.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue)
{
filteredList.setPredicate((item) -> {
return item.getMake().equals("Honda");
});
}
else{
filteredList.setPredicate((item) -> {
return true;
});
}
});
CheckBox checkBox2 = new CheckBox("Ford");
checkBox2.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue)
{
filteredList.setPredicate((item) -> {
return item.getMake().equals("Ford");
});
}
else{
filteredList.setPredicate((item) -> {
return true;
});
}
});
CheckBox checkBox3 = new CheckBox("2004");
checkBox3.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue)
{
filteredList.setPredicate((item) -> {
return item.getModel().equals("2004");
});
}
else{
filteredList.setPredicate((item) -> {
return true;
});
}
});
CheckBox checkBox4 = new CheckBox("2005");
checkBox4.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue)
{
filteredList.setPredicate((item) -> {
return item.getModel().equals("2005");
});
}
else{
filteredList.setPredicate((item) -> {
return true;
});
}
});
VBox leftPanel = new VBox(checkBox, checkBox2, checkBox3, checkBox4);
HBox root = new HBox(leftPanel, view);
Scene scene = new Scene(root, 625, 500);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
CarView
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
/**
*
* #author Sedrick
*/
final public class CarView extends HBox{
Label make = new Label();
Label model = new Label();
ImageView imageView = new ImageView();
public CarView(String make, String model, String url) {
this.make.setText(make);
this.model.setText(model);
HBox row1 = new HBox(new Label("Make: "), this.make);
HBox row2 = new HBox(new Label("Model: "), this.model);
VBox vbox = new VBox(row1, row2);
vbox.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
StackPane stackPane1 = new StackPane(vbox);
HBox.setHgrow(stackPane1, Priority.ALWAYS);
Image image = new Image(url);
this.imageView.setImage(image);
this.imageView.setFitHeight(100);
this.imageView.setFitWidth(200);
StackPane stackPane2 = new StackPane(this.imageView);
stackPane2.setStyle("-fx-background-color: yellow");
getChildren().addAll(stackPane1, stackPane2);
setPrefSize(500, 125);
}
public CarView()
{
HBox row1 = new HBox(new Label("Make: "), this.make);
HBox row2 = new HBox(new Label("Model: "), this.model);
VBox vbox = new VBox(row1, row2);
vbox.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
StackPane stackPane1 = new StackPane(vbox);
HBox.setHgrow(stackPane1, Priority.ALWAYS);
this.imageView.setFitHeight(100);
this.imageView.setFitWidth(200);
StackPane stackPane2 = new StackPane(this.imageView);
stackPane2.setStyle("-fx-background-color: yellow");
getChildren().addAll(stackPane1, stackPane2);
setPrefSize(500, 125);
}
public void setImageView(String url) {
Image image = new Image(url);
this.imageView.setImage(image);
}
public void setMake(String make) {
this.make.setText(make);
}
public void setModel(String model)
{
this.model.setText(model);
}
}
Car
/**
*
* #author Sedrick
*/
public class Car {
private String make;
private String model;
private String url = "https://cdn.pixabay.com/photo/2012/05/29/00/43/car-49278_960_720.jpg";
public Car(String make, String model) {
this.make = make;
this.model = model;
}
public String getMake() {
return make;
}
public String getModel() {
return model;
}
public String getUrl()
{
return url;
}
public void setMake(String make) {
this.make = make;
}
public void setModel(String model) {
this.model = model;
}
}
I am using PopOver from ControlsFX, in a TableView If I trigger the startEdit of a cell, it should pop the PopOver. This part it works, the problem is, the arrow which is pointing to the row is not on the right place every time. If I select a row from the table which is at the bottom of the table , it points to a cell above it.
I need that arrow to point every time to the right cell in the TableView.
ControlsFX , version: 8.40.14
How can I solve this?
Here is the code where you can see how it works:
package stackoverflow.popover;
import com.sun.deploy.util.StringUtils;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
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.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.controlsfx.control.PopOver;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
public class Controller implements Initializable {
#FXML
private TableView<Model> table;
#FXML
private TableColumn<Model, ObservableList<String>> listCell;
#Override
public void initialize(URL location, ResourceBundle resources) {
Model model = new Model(FXCollections.observableArrayList("Apple", "Peach"));
ObservableList<Model> items = FXCollections.observableArrayList();
for (int i = 0; i < 50; i++) {
items.add(model);
}
table.setItems(items);
table.setEditable(true);
listCell.setCellFactory(factory -> new ListTableCell(
FXCollections.observableArrayList("Apple", "Orange", "Peach", "Banana", "Lemon", "Lime")));
listCell.setCellValueFactory(data -> data.getValue().list);
}
private class ListTableCell extends TableCell<Model, ObservableList<String>> {
private ObservableList<String> allItems;
ListTableCell(ObservableList<String> allItems) {
this.allItems = allItems;
}
#Override
public void startEdit() {
super.startEdit();
PopOver popOver = new PopOver();
popOver.setAutoHide(true);
PopupController sc = new PopupController(allItems, new ArrayList<>(getItem()));
popOver.setContentNode(new StackPane(sc.getPane()));
popOver.setOnHiding(event -> commitEdit(sc.getItems()));
popOver.show(this);
}
#Override
protected void updateItem(ObservableList<String> item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(StringUtils.join(item, ","));
}
}
}
private class Model {
ListProperty<String> list;
public Model(ObservableList<String> list) {
this.list = new SimpleListProperty<>(list);
}
}
private class PopupController {
private BorderPane pane = new BorderPane();
private ListView<String> left = new ListView<>();
private ListView<String> right = new ListView<>();
private Button toLeft = new Button("<");
private Button toRight = new Button(">");
PopupController(List<String> all, List<String> selected) {
VBox leftBox = new VBox();
leftBox.setSpacing(5);
leftBox.getChildren().add(toRight);
leftBox.getChildren().add(left);
pane.setLeft(leftBox);
VBox rightBox = new VBox();
rightBox.setSpacing(5);
rightBox.getChildren().add(toLeft);
rightBox.getChildren().add(right);
pane.setRight(rightBox);
ObservableList<String> allItems = FXCollections.observableArrayList(all);
allItems.removeAll(selected);
left.setItems(allItems);
right.setItems(FXCollections.observableArrayList(selected));
toLeft.disableProperty().bind(right.getSelectionModel().selectedItemProperty().isNull());
toRight.disableProperty().bind(left.getSelectionModel().selectedItemProperty().isNull());
toLeft.setOnAction(event -> {
String str = right.getSelectionModel().getSelectedItem();
right.getItems().remove(str);
left.getItems().add(str);
});
toRight.setOnAction(event -> {
String str = left.getSelectionModel().getSelectedItem();
left.getItems().remove(str);
right.getItems().add(str);
});
}
BorderPane getPane() {
return pane;
}
ObservableList<String> getItems() {
return right.getItems();
}
}
}
Here are two screenshots to show what I mean :
This is even worst: (with setAutoFix(false))
I am not expert with ControlFX but I believe the problem you are facing its because the height of your PopOver is greater than your current screen size thus it is trying to relocate itself in a way to be inside the screen local bounds. So in order to achieve what you are trying you will need to manually set the ArrowLocation of your PopOver control. Here is how you can solve the issue (using your code) :
#Override
public void startEdit() {
super.startEdit();
PopOver popOver = new PopOver();
popOver.setAutoHide(true);
// first set auto fix to false
// to manually set the arrow location
popOver.setAutoFix(false);
PopupController sc = new PopupController(allItems, new ArrayList<>(getItem()));
// set a specific height for our pane
final double paneHeight = 300;
StackPane popOverPane = new StackPane(sc.getPane());
popOverPane.setPrefHeight(paneHeight);
popOver.setContentNode(popOverPane);
popOver.setOnHiding(event -> commitEdit(sc.getItems()));
// find coordinates relative to the screen
Bounds screenBounds = this.localToScreen(this.getBoundsInLocal());
// get our current y position ( on screen )
int yPos = (int) screenBounds.getMinY();
// get screen size
Rectangle2D primaryScreenBounds = Screen.getPrimary().getVisualBounds();
int screenHeight = (int) primaryScreenBounds.getHeight();
// if the PopOver height + the current position is greater than
// the max screen's height then set the arrow position to bottom left
if(screenHeight < yPos + paneHeight) {
popOver.setArrowLocation(ArrowLocation.LEFT_BOTTOM);
}
popOver.show(this);
}
Using the code above you would see some things you need to change and think more carefully.
The first one is that you will need to set a specific size for your StackPane or to find a dynamic way to calculate it.
Secondly in my example I am using the Screen.getPrimary() which will get the Rectangle2D dimensions of your primary screen and not the screen you have your application, this means that if you have more monitors with different resolution and your program is displayed on the second one, the code above will still use the first ( default ) monitor's resolution which might not match with the primary one, so you will have to find a way to get the correct monitor resolution.
Lastly you will need to do the same when the window is on the right side of the screen because then the width of the 'Popover' will exceed the width of your monitor
Try setting setAutoFix(false) on the PopOver instance. From the documentation of the autoFix property of the PopOver's superclass PopupWindow:
This convenience variable indicates whether, when the popup is shown,
it should automatically correct its position such that it doesn't end
up positioned off the screen.
How can I make adding a value to items in a combo box possible so the user can either select from the existing items or clique "Add element" item to add a new item?
private ComboBox<String> comboStructDonnees;
Followed by:
comboData.getItems().addAll("TVW", "VWT", "TTVW", "VWXT", "Add item");
I don't know which event should I create next, I want to the text to be entered on the added element if possible.
Any help would be appreciated.
You can add an item with a "special value" (e.g. an empty string) to the end of the list of items for the combo box.
Use a cell factory to create a cell that displays a user-friendly message ("Add item..", for example) to the user when that value is displayed. Add an event filter to the cell that displays a dialog for inputting a new value if the cell is displaying the special value.
Here's a quick SSCCE:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextInputDialog;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class AddItemToComboBox extends Application {
#Override
public void start(Stage primaryStage) {
ComboBox<String> combo = new ComboBox<>();
combo.getItems().addAll("One", "Two", "Three", "");
combo.setCellFactory(lv -> {
ListCell<String> cell = new ListCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
if (item.isEmpty()) {
setText("Add item...");
} else {
setText(item);
}
}
}
};
cell.addEventFilter(MouseEvent.MOUSE_PRESSED, evt -> {
if (cell.getItem().isEmpty() && ! cell.isEmpty()) {
TextInputDialog dialog = new TextInputDialog();
dialog.setContentText("Enter item");
dialog.showAndWait().ifPresent(text -> {
int index = combo.getItems().size()-1;
combo.getItems().add(index, text);
combo.getSelectionModel().select(index);
});
evt.consume();
}
});
return cell ;
});
BorderPane root = new BorderPane();
root.setTop(combo);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I turned my column names as suggested here to labels so that I can get tooltip on their names:
for (Entry<String, String> ent : dc.getSortedAssignedOrg().entrySet()) {
TreeTableColumn<String, ArrayList<String>> col = new TreeTableColumn<>();
Label label = new Label(ent.getValue());
col.setGraphic(label);
col.setEditable(false);
col.setSortable(false);
label.setTooltip(new Tooltip(label.getText()));// tooltip for column
.
.
.
Now the problem is my TableMenuButton does not show the column names, and clicking on the plus sign on the right corner of treetableview opens a list in which there are only the checked signs, which I can remove or add. But the name itself is not shown. How can I fix this?
You could create your own table menu. You're better off with a custom menu anyway, since the in-built menu e. g. closes each time you click on a button. Unfortunately there is no getter for the context menu, so you'll have to find access to it either via reflection or a lookup.
I created a gist for a custom menu via the reflection and the lookup mechanism. Maybe it's of help for you.
The relevant part for you would be
CheckBox cb = new CheckBox(tableColumn.getText());
where you set the menu item to the text of your preferrence, i. e. the text of your labels.
Here's a lookup version for TreeTableView:
CustomTreeTableMenuDemo.java
import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class CustomTreeTableMenuDemo extends Application {
List<Employee> employees = Arrays.<Employee> asList(new Employee(
"Ethan Williams", "ethan.williams#example.com"), new Employee(
"Emma Jones", "emma.jones#example.com"), new Employee(
"Michael Brown", "michael.brown#example.com"), new Employee(
"Anna Black", "anna.black#example.com"), new Employee(
"Rodger York", "roger.york#example.com"), new Employee(
"Susan Collins", "susan.collins#example.com"));
final TreeItem<Employee> root = new TreeItem<>(new Employee(
"Sales Department", ""));
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
stage.setTitle("Table Menu Demo");
stage.setWidth(500);
stage.setHeight(550);
root.setExpanded(true);
employees.stream().forEach((employee) -> {
root.getChildren().add(new TreeItem<>(employee));
});
stage.setTitle("Tree Table View Sample");
final Scene scene = new Scene(new Group(), 400, 400);
scene.setFill(Color.LIGHTGRAY);
Group sceneRoot = (Group) scene.getRoot();
TreeTableColumn<Employee, String> empColumn = new TreeTableColumn<>(
"Employee");
empColumn.setPrefWidth(150);
empColumn
.setCellValueFactory((
TreeTableColumn.CellDataFeatures<Employee, String> param) -> new ReadOnlyStringWrapper(
param.getValue().getValue().getName()));
TreeTableColumn<Employee, String> emailColumn = new TreeTableColumn<>(
"Email");
emailColumn.setPrefWidth(190);
emailColumn
.setCellValueFactory((
TreeTableColumn.CellDataFeatures<Employee, String> param) -> new ReadOnlyStringWrapper(
param.getValue().getValue().getEmail()));
TreeTableView<Employee> treeTableView = new TreeTableView<>(root);
treeTableView.getColumns().setAll(empColumn, emailColumn);
sceneRoot.getChildren().add(treeTableView);
stage.setScene(scene);
stage.show();
// enable table menu button and add a custom menu to it
TreeTableUtils.addCustomTreeTableMenu(treeTableView);
}
public class Employee {
private SimpleStringProperty name;
private SimpleStringProperty email;
public SimpleStringProperty nameProperty() {
if (name == null) {
name = new SimpleStringProperty(this, "name");
}
return name;
}
public SimpleStringProperty emailProperty() {
if (email == null) {
email = new SimpleStringProperty(this, "email");
}
return email;
}
private Employee(String name, String email) {
this.name = new SimpleStringProperty(name);
this.email = new SimpleStringProperty(email);
}
public String getName() {
return name.get();
}
public void setName(String fName) {
name.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
TreeTableUtils.java
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.input.MouseEvent;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TreeTableViewSkin;
public class TreeTableUtils {
/**
* Make table menu button visible and replace the context menu with a custom context menu via reflection.
* The preferred height is modified so that an empty header row remains visible. This is needed in case you remove all columns, so that the menu button won't disappear with the row header.
* IMPORTANT: Modification is only possible AFTER the table has been made visible, otherwise you'd get a NullPointerException
* #param treeTableView
*/
public static void addCustomTreeTableMenu( TreeTableView treeTableView) {
// enable table menu
treeTableView.setTableMenuButtonVisible(true);
// replace internal mouse listener with custom listener
setCustomContextMenu( treeTableView);
}
private static void setCustomContextMenu( TreeTableView treeTableView) {
TreeTableViewSkin<?> treeTableViewSkin = (TreeTableViewSkin<?>) treeTableView.getSkin();
// get all children of the skin
ObservableList<Node> children = treeTableViewSkin.getChildren();
// find the TableHeaderRow child
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof TableHeaderRow) {
TableHeaderRow tableHeaderRow = (TableHeaderRow) node;
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
for( Node child: tableHeaderRow.getChildren()) {
// child identified as cornerRegion in TableHeaderRow.java
if( child.getStyleClass().contains( "show-hide-columns-button")) {
// get the context menu
ContextMenu columnPopupMenu = createContextMenu( treeTableView);
// replace mouse listener
child.setOnMousePressed(me -> {
// show a popupMenu which lists all columns
columnPopupMenu.show(child, Side.BOTTOM, 0, 0);
me.consume();
});
}
}
}
}
}
/**
* Create a menu with custom items. The important thing is that the menu remains open while you click on the menu items.
* #param cm
* #param treeTableView
*/
private static ContextMenu createContextMenu( TreeTableView treeTableView) {
ContextMenu cm = new ContextMenu();
// create new context menu
CustomMenuItem cmi;
// select all item
Label showAll = new Label("Show all");
showAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : treeTableView.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(true);
}
}
});
cmi = new CustomMenuItem(showAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label hideAll = new Label("Hide all");
hideAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : treeTableView.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(false);
}
}
});
cmi = new CustomMenuItem(hideAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
// menu item for each of the available columns
for (Object obj : treeTableView.getColumns()) {
TreeTableColumn<?, ?> tableColumn = (TreeTableColumn<?, ?>) obj;
CheckBox cb = new CheckBox(tableColumn.getText());
cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
cmi = new CustomMenuItem(cb);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
return cm;
}
}
I've tried everything. I think they made a big mistake not giving any reference to the indexed cell in anything.
I can get my menu, but not in the right place. Right click is fine.
In my TreeView I can use get KeyReleased but I don't know where to put the menu.
setOnKeyReleased((KeyEvent t) -> {
switch (t.getCode()) {
case CONTEXT_MENU:
getSelectionModel().getSelectedItem().setGraphic(new Label("hi"));
//showMenu just calls show on my ContextMenu of my subclass TreeNode
((TreeNode)getSelectionModel().getSelectedItem()).showMenu(
getSelectionModel().getSelectedItem().getGraphic().getLocalToSceneTransform());
break;
}
});
None of the layout methods will give me the coords of the TreeCell
It simply isn't possible to provide API access to the cell for a given item. Not every item has a cell associated with it. On top of that, the item which is represented by a cell may change at any time, so even if you could provide access to the cell, the API would potentially be very confusing.
The basic trick to anything like this is to create a cell factory, and register the appropriate listeners with the cell. Your case is somewhat tricky, but possible. The following works to get the cell representing the selected item (you may want to modify the code somewhat to deal with the case where the cell is scrolled off the screen).
(Note that I used the Z key, arbitrarily, as I don't have a ContextMenu key on my laptop.)
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
TreeView<String> treeView = new TreeView<>();
TreeItem<String> treeRoot = new TreeItem<>("Root");
for (int i=1; i<=5; i++) {
TreeItem<String> child = new TreeItem<>("Item "+i);
child.getChildren().addAll(new TreeItem<>("Item "+i+"A"), new TreeItem<>("Item "+i+"B"));
treeRoot.getChildren().add(child);
}
treeView.setRoot(treeRoot);
root.setCenter(treeView);
ObjectProperty<TreeCell<String>> selectedCell = new SimpleObjectProperty<>();
treeView.setCellFactory(tree -> {
TreeCell<String> cell = new TreeCell<>();
cell.textProperty().bind(cell.itemProperty());
ChangeListener<TreeItem<String>> listener = (obs, oldItem, newItem) -> {
TreeItem<String> selectedItem = treeView.getSelectionModel().getSelectedItem();
if (selectedItem == null) {
selectedCell.set(null);
} else {
if (selectedItem == cell.getTreeItem()) {
selectedCell.set(cell);
}
}
};
cell.treeItemProperty().addListener(listener);
treeView.getSelectionModel().selectedItemProperty().addListener(listener);
return cell ;
});
ContextMenu contextMenu = new ContextMenu();
for (int i=1; i<=3; i++) {
String text = "Choice "+i;
MenuItem menuItem = new MenuItem(text);
menuItem.setOnAction(event -> System.out.println(text));
contextMenu.getItems().add(menuItem);
}
treeView.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.Z) {
if (selectedCell.get() != null) {
Node anchor = selectedCell.get();
// figure center of cell in screen coords:
Bounds anchorBounds = anchor.getBoundsInParent();
double x = anchorBounds.getMinX() + anchorBounds.getWidth() / 2 ;
double y = anchorBounds.getMinY() + anchorBounds.getHeight() / 2 ;
Point2D screenLoc = anchor.getParent().localToScreen(x, y);
contextMenu.show(selectedCell.get(), screenLoc.getX(), screenLoc.getY());
}
}
});
Scene scene = new Scene(root,400,400);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}