JavaFX using ObversableList.getItems().addAll() cannot be used multiple times - java

I'm trying to populatw multiple MenuButton's in javaFX with an arrayList of checkMenuItems. To add the check menu items im doing:
myMenuButton.getItems().addAll(ChecKMenuItemList);
this is what my code looks like
class Scratch {
private void updateClassList(ArrayList<Class> classArrayList) {
ArrayList<String> classCodeList = new ArrayList<>();
ArrayList<CheckMenuItem> checkMenuItemList = new ArrayList<>();
ArrayList<CheckMenuItem> checkMenuItemList2 = new ArrayList<>();
ArrayList<String> classNameList = new ArrayList<>();
//Create Arrays of class elements
for(Class aClass : classArrayList){
checkMenuItemList.add(new CheckMenuItem(aClass.getClassCode()));
}
//Clear Class Lists
addStudentsToClassClassListView.getItems().clear();
assignClassesToTeachersClassListView.getItems().clear();
//Populate dropdown lists
addStudentSelectClassesMenuButton.getItems().setAll(checkMenuItemList);
addTeacherSelectClasses.getItems().setAll(checkMenuItemList);
}
}
This function is called from another function after the user inputs a json file that is parsed for data.
The problem im running into is when i try to use .getItems().addAll() it only works once, in my code if you comment one of the two lines the other one will work and vice versa, its strange since they work on their own but not together
Since both of them work on their own I'm not sure what the issue would be thats causing it not too update. There is no error or exception simply nothing happens. After both of the lines executes and before the function completes while debugging it says the both menubuttons have 6 items but when you click on the menu button nothing happens

The issue is NOT:
ObservableList.getItems().addAll() cannot be used multiple times
it definitely can be used multiple times.
ObservableList<Integer> list = FXCollections.observableArrayList(1, 2, 3);
list.addAll(4, 5, 6);
list.addAll(7, 8, 9);
System.out.println(list);
Will output as expected:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
However, you need to use APIs correctly in context.
Items in the scene graph can only be in one position at a time. A CheckMenuItem is not a node, but it is probably backed by nodes and thus acts like a node, so I wouldn't add a single instance to more than one menu at a time.
Instead, create another CheckMenuItem instance, with the same data, and add that. Bidirectional binding can be used to ensure that if one menu item is checked, the other menu item's state is updated to reflect that, and vice versa.
See the scene javadoc:
A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in the children list of a Parent or as the clip of a Node. See the Node class for more details on these restrictions.
Also, the node javadoc:
If a program adds a child node to a Parent (including Group, Region, etc) and that node is already a child of a different Parent or the root of a Scene, the node is automatically (and silently) removed from its former parent.
It would appear that CheckMenuItem acts the same way. It would probably be better if the Menu documentation stated that items can only appear in one menu at a time.
Examples to demonstrate failure and fixes
In this example, two menus are created and the same items are added to both menus. On execution, only one of the menus (the last one to which the items were added) will contain the added items.
The execution warns you, in the system error console, that there is a problem.
Dec 13, 2022 4:00:27 PM javafx.scene.control.Menu$6 onChanged
WARNING: Adding MenuItem Check 1 that has already been added to Menu 1
Dec 13, 2022 4:00:27 PM javafx.scene.control.Menu$6 onChanged
WARNING: Adding MenuItem Check 2 that has already been added to Menu 1
Broken code
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;
public class MenuItemApp extends Application {
#Override
public void start(Stage stage) throws Exception {
MenuItem[] menuItems = createCheckMenuItems();
Menu menu1 = new Menu("Menu 1");
menu1.getItems().addAll(menuItems);
Menu menu2 = new Menu("Menu 2");
menu2.getItems().addAll(menuItems);
MenuBar menuBar = new MenuBar(menu1, menu2);
Scene scene = new Scene(menuBar);
stage.setScene(scene);
stage.show();
}
private MenuItem[] createCheckMenuItems() {
return new MenuItem[] {
new CheckMenuItem("Check 1"),
new CheckMenuItem("Check 2")
};
}
public static void main(String[] args) {
Application.launch();
}
}
We can fix this by just creating new check menu items for each menu.
Menu menu1 = new Menu("Menu 1");
menu1.getItems().addAll(createCheckMenuItems());
Menu menu2 = new Menu("Menu 2");
menu2.getItems().addAll(createCheckMenuItems());
But now the check menu items aren't in sync, if you change one, the other one doesn't automatically change. If you also want that behavior, you can use an MVC approach with a shared binding.
Fixed code with bidirectional binding to model properties
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;
public class MenuItemApp extends Application {
class Model {
private final BooleanProperty boolean1 = new SimpleBooleanProperty();
private final BooleanProperty boolean2 = new SimpleBooleanProperty();
public BooleanProperty boolean1Property() {
return boolean1;
}
public BooleanProperty boolean2Property() {
return boolean2;
}
}
#Override
public void start(Stage stage) throws Exception {
Model model = new Model();
Menu menu1 = new Menu("Menu 1");
menu1.getItems().addAll(createCheckMenuItems(model));
Menu menu2 = new Menu("Menu 2");
menu2.getItems().addAll(createCheckMenuItems(model));
MenuBar menuBar = new MenuBar(menu1, menu2);
Scene scene = new Scene(menuBar);
stage.setScene(scene);
stage.show();
}
private MenuItem[] createCheckMenuItems(Model model) {
return new MenuItem[] {
createCheckMenuItem(1, model.boolean1Property()),
createCheckMenuItem(2, model.boolean2Property()),
};
}
private CheckMenuItem createCheckMenuItem(int n, BooleanProperty modelProperty) {
CheckMenuItem checkMenuItem = new CheckMenuItem("Check " + n);
checkMenuItem.selectedProperty().bindBidirectional(modelProperty);
return checkMenuItem;
}
public static void main(String[] args) {
Application.launch();
}
}

