javafx: How to bind disable button with dynamic created checkboxes? - javafx

I want to bind the disable of a button with dynamically created checkboxes. The Button should be enabled if a checkbox is selected.
This is my code
public class DietTabPageController {
#FXML
private FlowPane parent;
#FXML
private Button okButton;
private ObservableList<CheckBox> checkBoxes=FXCollections.observableArrayList();
#FXML
private void initialize() {
ObservableList<Diet> diets = DietDAO.getDiets();
diets.forEach(diet -> checkBoxes.add(new CheckBox(diet.getName())));
//checkboxes added in parent Flowpane
parent.getChildren().addAll(checkBoxes);
}
}
Any suggestions? Thanks

You can use JavaFX's really nice Bindings-class!
Try this:
okButton.disableProperty().bind(
Bindings.createBooleanBinding(
()->!checkBoxes.stream().anyMatch(CheckBox::isSelected),
checkBoxes.stream().map(x->x.selectedProperty()).toArray(Observable[]::new)
)
);
This creates a new Binding, which will listen on every checkbox and then call the given function to calculate the value of your property.
Additional reading here: Bindings
Regarding your comment:
I don't know how much you can edit your Diet class, but if you can, there is a very simple way to display your checkboxes and add the button-binding. Take a look at the following sample:
ListView<Diet> dietsView = new ListView<>(diets);
dietsView.setCellFactory(CheckBoxListCell.forListView(diet ->
diet.selectedProperty()));
btn.disableProperty().bind(
Bindings.createBooleanBinding(
() -> !diets.stream().anyMatch(diet->diet.isSelected()),
diets.stream().map(x->x.selectedProperty())
.toArray(Observable[]::new)
)
);
add this to Diet class:
private final BooleanProperty selected = new SimpleBooleanProperty();
public final BooleanProperty selectedProperty() {
return this.selected;
}
public final boolean isSelected() {
return this.selectedProperty().get();
}
public final void setSelected(final boolean on) {
this.selectedProperty().set(on);
}

You need to add listeners to all the selected properties of the CheckBoxes. Every time one of the property changes, modify the Button's disable property, if necessary. BTW: Making checkBoxes observable doesn't seem necessary:
private List<CheckBox> checkBoxes;
#FXML
private void initialize() {
ObservableList<Diet> diets = DietDAO.getDiets();
checkBoxes = new ArrayList<>(diets.size());
ChangeListener<Boolean> listener = (o, oldValue, newValue) -> {
if (newValue) {
// activate button since at least one CheckBox is selected
okButton.setDisable(false);
} else {
// disable button, if the last CheckBox was unselected
for (CheckBox cb : checkBoxes) {
if (cb.isSelected()) {
return; // don't do anything, if there still is a selected CheckBox
}
}
okButton.setDisable(true);
}
};
for (Diet diet : diets) {
CheckBox cb = new CheckBox(diet.getName());
cb.selectedProperty().addListener(listener);
checkBoxes.add(cb);
}
//checkboxes added in parent Flowpane
parent.getChildren().addAll(checkBoxes);
}

Related

JavaFX tableview auto scroll to selected item when pressing a button to selectNext() or selectPrevious()

