Get more than two inputs in a JavaFX Dialog - java

I trying to build the text dialog where the user enter the event name, event size, and the selected the venue.
My problem is in how can I gather the inputs; here what I did so far:
eventName = new TextField();
eventSize = new TextField();
ObservableList<Venue> options =
FXCollections.observableArrayList(model.getVenuesList());
VeunueList = new ComboBox<Venue>(options);
I create a class that encapsulate all my inputs:
public class MyResult {
String eventname;
String eventsize;
Venue venue;
}
I define the variable to be object of class Myresult:
private Dialog<MyResult> dialog ;
private Optional<MyResult> EventInput;
The problem is how to write return statement in the result converter; it gives me error:
dialog.setResultConverter(dialogButton -> {
if (dialogButton == submit) {
return new MyResult(eventName.getText(),eventSize.getText(),VeunueList.getValue())
}
return null;
});
EventInput = dialog.showAndWait();

It's not clear where your fragment goes awry, but getting the types correct for a call to setResultConverter() is sometimes problematical. The example below illustrates a Dialog that collects inputs from a TextField, DatePicker and ComboBox<Venue>. In the ComboBox<Venue>, the choice of Venue comes from an enum, and the corresponding ComboBox model is constructed using the enum's implicit values() method. The resultConverter property's Callback returns a new instance of Results having the current values of the various view components. The Optional<Results> shows those values ifPresent(). Some related examples may be found here and in the tutorial, JavaFX improvements in Java SE 8u40.
Console: Name 2017-05-24 Elsewhere
import java.time.LocalDate;
import java.util.Optional;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* #see http://stackoverflow.com/q/44147595/230513
* #see http://www.javaworld.com/article/2991463/
*/
public class DialogTest extends Application {
#Override
public void start(Stage primaryStage) {
Dialog<Results> dialog = new Dialog<>();
dialog.setTitle("Dialog Test");
dialog.setHeaderText("Please specify…");
DialogPane dialogPane = dialog.getDialogPane();
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
TextField textField = new TextField("Name");
DatePicker datePicker = new DatePicker(LocalDate.now());
ObservableList<Venue> options =
FXCollections.observableArrayList(Venue.values());
ComboBox<Venue> comboBox = new ComboBox<>(options);
comboBox.getSelectionModel().selectFirst();
dialogPane.setContent(new VBox(8, textField, datePicker, comboBox));
Platform.runLater(textField::requestFocus);
dialog.setResultConverter((ButtonType button) -> {
if (button == ButtonType.OK) {
return new Results(textField.getText(),
datePicker.getValue(), comboBox.getValue());
}
return null;
});
Optional<Results> optionalResult = dialog.showAndWait();
optionalResult.ifPresent((Results results) -> {
System.out.println(
results.text + " " + results.date + " " + results.venue);
});
}
private static enum Venue {Here, There, Elsewhere}
private static class Results {
String text;
LocalDate date;
Venue venue;
public Results(String name, LocalDate date, Venue venue) {
this.text = name;
this.date = date;
this.venue = venue;
}
}
public static void main(String[] args) {
launch(args);
}
}

Related

JavaFX TableView with CheckBoxes: retrieve the rows whose checkboxes are checked