Related

java fx remove stackPane

I need help creating a delete button on a stack pane in JavaFX.
It should remove the chosen stack pane from the group.
RinStackPane is extended javafx StackPane, gp is java fx Group.
Thank you in advance!
Here is my code:
private void setOnCreateMathOperation(Button createMathOperationBtn) {
createMathOperationBtn.setOnAction(event -> {
var mathOperation = createMathOperation();
RinStackPane stack1 = new RinStackPane();
stack1.setNodeType(NodesTypes.MathOperation);
stack1.setUnderlyingNode(mathOperation);
stack1.setName(mathOperation.getName());
stack1.getChildren().add(mathOperation);
TextField newtf1 = new TextField();
GridPane grid = new GridPane();
grid.addRow(1, newtf1);
stack1.getChildren().add(grid);
makeDraggable(stack1);
addToRectCollection(stack1);
gp.getChildren().add(stack1);
});
}
I am not sure if you want to keep the delete button inside or outside the StackPane.
Anyway you can easily bind an ObservableList to the Group's children. When an elemente will be deleted from the list, automatically this event will be reflected on Group's children:
//Create observable list of Node (you can constrain to RinStackPane if you prefer):
ObservableList<Node> list = FXCollections.observableArrayList();
//bind lists to gp's children:
Bindings.bindContentBidirectional(list, gp.getChildren());
Your code:
private void setOnCreateMathOperation(Button createMathOperationBtn) {
createMathOperationBtn.setOnAction(event -> {
RinStackPane stack1 = new RinStackPane();
//...
//setup the stack
//...
//Add the delete button to the stack:
Button deleteButton = new Button("Delete me!");
deleteButton.setOnMouseClicked(event -> {
list.remove(stack1); //<--- remove action
});
stack1.getChildren().add(deleteButton);
//add to the list instead of gp's children
list.add(stack1);
});
}
Note how the stack is not added nor removed to/from the gp's children, instead the list is used.
If you want the deleteButton outside the stack the code is the same, just be sure that both list and stack1 are somehow passed to the button's click handler.
Hope this helps.

JavaFx: ComboBox editor's text