I'm writing a JavaFX program with a TableView called 'table' and 2 buttons called 'previous' & 'next'.
Here is part of the code:
previous.setOnAction(event -> {
table.getSelectionModel().selectPrevious();
});
next.setOnAction(event -> {
table.getSelectionModel().selectNext();
});
However, if I keep pressing the buttons, the table will not scroll automatically to keep the selected item visible. So I modified the code like this :
previous.setOnAction(event -> {
table.getSelectionModel().selectPrevious();
table.scrollTo(table.getSelectionModel().getSelectedItem());
});
next.setOnAction(event -> {
table.getSelectionModel().selectNext();
table.scrollTo(table.getSelectionModel().getSelectedItem());
});
But it will always try to keep the selected item at the top of the visible region. If I keep pressing 'next'. The selected item will stay at the top instead of staying at the bottom.
I want to mimic the natural behavior of a tableview in the way that if I press up or down on the keyboard with something selected, the tableview will scroll automatically to keep the selected item visible.
How should I modify the code to make the auto scrolling more natural when I press the buttons?
Thanks
The problem is
missing fine-grained control of scrollTo target location on application level
the (somewhat unfortunate) implementation of virtualizedControl.scrollTo(index) which (ultimately) leads to calling flow.scrollToTop(index)
There's a long-standing RFE (reported 2014!) requesting better control from application code. Actually, VirtualFlow has public methods (scrollToTop, scrollTo, scrollPixels) providing such, only they are not passed on to the control layer (getVirtualFlow in VirtualContainerBase is final protected), so can't be overridden in a custom skin. Since fx12, we can hack a bit, and expose the onSelectXX of Tree/TableViewSkin and use those, either directly in application code (example below) or in a custom TableView.
Example code:
public class TableSelectNextKeepVisible extends Application {
/**
* Custom table skin to expose onSelectXX methods for application use.
*/
public static class MyTableSkin<T> extends TableViewSkin<T> {
public MyTableSkin(TableView<T> control) {
super(control);
}
/**
* Overridden to widen scope to public.
*/
#Override
public void onSelectBelowCell() {
super.onSelectBelowCell();
}
/**
* Overridden to widen scope to public.
*/
#Override
public void onSelectAboveCell() {
super.onSelectAboveCell();
}
}
private Parent createContent() {
TableView<Locale> table = new TableView<>(FXCollections.observableArrayList(Locale.getAvailableLocales())) {
#Override
protected Skin<?> createDefaultSkin() {
return new MyTableSkin<>(this);
}
};
TableColumn<Locale, String> country = new TableColumn<>("Column");
country.setCellValueFactory(new PropertyValueFactory<>("displayLanguage"));
table.getColumns().addAll(country);
Button next = new Button("next");
next.setOnAction(e -> {
table.getSelectionModel().selectNext();
// scrolls to top
// table.scrollTo(table.getSelectionModel().getSelectedIndex());
((MyTableSkin<?>) table.getSkin()).onSelectBelowCell();
});
Button previous = new Button("previous");
previous.setOnAction(e -> {
table.getSelectionModel().selectPrevious();
// scrolls to top
// table.scrollTo(table.getSelectionModel().getSelectedIndex());
((MyTableSkin<?>) table.getSkin()).onSelectAboveCell();
});
BorderPane content = new BorderPane(table);
content.setBottom(new HBox(10, next, previous));
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
try using getSelectedIndex as follows instead of using getSelectedItem
previous.setOnAction(event -> {
table.getSelectionModel().selectPrevious();
table.scrollTo(table.getSelectionModel().getSelectedIndex());
});
Platform.runLater( () -> TABLE_NAME.scrollTo(TABLE_INFORMATION_LIST.getList().size()-index) );
should work if you call it whenever you add information to the table.

How to bind to a property within a ObservableMap in JavaFX?

I am trying to automatically update a JavaFX ListView when a change occurs on a Property located within an ObservableMap.
Below is my model, where I have a Project, containing a list of Seats, and each Seat in turn contains a Map of type <Layer, ObjectProperty<Category>>.
What I am trying to achieve is to bind an ui element to that ObjectProperty<Category> within the Map.
Here is the Model:
public class Seat {
private final DoubleProperty positionX;
private final DoubleProperty positionY;
private final MapProperty<Layer, ObjectProperty<Category>> categoryMap;
public Seat() {
this.positionX = new SimpleDoubleProperty();
this.positionY = new SimpleDoubleProperty();
this.categoryMap = new SimpleMapProperty(FXCollections.observableHashMap());
}
}
public class Project {
private ObservableList<Seat> seatList;
public Project() {
seatList = FXCollections.observableArrayList(
new Callback<Seat, Observable[]>() {
#Override
public Observable[] call(Seat seat) {
return new Observable[]{
seat.categoryMapProperty()
};
}
}
);
}
The UI element I want to bind is a ListView with a custom cell as follows:
public class CategoryCell extends ListCell<Category>{
private ToggleButton viewButton;
private Rectangle colorRect;
private Label name;
private Label count;
private GridPane pane;
public CategoryCell(ObservableList<Seat> seatList) {
super();
buildGui();
itemProperty().addListener((list, oldValue, newValue) -> {
if (newValue != null) {
//Bind color
colorRect.fillProperty().bind(newValue.colorProperty());
//Bind category name
name.textProperty().bind(newValue.nameProperty());
//Bind number of seats assigned to this category
LongBinding categorySeatNumProperty = Bindings.createLongBinding(() ->
seatList.stream().filter(seat -> seat.getCategory(newValue.getLayer()).equals(newValue)).count(), seatList);
count.textProperty().bind(categorySeatNumProperty.asString());
}
if (oldValue != null) {
name.textProperty().unbind();
count.textProperty().unbind();
colorRect.fillProperty().unbind();
}
});
}
private void buildGui() {
FontIcon hidden = new FontIcon("mdi-eye-off");
viewButton = new ToggleButton("");
viewButton.setGraphic(hidden);
viewButton.selectedProperty().addListener((observable,oldValue, newValue) -> {
Category category = itemProperty().get();
if (newValue == true) {
category.shownColorProperty().unbind();
category.setShownColor(Color.TRANSPARENT);
}else {
category.shownColorProperty().bind(category.colorProperty());
}
});
colorRect = new Rectangle(30,30);
name = new Label();
name.setMaxWidth(120);
pane = new GridPane();
count = new Label();
count.setPadding(new Insets(0,0,0,10));
ColumnConstraints nameCol = new ColumnConstraints();
nameCol.setHgrow( Priority.ALWAYS );
pane.getColumnConstraints().addAll(
new ColumnConstraints(40),
new ColumnConstraints(40),
nameCol,
new ColumnConstraints(40));
pane.addColumn(0, viewButton);
pane.addColumn(1, colorRect);
pane.addColumn(2, name );
pane.addColumn(3, count);
this.setText(null);
name.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getClickCount()==2) {
//launch the category editor TODO
}
}
});
}
The problem is that the code below is not triggered when I change the Category value of a CategoryProperty within the MapProperty of a Seat.
//Bind number of seats assigned to this category
LongBinding categorySeatNumProperty = Bindings.createLongBinding(() ->
seatList.stream().filter(seat -> seat.getCategory(newValue.getLayer()).equals(newValue)).count(), seatList);
count.textProperty().bind(categorySeatNumProperty.asString());
}
Any advice on how to achieve this?
===== Clarifications following James_D comment ====
1) About the model: I have actually thought and hesitated quite a bit about this. I want to allocate categories to seats in concert halls, and do this on multiple "layers/levels". Say for example a price "layer" where I could have four price tag categories, and "selling company" layer where I would have 3 companies, etc... In order to model this in my Seat class I have a Map<Layer, Category> which looks like a good choice as a seat should only be assigned to one unique category per layer. Then my Project class keeps track of Layers and their respective Categories, which is not really needed but handy to keep their user-specified display order.
2) Thank you for spotting that bug in the CategoryCell! The order of if (oldValue != null) and if (newValue != null) should indeed be reversed.
3) Now what I need to answer my initial question is a way to trigger a notification when the categoryProperty in the Map of the Seat class is modified.
Actually, just refreshing the listview whenever I make a change to my Map solves the issue, but it kinds of defeat the purpose of having a Observable property...
Answering myself now that I understand a little more.
1) How to bind to a property within a ObservableMap?
By using the valueAt() method of a MapProperty.
Instead of using ObservableMap<Layer, ObjectProperty<Category>>, use
MapProperty<Layer, Category>.
2) How to trigger a notification when the objectProperty in the ObservableMap is modified?
Since we are now using a MapProperty where the value is the object and not the property wrapping it, we can just use the addListener() method of the MapProperty.