I've been searching for a while, but all I found seems very old and can't get it to work and I'm very confused.
I have a tableview with a checkbox in a column header (select all) and another checkbox for each row (select row). What I am trying to achieve is to get all the rows whose checkboxes are checked to perform an action.
Here's what it looks like:
And here's the code in my controller:
package com.comparador.controller;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.comparador.ComparadorPreciosApplication;
import com.comparador.entity.Commerce;
import com.comparador.entity.Items;
import com.comparador.entity.ShoppingListPrices;
import com.comparador.repository.CommerceRepository;
import com.comparador.repository.ProductRepository;
import com.comparador.service.ShoppingService;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import javafx.util.converter.IntegerStringConverter;
#Component
public class ShoppingController implements Initializable {
// #Autowired
// #Qualifier("lblTitulo")
private String titulo = "Productos";
#Autowired
private ProductRepository productRepository;
#Autowired
private CommerceRepository commerceRepository;
#Autowired
private ShoppingService shoppingService;
#FXML
private Label lblTitulo;
#FXML
private Button btBack;
#FXML
private TableView<Items> tvProducts;
#FXML
private TableColumn<Items, CheckBox> colSelected; //THE CHECKBOX COLUMN
#FXML
private TableColumn<Items, String> colName;
#FXML
private TableColumn<Items, Integer> colAmount;
#FXML
private TableView<ShoppingListPrices> tvTotalPrices;
#FXML
private TableColumn<ShoppingListPrices, String> colCommerce;
#FXML
private TableColumn<ShoppingListPrices, Double> colTotal;
private CheckBox selectAll;
List<ShoppingListPrices> shoppingList = new ArrayList<>();
#Override
public void initialize(URL location, ResourceBundle resources) {
colName.setCellValueFactory(new PropertyValueFactory<>("name"));
colAmount.setCellValueFactory(new PropertyValueFactory<>("amount"));
colAmount.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
// colSelected.setCellFactory(CheckBoxTableCell.forTableColumn(colSelected));
// colSelected.setCellValueFactory(cellData -> new ReadOnlyBooleanWrapper(cellData.getValue().getChecked()));
colSelected.setCellValueFactory(new PropertyValueFactory<>("selected"));
colCommerce.setCellValueFactory(new PropertyValueFactory<>("commerceName"));
colTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
lblTitulo.setText(titulo);
tvProducts.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
reloadTableViewProducts();
selectAll = new CheckBox();
selectAll.setOnAction(event -> {
event.consume();
tvProducts.getItems().forEach(item -> {
item.getSelected().setSelected(selectAll.isSelected());
});
});
setShoppingList();
colSelected.setGraphic(selectAll);
}
#FXML
public void editAmount(CellEditEvent<Items, Integer> event) {
Items item = event.getRowValue();
if(event.getTableColumn().getText().equals("Cantidad")) {
item.setAmount(event.getNewValue());
}
setShoppingList();
}
/*
* CLICKING ON A CHECKBOX SHOULD CALL THIS METHOD AND ADD THE ROW TO "selectedItems"
*/
#FXML
public void setShoppingList() {
List<Items> selectedItems = new ArrayList<>();
//Before trying this I was selecting each row by Ctrl + Clicking on it
// List<Items> selectedItems = tvProducts.getSelectionModel().getSelectedItems();
//This didn't seem to work
// List<ShoppingListItems> selectedItems = tvProducts.getItems().filtered(x->x.getSelected() == true);
List<Commerce> commerces = commerceRepository.findByNameContaining("");
ShoppingListPrices pricesMixingCommerces = shoppingService.getCheapestShoppingList(commerces, selectedItems);
List<ShoppingListPrices> pricesByCommerce = shoppingService.getShoppingListsPerCommerce(commerces, selectedItems);
shoppingList = new ArrayList<>();
shoppingList.add(pricesMixingCommerces);
shoppingList.addAll(pricesByCommerce);
ObservableList<ShoppingListPrices> resultOL = FXCollections.observableArrayList();
resultOL.addAll(shoppingList);
tvTotalPrices.setItems(resultOL);
}
#FXML
public void openShoppingList() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/shoppingList.fxml"));
ShoppingListController shoppingListController = new ShoppingListController();
loader.setControllerFactory(ComparadorPreciosApplication.applicationContext::getBean);
loader.setController(shoppingListController);
shoppingListController.setup(tvTotalPrices.getSelectionModel().getSelectedItem());
try {
Scene scene = new Scene(loader.load(), 800, 400, true, SceneAntialiasing.BALANCED);
Stage stage = new Stage();//(Stage) btBack.getScene().getWindow();
stage.setUserData(tvTotalPrices.getSelectionModel().getSelectedItem());
stage.setScene(scene);
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
#FXML
public void goBack() {
FXMLLoader loader = new FXMLLoader(ComparadorPreciosApplication.class.getResource("/index.fxml"));
loader.setControllerFactory(ComparadorPreciosApplication.applicationContext::getBean);
try {
Scene scene = new Scene(loader.load(), 800, 800, false, SceneAntialiasing.BALANCED);
Stage stage = (Stage) btBack.getScene().getWindow();
stage.setScene(scene);
stage.show();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void reloadTableViewProducts() {
List<String> productNames = productRepository.findOnProductPerName("");
List<Items> items = new ArrayList<>();
for(String name : productNames) {
//items.add(new Items(new SimpleBooleanProperty(false), name, 1));
Items item = new Items((CheckBox) new CheckBox(), name, 1);
item.getSelected().setSelected(false);
items.add(item);
}
ObservableList<Items> itemsOL = FXCollections.observableArrayList();
itemsOL.addAll(items);
tvProducts.setItems(itemsOL);
}
}
Your Items class should not reference any UI objects, including CheckBox. The model should ideally not even know the view exists. If you plan on having Items track if it's selected itself, then it should expose a BooleanProperty representing this state. With a properly configured table and column, the check box associated with an item and the item's selected property will remain synchronized. And since the items of the table keep track of their own selected state, getting all the selected items is relatively straightforward. Simply iterate/stream the items and grab all the selected ones.
Here's an example using CheckBoxTableCell:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
var table = new TableView<Item>();
table.setEditable(true);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
for (int i = 0; i < 50; i++) {
table.getItems().add(new Item("Item #" + (i + 1)));
}
var selectedCol = new TableColumn<Item, Boolean>("Selected");
// configure cell factory to use a cell implementation that displays a CheckBox
selectedCol.setCellFactory(CheckBoxTableCell.forTableColumn(selectedCol));
// link CheckBox and model selected property
selectedCol.setCellValueFactory(data -> data.getValue().selectedProperty());
table.getColumns().add(selectedCol);
var nameCol = new TableColumn<Item, String>("Name");
nameCol.setCellValueFactory(data -> data.getValue().nameProperty());
table.getColumns().add(nameCol);
var button = new Button("Print checked items");
button.setOnAction(e -> {
// filter for selected items and collect into a list
var checkedItems = table.getItems().stream().filter(Item::isSelected).toList();
// log selected items
System.out.printf("There are %,d checked items:%n", checkedItems.size());
for (var item : checkedItems) {
System.out.println(" " + item);
}
});
var root = new BorderPane();
root.setTop(button);
root.setCenter(table);
root.setPadding(new Insets(10));
BorderPane.setMargin(button, new Insets(0, 0, 10, 0));
BorderPane.setAlignment(button, Pos.CENTER_RIGHT);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
}
public static class Item {
private final StringProperty name = new SimpleStringProperty(this, "name");
public final void setName(String name) { this.name.set(name); }
public final String getName() { return name.get(); }
public final StringProperty nameProperty() { return name; }
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
public final void setSelected(boolean selected) { this.selected.set(selected); }
public final boolean isSelected() { return selected.get(); }
public final BooleanProperty selectedProperty() { return selected; }
public Item() {}
public Item(String name) {
setName(name);
}
#Override
public String toString() {
return String.format("Item(name=%s, selected=%s)", getName(), isSelected());
}
}
}
Note that TableView has a selection model. That is not the same thing. It's used for the selection of rows or cells of the table (and thus works best on a per-table basis). You, however, want to be able to "check" items, and that requires keeping track of that state differently--an item's row could be selected while the item is not checked, and vice versa.
And note I recommend that any model class used with TableView expose JavaFX properties (like the Item class in the example above). It makes it much easier to work with TableView. But that could interfere with other parts of your code (e.g., Spring). In that case, you could do one of three things:
Create a simple adapter class that holds a reference to the "real" object and provides a BooleanProperty. This adapter class would only be used for the TableView.
Create a more complex adapter class that mirrors the "real" class in content, but exposes the properties as JavaFX properties (e.g., BooleanProperty, StringProperty, etc.). Map between them as you cross layer boundaries in your application.
In the controller, or wherever you have the TableView, keep the selected state external to the model class. For instance, you could use a Map<Item, BooleanProperty>.
I probably would only use this approach as a last resort, if ever.

JavaFX: Make Chips Editable in JFXChipView

I want to ask if it is possible to make a chip in JFXChipView editable once it has been set.
You can create your own JFXChip and implement a behavior to enable editing. First, you need to have an editable label. I looked up online and I found this post: JavaFX custom control - editable label. Then, you can extend JFXChip to use that EditableLabel:
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXChip;
import com.jfoenix.controls.JFXChipView;
import com.jfoenix.svg.SVGGlyph;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.scene.layout.HBox;
public class EditableChip<T> extends JFXChip<Property<T>> {
protected final HBox root;
public EditableChip(JFXChipView<Property<T>> view, Property<T> item) {
super(view, item);
JFXButton closeButton = new JFXButton(null, new SVGGlyph());
closeButton.getStyleClass().add("close-button");
closeButton.setOnAction(event -> {
view.getChips().remove(item);
event.consume();
});
// Create the label with an initial value from the item
String initialValue = view.getConverter().toString(item);
EditableLabel label = new EditableLabel(initialValue);
label.setMaxWidth(100);
// Bind the item to the text in the label
item.bind(Bindings.createObjectBinding(() -> view.getConverter().fromString(label.getText()).getValue(), label.textProperty()));
root = new HBox(label, closeButton);
getChildren().setAll(root);
}
}
Note: I am using Property<T> instead of using the desired class T because JFXChipView stores the item the first time you add it. And in that case, you're going to get the values as you entered them the first time when calling JFXChipView#getChips().
Sample application:
import com.jfoenix.controls.JFXChipView;
import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class EditableChipViewApp extends Application {
#Override
public void start(Stage primaryStage) {
JFXChipView<Property<String>> chipView = new JFXChipView<>();
chipView.setChipFactory(EditableChip::new);
chipView.setConverter(new StringConverter<Property<String>>() {
#Override
public String toString(Property<String> object) {
return object == null ? null : object.getValue();
}
#Override
public Property<String> fromString(String string) {
return new SimpleStringProperty(string);
}
});
VBox container = new VBox(chipView);
Scene scene = new Scene(container, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Result:
This is how you get the actual values of the chips:
List<String> chipsValues = chipView.getChips().stream().map(Property::getValue).collect(Collectors.toList());

How to increase the number of possible selections within a ToggleGroup (e.g. RadioButtons)?

I am somewhat new to programming and new to OOP (2nd Java project over all right now) and would love any hints or help.
I am currently working on a character creation program for my very own pen&paper game. I am using JavaFX (without FXML and thus without SceneBuilder) for the GUI part. I am working with Eclipse Neon on JDK1.8.0_131.
Here is my issue:
tl;dr: How to increase the number of possible selections within a ToggleGroup?
I am about to create a list of options for the user to choose from. The list consist of about 30 different advantages he or she can choose to improve their character. The allowed maximum of chosen options depends on the character and varies around 5. I already implemented an array of pairs (I know about HashMaps), where each entry is a pair consisting of the advantage's name and an integer representing its costs (they vary in their values, so in their costs).
The list itself should now be implemented via
ScrollPane scrollAdv = new ScrollPane();
VBox vBoxAdv = new VBox();
scrollAdv.setContent(vBoxAdv);
Pair<String, Integer>[] listAdv = info.getAdvantages();
for (int i = 0; i < listAdv.length; i++) {
String name = listAdv[i].getKey(); // delivers the 1st entry of a pair
int costs = listAdv[i].getValue(); // delivers the 2nd entry of a pair
ToggleButton toggleButton = new ToggleButton();
toggleButton.setUserData(name);
toggleButton.setText(name + " (" + costs + ")");
vBoxAdv.getChildren().add(toggleButton);
}
Note that I don't care too much about ToggleButtons and they could easily be replaced with RadioButtons. Both work the same way, if I understood the documentation correctly. They both use ToggleGroup to make sure, only one option is selected.
So while you probably guessed so by now, what I want to do is give the user the possibility to chose more than one option at once. I do not want to make it so the user has to chose one option after the other, while resetting the list in between.
Thanks for reading and thanks in advance for any help or hints.
edit: I could always just add a counter which refreshes whenever one option is selected or deselected and blocks any selection if it's < 1, but I thought that there should be a better solution, e.g. increase the built-in limit of 1 which ToggleGroup seems to be using.
One way would be to disable the remaining toggles when the limit is reached. Here's a ToggleSet class that does that:
import java.util.ArrayList;
import java.util.List;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Node ;
import javafx.scene.control.Toggle;
public class ToggleSet<T extends Node & Toggle> {
private final ObservableList<T> toggles = FXCollections.observableArrayList(t -> new Observable[] {t.selectedProperty()});
private final FilteredList<T> selectedToggles = toggles.filtered(t -> ((Toggle)t).isSelected());
private final IntegerProperty maximumSelectable = new SimpleIntegerProperty(0);
private final IntegerBinding numSelected = Bindings.size(selectedToggles);
public ToggleSet(int maximumSelectable) {
this.maximumSelectable.addListener((obs, oldMax, newMax) -> {
if (newMax.intValue() < numSelected.get()) {
List<Toggle> togglesToClear = new ArrayList<>(selectedToggles.subList(0, numSelected.get() - newMax.intValue()));
togglesToClear.forEach(t -> t.setSelected(false));
}
});
setMaximumSelectable(maximumSelectable);
}
public ToggleSet() {
this(0);
}
public ObservableList<T> getSelectedToggles() {
return FXCollections.unmodifiableObservableList(selectedToggles) ;
}
public IntegerProperty maximumSelectableProperty() {
return maximumSelectable ;
}
public final int getMaximumSelectable() {
return maximumSelectableProperty().get();
}
public final void setMaximumSelectable(int maximumSelectable) {
maximumSelectableProperty().set(maximumSelectable);
}
public void addToggle(T toggle) {
if (numSelected.get() >= getMaximumSelectable()) {
toggle.setSelected(false);
}
toggles.add(toggle);
toggle.disableProperty().bind(toggle.selectedProperty().not().and(numSelected.greaterThanOrEqualTo(maximumSelectable)));
}
public void removeToggle(T toggle) {
toggles.remove(toggle);
toggle.disableProperty().unbind();
}
}
Here's an example testing it:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Spinner;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class ToggleSetTest extends Application {
#Override
public void start(Stage primaryStage) {
ToggleSet<ToggleButton> toggleSet = new ToggleSet<>(5);
GridPane grid = new GridPane() ;
Spinner<Integer> maxSelectedSpinner = new Spinner<>(0, 20, 5);
maxSelectedSpinner.getValueFactory().valueProperty().bindBidirectional(toggleSet.maximumSelectableProperty().asObject());
grid.add(new HBox(2, new Label("Maximum selected"), maxSelectedSpinner), 0, 0, 2, 1);
grid.addRow(1, new Label("Selection"), new Label("Include in set"));
for (int i = 1; i <= 20 ; i++) {
RadioButton button = new RadioButton("Button "+i);
CheckBox checkBox = new CheckBox();
checkBox.selectedProperty().addListener((obs, wasChecked, isNowChecked) -> {
if (isNowChecked) {
toggleSet.addToggle(button);
} else {
toggleSet.removeToggle(button);
}
});
checkBox.setSelected(true);
grid.addRow(i + 1, button, checkBox);
}
grid.setPadding(new Insets(10));
grid.setHgap(5);
grid.setVgap(2);
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
If you want the same behavior as the ToggleGroup, where the previous selection becomes unselected, it's a little trickier, but the following should work:
import java.util.ArrayList;
import java.util.List;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node ;
import javafx.scene.control.Toggle;
public class ToggleSet<T extends Node & Toggle> {
private final ObservableList<T> toggles = FXCollections.observableArrayList(t -> new Observable[] {t.selectedProperty()});
private final ObservableList<T> selectedToggles = FXCollections.observableArrayList();
private final IntegerProperty maximumSelectable = new SimpleIntegerProperty(0);
private final ChangeListener<Boolean> toggleListener = (obs, wasSelected, isNowSelected) -> {
#SuppressWarnings("unchecked")
T toggle = (T) ((Property<?>)obs).getBean();
if (isNowSelected) {
selectedToggles.add(toggle);
ensureWithinMax();
} else {
selectedToggles.remove(toggle);
}
};
public ToggleSet(int maximumSelectable) {
this.maximumSelectable.addListener((obs, oldMax, newMax) -> ensureWithinMax());
setMaximumSelectable(maximumSelectable);
}
private void ensureWithinMax() {
if (this.maximumSelectable.get() < selectedToggles.size()) {
List<Toggle> togglesToClear = new ArrayList<>(selectedToggles.subList(0, selectedToggles.size() - this.maximumSelectable.get()));
togglesToClear.forEach(t -> t.setSelected(false));
}
}
public ToggleSet() {
this(0);
}
public ObservableList<T> getSelectedToggles() {
return FXCollections.unmodifiableObservableList(selectedToggles) ;
}
public IntegerProperty maximumSelectableProperty() {
return maximumSelectable ;
}
public final int getMaximumSelectable() {
return maximumSelectableProperty().get();
}
public final void setMaximumSelectable(int maximumSelectable) {
maximumSelectableProperty().set(maximumSelectable);
}
public void addToggle(T toggle) {
if (toggle.isSelected()) {
selectedToggles.add(toggle);
ensureWithinMax();
}
toggle.selectedProperty().addListener(toggleListener);
toggles.add(toggle);
}
public void removeToggle(T toggle) {
toggle.selectedProperty().removeListener(toggleListener);
toggles.remove(toggle);
}
}
(Use the same test code.)

JavaFX TreeView of multiple object types? (and more)

I currently have the following object data structure:
Item
String name
ArrayList of information
Character
String name
Collection of Item
Account
String name
Collection of Character (up to 8 max)
I want to make a TreeView that looks like the following:
Root(invisible)
======Jake(Account)
============JakesChar(Character)
==================Amazing Sword(Item)
==================Broken Bow(Item)
==================Junk Metal(Item)
======Mark(Account)
============myChar(Character)
==================Godly Axe(Item)
======FreshAcc(Account)
======MarksAltAcc(Account)
============IllLvlThisIPromise(Character)
======Jeffrey(Account)
============Jeff(Character)
==================Super Gun(Item)
==================Better Super Gun(Item)
==================Super Gun Scope(Item)
I made all those names up and such, obviously the real implementation would be a lot more complex. How can this be done? The TreeItem requires each TreeItem to be the same type as its' parent.
The ONLY solution I have is to do the following:
public class ObjectPointer
{
Object pointer;
String name;
}
My TreeView would be of type ObjectPointer and on each row I would cast the ObjectPointer to Account, Character, or Item. This is AWFUL but I think it would work.
Sub Questions:
How do I get TreeItem(s) to detect setOnMouseHover events?
How do I get TreeItem(s) to not use the toString method of their type and instead a custom way of displaying the String property that they need?
How do I get the TreeItem(s) to display colored text in the GUI instead of plain text?
Thank you!
If you look at your model and think generically, all the classes have a degree of similarity, which you could factor out into a superclass:
package model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public abstract class GameObject<T extends GameObject<?>> {
public GameObject(String name) {
setName(name);
}
private final StringProperty name = new SimpleStringProperty();
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
private final ObservableList<T> items = FXCollections.observableArrayList();
public ObservableList<T> getItems() {
return items ;
}
public abstract void createAndAddChild(String name);
}
The type parameter T here represents the type of "child" objects. So your Account class (whose child type is GameCharacter - don't name classes the same as anything in java.lang, btw...) looks like
package model;
public class Account extends GameObject<GameCharacter> {
public Account(String name) {
super(name);
}
#Override
public void createAndAddChild(String name) {
getItems().add(new GameCharacter(name));
}
}
and similarly all the way down the hierarchy. I'd define an Information class (even though it just has a name) to make everything fit the structure, so:
package model;
public class Item extends GameObject<Information> {
public Item(String name) {
super(name);
}
#Override
public void createAndAddChild(String name) {
getItems().add(new Information(name));
}
}
and, since Information has no children, its child list is just an empty list:
package model;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Information extends GameObject<GameObject<?>> {
public Information(String name) {
super(name);
}
#Override
public ObservableList<GameObject<?>> getItems() {
return FXCollections.emptyObservableList();
}
#Override
public void createAndAddChild(String name) {
throw new IllegalStateException("Information has no child items");
}
}
Now every item in your tree is a GameObject<?>, so you can basically build a TreeView<GameObject<?>>. The tricky part is that your tree items need to reflect the structure already built in the model. Since you have observable lists there, you can do this with listeners on the lists.
You can use a cell factory on the tree to customize the appearance of the cells displaying the TreeItems. If you want a different appearance for each type of item, I'd recommend defining the styles in an external CSS class, and setting a CSS PseudoClass on the cell corresponding to the type of item. If you use some naming convention (I have that the pseudo-class name is the lower case version of the class name), it can be quite slick to do that. Here's a fairly simple example:
package ui;
import static java.util.stream.Collectors.toList;
import java.util.Arrays;
import java.util.List;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item;
public class Tree {
private final TreeView<GameObject<?>> treeView ;
private final List<Class<? extends GameObject<?>>> itemTypes = Arrays.asList(
Account.class, GameCharacter.class, Item.class, Information.class
);
public Tree(ObservableList<Account> accounts) {
treeView = new TreeView<>();
GameObject<?> root = new GameObject<Account>("") {
#Override
public ObservableList<Account> getItems() {
return accounts ;
}
#Override
public void createAndAddChild(String name) {
getItems().add(new Account(name));
}
};
TreeItem<GameObject<?>> treeRoot = createItem(root);
treeView.setRoot(treeRoot);
treeView.setShowRoot(false);
treeView.setCellFactory(tv -> {
TreeCell<GameObject<?>> cell = new TreeCell<GameObject<?>>() {
#Override
protected void updateItem(GameObject<?> item, boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if (empty) {
setText(null);
itemTypes.stream().map(Tree.this::asPseudoClass)
.forEach(pc -> pseudoClassStateChanged(pc, false));
} else {
textProperty().bind(item.nameProperty());
PseudoClass itemPC = asPseudoClass(item.getClass());
itemTypes.stream().map(Tree.this::asPseudoClass)
.forEach(pc -> pseudoClassStateChanged(pc, itemPC.equals(pc)));
}
}
};
cell.hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
if (isNowHovered && (! cell.isEmpty())) {
System.out.println("Mouse hover on "+cell.getItem().getName());
}
});
return cell ;
}
}
public TreeView<GameObject<?>> getTreeView() {
return treeView ;
}
private TreeItem<GameObject<?>> createItem(GameObject<?> object) {
// create tree item with children from game object's list:
TreeItem<GameObject<?>> item = new TreeItem<>(object);
item.setExpanded(true);
item.getChildren().addAll(object.getItems().stream().map(this::createItem).collect(toList()));
// update tree item's children list if game object's list changes:
object.getItems().addListener((Change<? extends GameObject<?>> c) -> {
while (c.next()) {
if (c.wasAdded()) {
item.getChildren().addAll(c.getAddedSubList().stream().map(this::createItem).collect(toList()));
}
if (c.wasRemoved()) {
item.getChildren().removeIf(treeItem -> c.getRemoved().contains(treeItem.getValue()));
}
}
});
return item ;
}
private PseudoClass asPseudoClass(Class<?> clz) {
return PseudoClass.getPseudoClass(clz.getSimpleName().toLowerCase());
}
}
Quick test, which works, but note you probably need to test more of the functionality:
package application;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item;
import ui.Tree;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Tree tree = new Tree(createAccounts());
TreeView<GameObject<?>> treeView = tree.getTreeView();
TextField addField = new TextField();
Button addButton = new Button("Add");
EventHandler<ActionEvent> addHandler = e -> {
TreeItem<GameObject<?>> selected = treeView
.getSelectionModel()
.getSelectedItem();
if (selected != null) {
selected.getValue().createAndAddChild(addField.getText());
addField.clear();
}
};
addField.setOnAction(addHandler);
addButton.setOnAction(addHandler);
addButton.disableProperty().bind(Bindings.createBooleanBinding(() -> {
TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
return selected == null || selected.getValue() instanceof Information ;
}, treeView.getSelectionModel().selectedItemProperty()));
Button deleteButton = new Button("Delete");
deleteButton.setOnAction(e -> {
TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
TreeItem<GameObject<?>> parent = selected.getParent() ;
parent.getValue().getItems().remove(selected.getValue());
});
deleteButton.disableProperty().bind(treeView.getSelectionModel().selectedItemProperty().isNull());
HBox controls = new HBox(5, addField, addButton, deleteButton);
controls.setPadding(new Insets(5));
controls.setAlignment(Pos.CENTER);
BorderPane root = new BorderPane(treeView);
root.setBottom(controls);
Scene scene = new Scene(root, 600, 600);
scene.getStylesheets().add(getClass().getResource("/ui/style/style.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private ObservableList<Account> createAccounts() {
Account jake = new Account("Jake");
Account mark = new Account("Mark");
Account freshAcc = new Account("Fresh Account");
Account marksAltAcc = new Account("Mark's alternative account");
Account jeffrey = new Account("Jeffrey");
GameCharacter jakesChar = new GameCharacter("Jakes character");
Item amazingSword = new Item("Amazing Sword");
Item brokenBow = new Item("Broken Bow");
Item junkMetal = new Item("Junk Metal");
GameCharacter myChar = new GameCharacter("Me");
Item godlyAxe = new Item("Godly Axe");
GameCharacter level = new GameCharacter("I'll level this I promise");
GameCharacter jeff = new GameCharacter("Jeff");
Item superGun = new Item("Super Gun");
Item superGunScope = new Item("Super Gun Scope");
jake.getItems().add(jakesChar);
mark.getItems().add(myChar);
marksAltAcc.getItems().add(level);
jeffrey.getItems().add(jeff);
jakesChar.getItems().addAll(amazingSword, brokenBow, junkMetal);
myChar.getItems().add(godlyAxe);
jeff.getItems().addAll(superGun, superGunScope);
return FXCollections.observableArrayList(jake, mark, freshAcc, marksAltAcc, jeffrey);
}
}
and the CSS as an example:
.tree-cell, .tree-cell:hover:empty {
-fx-background-color: -fx-background ;
-fx-background: -fx-control-inner-background ;
}
.tree-cell:hover {
-fx-background-color: crimson, -fx-background ;
-fx-background-insets: 0, 1;
}
.tree-cell:account {
-fx-background: lightsalmon ;
}
.tree-cell:gamecharacter {
-fx-background: bisque ;
}
.tree-cell:item {
-fx-background: antiquewhite ;
}
.tree-cell:selected {
-fx-background: crimson ;
}

SelectedItems empty if multiple rows selected using different columns

I have a TableView in SelectionMode.MULTIPLE. Using a ListChangeListener I'm able to catch the selection of multiple rows (by pressing Shift).
However my solution only works if the items are being selected in the same column OR in the area without columns. Gif for illustration with 4 examples:
OK: Selecting 3 items using Shift in State column
OK: Selecting 4 items using Shift in Idx column
OK: Selecting 4 items using Shift starting from State column to area without columns
Error: Trying to select 4 items using Shift starting from State column to Data Item column
The problem seems to be that the SelectedItems-list is apparently empty in the last example. I'd really appreciate your help regarding this issue.
Here is my approach:
ObservableList<DataRowModel> dataRows = FXCollections.observableArrayList();
dataRows.addAll(dataSetModel.getRows());
tableDataRow.setItems(dataRows);
tableDataRowStateColumn.setCellValueFactory(f -> f.getValue().getState());
tableDataRow.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableDataRow.getSelectionModel().getSelectedItems()
.addListener((ListChangeListener.Change<? extends DataRowModel> c) -> {
while (c.next()) {
c.getRemoved().stream().forEach(remitem -> remitem.setSelected(false));
c.getAddedSubList().stream().forEach(additem -> additem.setSelected(true));
System.out.println(c.getList()); //Empty [] when selected using different columns
}
});
Just for a better understanding of my code: setSelected(...) sets a BooleanProperty on my DataRowModel which is bound to the State-Column.
Without context the reason for using this selected-property seems to be quite silly. However, there are various other fragments of code with ChangeListeners bound to the selected-property.
SSCCE ready to run:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class TableViewSample extends Application {
private TableView<DataRowModel> tableDataRow = new TableView<DataRowModel>();
private TableColumn<DataRowModel, String> tableDataRowNameColumn = new TableColumn<>("Data Item");
private TableColumn<DataRowModel, String> tableDataRowStateColumn = new TableColumn<>("State");
private final ObservableList<DataRowModel> dataRows =
FXCollections.observableArrayList(
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false),
new DataRowModel("Concinna", false)
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(500);
stage.setHeight(500);
tableDataRow.setItems(dataRows);
tableDataRowNameColumn.setCellValueFactory(f -> f.getValue().getName());
tableDataRowStateColumn.setCellValueFactory(f -> f.getValue().getState());
tableDataRow.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableDataRow.getSelectionModel().getSelectedItems()
.addListener((ListChangeListener.Change<? extends DataRowModel> c) -> {
while (c.next()) {
c.getRemoved().stream().forEach(remitem -> remitem.setSelected(false));
c.getAddedSubList().stream().forEach(additem -> additem.setSelected(true));
}
});
tableDataRow.getColumns().addAll(tableDataRowNameColumn, tableDataRowStateColumn);
((Group) scene.getRoot()).getChildren().addAll(tableDataRow);
stage.setScene(scene);
stage.show();
}
public static class DataRowModel {
private StringProperty name = new SimpleStringProperty(this, "name", "");
private BooleanProperty selected = new SimpleBooleanProperty(this, "selected", true);
private StringProperty state = new SimpleStringProperty(this, "state", "");
public DataRowModel(String name, boolean selected) {
this.name.setValue(name);
this.selected.setValue(selected);
this.selected.addListener((observable, oldVal, newVal) -> {
getState(); // Refresh State value
});
}
public StringProperty getName() {
return name;
}
public BooleanProperty isSelected() {
return selected;
}
public void setSelected(boolean selected) {
if (this.selected.getValue() != selected)
this.selected.setValue(selected);
}
public StringProperty getState() {
String stateStr = "";
if (selected.getValue())
stateStr += "Selected";
state.setValue(stateStr);
return state;
}
}
}
I was able to generate this by editing the Oracle's Person tableview example.
This is a bug, filed as https://bugs.openjdk.java.net/browse/JDK-8096787, and fixed in version 8u60 which is expected to be released in August 2015.

Categories