I am using the comboBox suggested in this answer: JavaFx: show DatePicker, but I have a problem displaying the correct text. If I chose a date from DatePicker I tried to set the editor's text but it happens nothing.
This is what I have tried:
getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) ->{
if (newValue != null) {
if (MyTupe.DATE.equals(newValue.getType())) {
initDatePicker();
datePicker.valueProperty().addListener((obs, oldDate, newDate) -> {
newValue.setValue(newDate);
getEditor().setText(newDate.toString());
datePopOver.hide();
});
StackPane stackPane = new StackPane(datePicker);
stackPane.setPadding(new Insets(10, 10, 10, 10));
datePopOver.setContentNode(stackPane);
datePopOver.show(this);
} else {
datePopOver.hide();
}
}
});
After experimenting a lot I added two events to the editor's textProperty and setOnMouseClicked like this:
getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
System.out.println("Text changed");
});
setOnMouseClicked(event -> {
System.out.println(getEditor().getText());
});
First is not triggered if I don't set manually getEditor().setText(); even if I change the value of the comboBox, that is a little bit weird for me, because I thought this textField holds the text of the comboBox.(maybe am I wrong??). The mouseClick event displays every time an empty string if that line is removed.
If the getEditor().setText(); line is there the editor's getText() returns the correct text but it is not displayed in comboBox.
The questions:
Where is the comboBox's text stored?
How can I change the text in the selectedItemProperty's listener?
I assume that maybe I did somewhere a mistake so that's why this is not working, but I have no idea what, can you help me?
In the linked code
Replace:
items.set(0, new ComboBoxNode(newDate, DATE_TYPE));
with:
items.set(customComboBox.getSelectionModel().getSelectedIndex(), new ComboBoxNode(newDate, DATE_TYPE));
The linked code only changes the date at location zero. In this new code, the date in the appropriate location is changed.
Full Code:
import java.time.LocalDate;
import java.time.Month;
import java.util.Objects;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.controlsfx.control.PopOver;
/**
*
* #author blj0011
*/
public class JavaFXApplication155 extends Application
{
private static final String DATE_TYPE = "DATE";
private class ComboBoxNode {
private final Object value;
private final String type;
private ComboBoxNode(final Object value, final String type) {
this.value = value;
this.type = type;
}
#Override
public String toString() {
return Objects.toString(value);
}
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
final ObservableList<ComboBoxNode> items =
FXCollections.observableArrayList(
new ComboBoxNode(LocalDate.now(), DATE_TYPE),
new ComboBoxNode(LocalDate.of(2017, Month.APRIL, 15), DATE_TYPE),//Added new date to ComboBox
new ComboBoxNode("11:35AM", "TIME"));
final PopOver datePopOver = new PopOver();
datePopOver.setTitle("Enter new date");
datePopOver.setCornerRadius(10);
datePopOver.setHeaderAlwaysVisible(true);
//datePopOver.set(true);
datePopOver.setAutoHide(true);
final ComboBox<ComboBoxNode> customComboBox = new ComboBox<>(items);
customComboBox.getSelectionModel().selectedItemProperty().addListener((o, old, newNode) -> {
if (newNode != null) {
if (newNode.type.equals(DATE_TYPE)) {
final DatePicker datePicker = new DatePicker((LocalDate) newNode.value);
datePicker.valueProperty().addListener((obs, oldDate, newDate) -> {
items.set(customComboBox.getSelectionModel().getSelectedIndex(), new ComboBoxNode(newDate, DATE_TYPE));//Fixed this line of code
datePopOver.hide();
});
final StackPane stackPane = new StackPane(datePicker);
stackPane.setPadding(new Insets(10, 10, 10, 10));
datePopOver.setContentNode(stackPane);
datePopOver.show(customComboBox);
} else {
datePopOver.hide();
}
}
});
final FlowPane pane = new FlowPane(customComboBox);
pane.setPadding(new Insets(10, 10, 10, 10));
pane.setPrefWidth(400);
pane.setPrefHeight(300);
// Show Scene
final Scene scene = new Scene(pane);
primaryStage.setTitle("Popup calendar");
primaryStage.setScene(scene);
primaryStage.show();
}
}
Regarding your question: where is the comboBox's text stored?
In general JavaFX controls are based on MVC architecture where the data is stored in model. Controls just represent model data according to defined converters and automatically reacts on any changes of model.
So, in your case the data is stored in ObservableList items. To have updated combobox you need just to replace the corresponding item by new object.
Note, the combobox already listens the ObservableList and any add/remove/set will be automatically represented on UI side.
Why this code doesn't work:
newValue.setValue(newDate);
Because you change the internal state of an item and ObservableList items doesn't react on such changes. Only the changing of the list will work. #Sedrick proposed correct solution.
See also the related question.