JavaFX focus on tab's content upon switch

I have a TabPane with a TextArea inside each of its Tabs.
What I want to achieve is when switching tabs, the textArea get focused.
I tried with a listener but it doesn't seem to work :
#FXML
public void initialize() {
for(Tab tab : tabPane.getTabs())
{
tab.setOnSelectionChanged(event->
{
if(tab.isSelected())
{
System.out.println(tab.getText());
TextArea ta = (TextArea)((AnchorPane)tab.getContent()).getChildren().get(0);
ta.requestFocus();
}
});
}
}
When I switch tabs, the output shows the active tab title but it stays focused, how can I focus on the TextArea after switching?
Thanks!
While it's not unusual that node.requestFocus() doesn't focus the node as expected (with the usual slightly smelly way around of wrapping it into Platform.runlater()) I'm interested why exactly it doesn't work in this context.
Turned out that one technical reason is that at the time of getting notified by any of the selection properties (selectedItem/-Index, isSelected) the node is not yet in a visible parent hierarchy - so it can't be a valid focus target. To see, add a println to the onSelected handler:
Node tabContent = tab.getContent();
if (tab.isSelected() && tab.getContent() != null && tab.getContent().getParent() != null ) {
System.out.println("onSelection " + tab.getText()
+ tabContent.getParent().isVisible());
}
That is due to skin's layout/management of tabs: the content of each is wrapped into a specialized StackPane (TabContentRegion), all these are stacked on top of each other with only the selected with its visibility property true.
So a first approximation for a solution is to register a listener to the visibility property of that container: when changed to true, its children should be eligable as focus targets. Which in fact they are .. just .. the TabPaneBehavior is interfering by forcing the focus onto the tabPane itself whenever selection is changed by user interaction (both by clicking the tab header and using ctrl-tab)
// unconditionally by mouse
new MouseMapping(MouseEvent.MOUSE_PRESSED, e -> getNode().requestFocus())
// method called by keyMappings that move the selection
private void moveSelection(int startIndex, int delta) {
final TabPane tabPane = getNode();
if (tabPane.getTabs().isEmpty()) return;
int tabIndex = findValidTab(startIndex, delta);
if (tabIndex > -1) {
final SelectionModel<Tab> selectionModel = tabPane.getSelectionModel();
selectionModel.select(tabIndex);
}
tabPane.requestFocus();
}
Next round: let the tabPane pass-on the focus whenever it gets focused during selection change. One sentence posing two stumble stones:
there is no public api to support transfer focus, it must be hacked around, f.i. by manually firing a TAB
during selection change needs state logic to decide its start and end
In all, looks like a task for a custom skin which is outlined (beware: not formally tested!) in the example below (it's for fx11, fx8 might be similar but requires to access internal classes because skins are not yet public)
public class TabPaneFocusOnSelectionSO extends Application {
/**
* Custom skin that tries to focus the first child of selected tab when
* selection changed.
*
*/
public static class MyTabPaneSkin extends TabPaneSkin {
private boolean selecting = true;
/**
* #param control
*/
public MyTabPaneSkin(TabPane control) {
super(control);
// TBD: dynamic update on changing tabs at runtime
addTabContentVisibilityListener(getChildren());
registerChangeListener(control.focusedProperty(), this::focusChanged);
registerChangeListener(control.getSelectionModel().selectedItemProperty(), e -> {
selecting = true;
});
}
/**
* Callback from listener to skinnable's focusedProperty.
*
* #param focusedProperty the property that's changed
*/
protected void focusChanged(ObservableValue focusedProperty) {
if (getSkinnable().isFocused() && selecting) {
transferFocus();
selecting = false;
}
}
/**
* Callback from listener to tab visibility.
*
* #param visibleProperty the property that's changed
*/
protected void tabVisibilityChanged(ObservableValue visibleProperty) {
BooleanProperty b = (BooleanProperty) visibleProperty;
if (b.get()) {
transferFocus();
}
}
/**
* No public api to transfer focus "away" from any node, hack by firing
* a TAB key on the TabPane.
*/
protected void transferFocus() {
final KeyEvent tabEvent = new KeyEvent(KeyEvent.KEY_PRESSED, "", "",
KeyCode.TAB, false, false, false, false);
Event.fireEvent(getSkinnable(), tabEvent);
}
/**
* Register the visibilityListener to each child in the given list that
* is a TabContentArea.
*
*/
protected void addTabContentVisibilityListener(List<? extends Node> children) {
children.forEach(node -> {
if (node.getStyleClass().contains("tab-content-area")) {
registerChangeListener(node.visibleProperty(), this::tabVisibilityChanged);
}
});
}
}
private TabPane tabPane;
private Parent createContent() {
tabPane = new TabPane() {
#Override
protected Skin<?> createDefaultSkin() {
return new MyTabPaneSkin(this);
}
};
for (int i = 0; i < 3; i++) {
VBox tabContent = new VBox();
tabContent.getChildren().addAll(new Button("dummy " +i), new TextField("just a field " + i));
Tab tab = new Tab("Tab " + i, tabContent);
tabPane.getTabs().add(tab);
}
tabPane.getTabs().add(new Tab("no content"));
tabPane.getTabs().add(new Tab("not focusable content", new Label("me!")));
BorderPane content = new BorderPane(tabPane);
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.setTitle(" TabPane with custom skin ");
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

Context menu on TableRow<Object> does not show up on first right click

So I followed this example on using context menu with TableViews from here. I noticed that using this code
row.contextMenuProperty().bind(Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu)null));
does not show up on first right click on a row with values. I need to right click on that row again for the context menu to show up. I also tried this code(which is my first approach, but not using it anymore because I've read somewhere that that guide is the best/good practice for anything related about context menu and tableview), and it displays the context menu immediately
if (row.getItem() != null) {
rowMenu.show(row, event.getScreenX(), event.getScreenY());
}
else {
// do nothing
}
but my problem with this code is it throws a NullPointerException whenever i try to right click on a row that has no data.
What could I possibly do to prevent NullPointerException while having the context menu show up immediately after a right click? In my code, I also have a code that a certain menu item in the context menu will be disabled based on the property of the myObject binded to row, that's why i need the context menu to pop up right away.
I noticed this too with the first block of code. Even if the property of myObject has already changed, it still has a menu item enabled/disabled unless I right click on that row again. I hope that you could help me. Thank you!
Here is a MCVE:
public class MCVE_TableView extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane myBorderPane = new BorderPane();
TableView<People> myTable = new TableView<>();
TableColumn<People, String> nameColumn = new TableColumn<>();
TableColumn<People, Integer> ageColumn = new TableColumn<>();
ContextMenu rowMenu = new ContextMenu();
ObservableList<People> peopleList = FXCollections.observableArrayList();
peopleList.add(new People("John Doe", 23));
nameColumn.setMinWidth(100);
nameColumn.setCellValueFactory(
new PropertyValueFactory<>("Name"));
ageColumn.setMinWidth(100);
ageColumn.setCellValueFactory(
new PropertyValueFactory<>("Age"));
myTable.setItems(peopleList);
myTable.getColumns().addAll(nameColumn, ageColumn);
myTable.setRowFactory(tv -> {
TableRow<People> row = new TableRow<>();
row.setOnContextMenuRequested((event) -> {
People selectedRow = row.getItem();
rowMenu.getItems().clear();
MenuItem sampleMenuItem = new MenuItem("Sample Button");
if (selectedRow != null) {
if (selectedRow.getAge() > 100) {
sampleMenuItem.setDisable(true);
}
rowMenu.getItems().add(sampleMenuItem);
}
else {
event.consume();
}
/*if (row.getItem() != null) { // this block comment displays the context menu instantly
rowMenu.show(row, event.getScreenX(), event.getScreenY());
}
else {
// do nothing
}*/
// this requires the row to be right clicked 2 times before displaying the context menu
row.contextMenuProperty().bind(Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu)null));
});
return row;
});
myBorderPane.setCenter(myTable);
Scene scene = new Scene(myBorderPane, 500, 500);
primaryStage.setTitle("MCVE");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main (String[] args) {
launch(args);
}
}
Here is the People Class
public class People {
SimpleStringProperty name;
SimpleIntegerProperty age;
public People(String name, int age) {
this.name = new SimpleStringProperty(name);
this.age = new SimpleIntegerProperty(age);
}
public SimpleStringProperty NameProperty() {
return this.name;
}
public SimpleIntegerProperty AgeProperty() {
return this.age;
}
public String getName() {
return this.name.get();
}
public int getAge() {
return this.age.get();
}
}
Edit: MCVE added
Edit2: Updated the MCVE. Still requires to be right-clicked twice before the contextMenu pops up
Below's a code snippet as a quick demonstration of how-to/where-to instantiate and configure a per-row ContextMenu. It
creates a ContextMenu/MenuItem for each TableRow at the row's instantiation time
creates a conditional binding that binds the menu to the row's contextMenuProperty if not empty (just the same as you did)
configures the contextMenu in an onShowing handler, depending on the current item (note: no need for a guard against null, because the conditional binding will implicitly guarantee to not show the the menu in that case)
The snippet:
myTable.setRowFactory(tv -> {
TableRow<People> row = new TableRow<>() {
ContextMenu rowMenu = new ContextMenu();
MenuItem sampleMenuItem = new MenuItem("Sample Button");
{
rowMenu.getItems().addAll(sampleMenuItem);
contextMenuProperty()
.bind(Bindings
.when(Bindings.isNotNull(itemProperty()))
.then(rowMenu).otherwise((ContextMenu) null));
rowMenu.setOnShowing(e -> {
People selectedRow = getItem();
sampleMenuItem.setDisable(selectedRow.getAge() > 100);
});
}
};
return row;
});

JavaFX set action to button in another class

I created listview in my application and its item has a delete button:
public Cell() {
super();
this.delete = new Button("delete");
// I have also image and other labels in this cell
this.hBox.getChildren().addAll(productImage, amountLabel, priceLabel, delete);
HBox.setHgrow(pane, Priority.ALWAYS);
delete.setOnAction(event -> getListView().getItems().remove(getItem()));
}
But in my main controller window when I click this button amount field and amountLabel must be changed which are my main controller class:
#FXML
void addToShopCart(ActionEvent event) {
selectedPart.setAmount(amount);
selectedPart.setSumma(amount*selectedPart.getPrice());
shopListView.getItems().add(selectedPart);
summa += amount*selectedPart.getPrice();
totalPriceLabel.setText(summa + "$");
} // this is when I add an item to the list
Now I don't know how to subtract amount from summa that was added. For this I tried return button like this:
Cell cell;
#FXML
void initialize() {
cell = new Cell();
shopListView.setCellFactory(param -> cell));
Button button = cell.getDeleteButton();
button.setOnAction(...//some action)
}
But it doesn't help. I tried to cut the code.

Resources