Why can I update TableView from a non-UI thread but not ListView?

I have encountered something odd in JavaFX (java version 1.8.0_91). It was my understanding that if one wants to update the UI from a separate thread, one must either use Platform.runLater(taskThatUpdates) or one of the tools in the javafx.concurrent package.
However, if I have a TableView on which I call .setItems(someObservableList), I can update someObservableList from a separate thread and see the corresponding changes to my TableView without the expected Exception in thread "X" java.lang.IllegalStateException: Not on FX application thread; currentThread = X error.
If I replace TableView with ListView, the expected error occurs.
Example code for situation #1: updating a TableView from a different thread, with no call to Platform.runLater()--and no error.
public class Test extends Application {
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) throws Exception {
// Create a table of integers with one column to display
TableView<Integer> data = new TableView<>();
TableColumn<Integer, Integer> num = new TableColumn<>("Number");
num.setCellValueFactory(v -> new ReadOnlyObjectWrapper(v.getValue()));
data.getColumns().add(num);
// Create a window & add the table
VBox root = new VBox();
Scene scene = new Scene(root);
root.getChildren().addAll(data);
stage.setScene(scene);
stage.show();
// Create a list of numbers & bind the table to it
ObservableList<Integer> someNumbers = FXCollections.observableArrayList();
data.setItems(someNumbers);
// Add a new number every second from a different thread
new Thread( () -> {
for (;;) {
try {
Thread.sleep(1000);
someNumbers.add((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
Example code for situation #2: updating a ListView from a different thread, with no call to Platform.runLater()--produces an error.
public class Test extends Application {
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) throws Exception {
// Create a list of integers (instead of a table)
ListView<Integer> data = new ListView<>();
// Create a window & add the table
VBox root = new VBox();
Scene scene = new Scene(root);
root.getChildren().addAll(data);
stage.setScene(scene);
stage.show();
// Create a list of numbers & bind the table to it
ObservableList<Integer> someNumbers = FXCollections.observableArrayList();
data.setItems(someNumbers);
// Add a new number every second from a different thread
new Thread( () -> {
for (;;) {
try {
Thread.sleep(1000);
someNumbers.add((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
Note that the only difference is the instantiation of data as a ListView<Integer> rather than a TableView<Integer>.
So what gives here? Is this happening because of the call to TableColumn::setCellValueFactory() in the first example?--that's my intuition. I would like to know why one does not cause an error and the other does, and more specifically what the rules are for how the .setItems call binds data to the view.
As #James_D already mentioned, you should not update things that belongs to the FX Application Thread on any other thread. But in your example you've been updating the ObservableList in another thread. No matter why this work for TableView and for ListView not, it is the wrong way of doing it.
Have a look at the Task class if you want to perform intermediate updates on backing ObservableLists.
From the Task-API-Doc
A Task Which Returns An ObservableList
Because the ListView, TableView, and other UI controls and scene graph
nodes make use of ObservableList, it is common to want to create and
return an ObservableList from a Task. When you do not care to display
intermediate values, the easiest way to correctly write such a Task is
simply to construct an ObservableList within the call method, and then
return it at the conclusion of the Task.
and another hint:
A Task Which Returns Partial Results
Sometimes you want to create a Task which will return partial results.
Perhaps you are building a complex scene graph and want to show the
scene graph as it is being constructed. Or perhaps you are reading a
large amount of data over the network and want to display the entries
in a TableView as the data is arriving. In such cases, there is some
shared state available both to the FX Application Thread and the
background thread. Great care must be taken to never update shared
state from any thread other than the FX Application Thread.
The easiest way to do this is to take advantage of the
updateValue(Object) method. This method may be called repeatedly from
the background thread. Updates are coalesced to prevent saturation of
the FX event queue. This means you can call it as frequently as you
like from the background thread but only the most recent set is
ultimately set.
An example of such a Task class for doing intermediate updates on ObservableLists for TableView and ListView is:
import java.util.ArrayList;
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class IntermediateTest extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Integer> tv = new TableView<>();
TableColumn<Integer, Integer> num = new TableColumn<>("Number");
num.setCellValueFactory(v -> new ReadOnlyObjectWrapper(v.getValue()));
tv.getColumns().add(num);
PartialResultsTask prt = new PartialResultsTask();
tv.setItems(prt.getPartialResults());
ListView<Integer> lv = new ListView<>();
PartialResultsTask prt1 = new PartialResultsTask();
lv.setItems(prt1.getPartialResults());
new Thread(prt).start();
new Thread(prt1).start();
// Create a window & add the table
VBox root = new VBox();
root.getChildren().addAll(tv, lv);
Scene scene = new Scene(root, 300, 450);
primaryStage.setTitle("Data-Adding");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
public class PartialResultsTask extends Task<ObservableList<Integer>> {
private ReadOnlyObjectWrapper<ObservableList<Integer>> partialResults
= new ReadOnlyObjectWrapper<>(this, "partialResults",
FXCollections.observableArrayList(new ArrayList()));
public final ObservableList getPartialResults() {
return partialResults.get();
}
public final ReadOnlyObjectProperty<ObservableList<Integer>> partialResultsProperty() {
return partialResults.getReadOnlyProperty();
}
#Override
protected ObservableList call() throws Exception {
updateMessage("Creating Integers...");
Random rnd = new Random();
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
if (isCancelled()) {
break;
}
final Integer r = rnd.ints(100, 10000).findFirst().getAsInt();
Platform.runLater(() -> {
getPartialResults().add(r);
});
updateProgress(i, 10);
}
return partialResults.get();
}
}
}

Bind ObservableList<TextField> to ListProperty<String> JFX

Given
I have a model class Model that contains the following fields:
StringProperty stringProperty; ListProperty<String> listOfStringsProperty
I have a view class View extends VBox that has the following:
TextField stringTextField; TextFieldList stringsTextFieldList;
TextFieldList extends VBox is a custom class that I created that handles multiple TextFields with nice + and - buttons to add and remove input text fields.
TextFieldList class contains the field ObservableList<Node> inputTextFields and I can get the data from these InputTextFields by a method call List<String> getData()
Question
I was able to do the following:
stringTextField.textProperty().bindBidirectional(model.getStringProperty());
in order to bind the result of the stringTextField in View to the stringProperty in Model
And I need to do something like
stringsTextFieldList.listProperty().bindBidirectional(model.getListOfStringsProperty());
How can I do that?
If this design would not work, then how do you suggest I fix it? Is there a built-in class that does the same as TextFieldList but instead extends Control?
If you decide to make your own control you should create the "binding manually", that means that in the input ObservableList you add a ListChangeListener then you process the Change like in the example: check whether a new item is added, removed or updated and maintain your TextFields accordingly. It is possible, but my answer is mainly about proposing an existing control to re-use rather than create your own one.
So if you don't want to re-invent the wheel:
I don't know your exact use-case, but maybe it is reasonable to reuse a control that actually support a data model, like a ListView.
In the example I have modified the Model class to have an ObservableList<StringProperty> rather than a ListProperty<String> (note: it is also possible to simply have String objects in the list, I just modified it to make the binding really clear). I have added a ListView and used setCellFactory to draw TextFields as elements in the list which are bidirectionally bounded to the corresponding StringProperty in the list. I have also added several buttons to add and remove elements and a button to print the current content of the model.
Example:
Model.java
public class Model {
public ObservableList<StringProperty> listOfStringsProperty;
public Model(){
listOfStringsProperty = FXCollections.observableArrayList();
}
}
Main.java
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
Scene scene = new Scene(root,400,400);
Model m = new Model();
m.listOfStringsProperty.addAll(new SimpleStringProperty("First"),
new SimpleStringProperty("Second"),
new SimpleStringProperty("Third"));
ListView<StringProperty> lv = new ListView<StringProperty>();
lv.setCellFactory(new Callback<ListView<StringProperty>, ListCell<StringProperty>>() {
#Override
public ListCell<StringProperty> call(ListView<StringProperty> param) {
return new ListCell<StringProperty>(){
#Override
protected void updateItem(StringProperty item, boolean empty) {
super.updateItem(item, empty);
if(item == null){
setText(null);
setGraphic(null);
return;
}
TextField tf = new TextField();
tf.textProperty().bindBidirectional(item);
setGraphic(tf);
}
};
}
});
lv.setItems(m.listOfStringsProperty);
root.setCenter(lv);
// Control buttons
HBox hbox = new HBox();
Button buttonAdd = new Button("Add");
buttonAdd.setOnAction(e -> m.listOfStringsProperty.add(new SimpleStringProperty("")));
Button buttonRemove = new Button("Remove last");
buttonRemove.setOnAction(e -> m.listOfStringsProperty.remove(m.listOfStringsProperty.size()-1));
Button buttonPrintModel = new Button("Print model");
buttonPrintModel.setOnAction(e -> System.out.println(m.listOfStringsProperty.toString()));
hbox.getChildren().addAll(buttonAdd, buttonRemove, buttonPrintModel);
root.setBottom(hbox);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
This will produce the following window:
You can use
Bindings.bindContent(List list1, ObservableList list2).
This is a special binding that keeps the list in sync with the observable list. Keep in mind that this is not bidirectional though.
If bidirectorionality is what you want, you should use invalidation listeners or change listeners to detect changes and synchronize the lists manually. You might need some crude locking mechanism to prevent a stack overflow.

JavaFX Split Menu Button Arrow Trigger Event

I have a SplitMenuButton, and I can't seem to find a way to trigger an event when the user clicks the arrow next to the button.
I would like the dropdown to fill with items from a database when the dropdown arrow is clicked.
I am not sure which event can do that, and I can not find any info on this either.
Short answer: register a listener with the showing property.
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitMenuButton;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class SplitMenuButtonTest extends Application {
#Override
public void start(Stage primaryStage) {
IntegerProperty count = new SimpleIntegerProperty();
SplitMenuButton splitMenuButton = new SplitMenuButton();
splitMenuButton.setText("Action");
splitMenuButton.showingProperty().addListener((obs, wasShowing, isNowShowing) -> {
if (isNowShowing) {
int c = count.get() + 1;
count.set(c);
splitMenuButton.getItems().clear();
for (int choice = 1; choice <= 3; choice++) {
MenuItem mi = new MenuItem("Choice "+choice+" (" + c + ")");
splitMenuButton.getItems().add(mi);
}
}
});
BorderPane root = new BorderPane(null, splitMenuButton, null, null, null);
primaryStage.setScene(new Scene(root, 350, 150));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Sort of as an aside, I'm not sure this is a really good idea. Database connections are typically long-running processes (i.e. long enough to be visually noticeable in a UI environment). If you run this on the FX Application Thread, then you're going to block the UI from doing anything while the data is retrieved, and that's also right at the moment the user has just tried to do something. Of course, if you run it as a background task, then the menu will popup with the previous data, and then later update once the data is downloaded. I would recommend finding a way to populate this before the user requests it.
The 'arrow' is just another button used to show the popup with the menu items.
One easy way of knowing if this arrow button is pressed is by listening to the showing property of the popup.
Once you know that the popup is showing up, you can add your items.
#Override
public void start(Stage primaryStage) {
SplitMenuButton m = new SplitMenuButton();
m.showingProperty().addListener((ov,b,b1)->{
if(b1){
System.out.println("popup visible");
MenuItem menuItem = new MenuItem("New Option");
if(m.getItems().stream().noneMatch(i->i.getText().equals(menuItem.getText()))){
menuItem.setOnAction(e -> System.out.println("New Option added"));
m.getItems().add(menuItem);
}
}
});
m.setText("Click the arrow ->");
m.getItems().add(new MenuItem("First option"));
StackPane root = new StackPane(m);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}

Categories

Resources