Creating Groups of specific types - javafx

I've created a simple 3D viewer using a SubScene and adding 3D objects to a top level root item.
public class Viewer extends AnchorPane {
private final PerspectiveCamera camera = new PerspectiveCamera(true);
private final Group root = new Group(camera)
public Viewer() {
SubScene scene = new SubScene(root, 300, 300, true, SceneAntialiasing.BALANCED);
}
public Group getRoot() {
return root;
}
Calling code that users the Viewer can then get the root, create 3D objects and add them to the root. This works nicely.
However, I realised I need to logically separate out the root into three separate areas due to requirements. The root turned into:
private final Group cameraGroup = new Group(camera);
private final Group lightGroup = new Group();
private final Group objectsGroup = new Group();
private final Group root = new Group(cameraGroup, lightGroup, objectsGroup);
Calling code can add multiple cameras, lights, 3D objects via getters for the above groups.
Question:
If I provide calling code with getters to each group, then calling code also needs to constantly cast the Nodes in the group to the given types (e.g. Cameras in cameraGroup). Calling code could also add non-camera types to the Group, as a lot of things in JavaFX inherit from the common Node base type and that's what Group works with.
Providing individual getters, setters, removers etc that cast groups to lists/observablelists of each type complicates the interface.
Is there a way to have a Group? i.e. a Group of a specific type? Or is there a different way to go about my design?

Group is not generic, so there is no direct way to do what you want.
However, you could keep separate ObservableLists and expose them (instead of the Groups), and register listeners with them to keep the groups' child node lists in sync:
public class Viewer {
private final PerspectiveCamera camera = new PerspectiveCamera();
private final ObservableList<Camera> cameras = FXCollections.observableArrayList();
private final Group cameraGroup = new Group();
// etc
public Viewer() {
cameras.addListener((Change<? extends Camera> c) -> {
while (c.next()) {
if (c.wasAdded()) {
cameraGroup.getChildren().addAll(c.getAddedSubList());
}
if (c.wasRemoved()) {
cameraGroup.getChildren().removeAll(c.getRemoved());
}
}
});
cameras.add(camera);
// etc
}
public ObservableList<Camera> getCameras() {
return cameras ;
}
}
An alternative solution (basically the one you describe) is to define specific addCamera() and removeCamera() , etc, methods, and only expose unmodifiable lists:
public class Viewer {
public final PerspectiveCamera camera = new PerspectiveCamera();
public final Group cameraGroup = new Group(camera);
public final void addCamera(Camera camera) {
cameraGroup.getChildren().add(camera);
}
public boolean removeCamera(Camera camera) {
return cameraGroup.getChildren().remove(camera);
}
public List<Camera> getCameras() {
List<Camera> cameras = new ArrayList<>();
for (Node n : cameraGroup.getChildren()) {
cameras.add((Camera)n);
}
return Collections.unmodifiableList(cameras);
}
// etc...
}

Related

JavaFX: Data binding between two observable lists of different type

I have an application that manages (bank) accounts. In my data model, I have defined an observable list for the accounts:
private final ObservableList<Account> accounts = observableArrayList();
Each account has a list of cashflows, which is implemented via a property (also being an observable list):
// in Account class:
private final SimpleListProperty<Cashflow> cashflows = new SimpleListProperty<>(observableArrayList());
In my UI, I have a table containing all accounts, and I am using the cashflow list property to show the number of cashflows for each account, which works fine.
The accounts table also provides checkboxes to select or unselect specific accounts. There's a property for this in my Account class as well:
// in Account class:
private final SimpleBooleanProperty selected = new SimpleBooleanProperty();
Now I want to add another table to the UI, which contains the cashflows, but only for the selected accounts, and preferably I want to implement this via data binding.
But I don't know how to achieve this. I quickly dismissed the idea of using directly the cashflows property of the Account class in some way, because I wouldn't even know where to start here.
So what I tried is define a separate observable list for the cashflows in my data model:
private final ObservableList<Cashflow> cashflowsOfSelectedAccounts = observableArrayList();
I know that I can define extractors for the account list that will notify observers when something changes. So for example, I could extend my account list to something like:
private final ObservableList<Account> accounts = observableArrayList(
account -> new Observable[]{
account.selectedProperty(),
account.cashflowsProperty().sizeProperty()});
This would trigger a notification to a listener on the accounts list on any of the following:
an account is added or removed
a cashflow is added to or removed from an account
an account gets selected or unselected
But now I don't know how I can bring this together with my observable cashflow list, because I have two different data types here: Account, and Cashflow.
The only solution I can think of is to add a custom listener to the account list to react to all of the relevant events listed above, and maintain the cashflowsOfSelectedAccounts manually.
So here's my Question:
Is it possible to sync the accounts list with the list of cashflows of selected accounts via data binding, or via some other way that I'm not aware of and that would be more elegant than manually maintaining the cashflow list with a custom listener on the accounts list?
Thanks!
Personally, I wouldn't try to overcomplicate the bindings too much beyond simple applications of built-in support for the high-level binding APIs. Once you add a few bindings things get complicated enough already.
Alternative 1
What I suggest you do is:
Create a filtered list of selected accounts.
Use the filtered list of selected accounts as the backing list for the second table.
As the second table is only to display cashflow data and not full account data, for column data, provide custom value factories to access the cashflow data in the account.
Making the second table a TreeTableView may make sense, that way it can group the cashflows by account.
This may or may not be a valid approach for your app.
Alternative 2
Alternately, also working off the filteredlist of accounts, add a list change listener to the filtered list, when it changes, update the content of a separate list of related cashflows which you use as the backing list for the cashflow table.
Handling your use cases.
An account is added or removed
Just add or remove from the account list.
A cashflow is added to or removed from an account
An extractor on the account list and a list listener can be triggered when associated cashflows change to trigger an update to the cashflow list.
an account gets selected or unselected
See the linked filtered list example, it is based on an extractor combined with a filtered list.
Use a listener to get selected rows (mails) in tableView and add mails to my list of mails
Alternative 3
Alternately, you could change your UI. For example, have separate edit and commit pages for cashflow data and account data with user button presses for committing or discarding changes. The commits update the backing database. Then, after committing, navigate back to the original page which just reads the new data from the source database again. That's generally how these things often work rather than a bunch of binding.
I realize that none of these options are what you are asking about and some of them probably do work you were trying to avoid through a different binding type, but, those are the ideas I came up with.
Example
FWIW, here is an example of Alternative 2, which relies on an account list, a filtered account list a separate cashflow list, and extractors and listeners to keep stuff in sync.
It won't be exactly what you want, but perhaps you can adapt it or learn something from it.
I would note that the cashflow list doesn't tie a given cashflow to a given account, so, if you want to do that, you might want to add additional functionality to support visual feedback for that association.
Initial state:
Select only a single account:
Remove an account and change the cashflow data for a given account:
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.*;
import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class FlowApp extends Application {
private final ObservableList<Account> accounts = FXCollections.observableArrayList(
account -> new Observable[] { account.selectedProperty(), account.getCashflows() }
);
private final ObservableList<Account> cashflowAccounts = new FilteredList<>(
accounts,
account -> account.selectedProperty().get()
);
private final ObservableList<Cashflow> cashflows = FXCollections.observableArrayList(
cashflow -> new Observable[] { cashflow.amountProperty() }
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
cashflowAccounts.addListener((ListChangeListener<Account>) c -> updateCashflows());
initDataStructures();
final TableView<Account> accountSelectionTableView =
createAccountSelectionTableView();
final TableView<Cashflow> cashflowView =
createCashflowView();
final Button change = new Button("Change");
change.setOnAction(e -> changeData(change));
final Button reset = new Button("Reset");
reset.setOnAction(e -> { initDataStructures(); change.setDisable(false); });
final VBox vbox = new VBox(
10,
new TitledPane("Accounts", accountSelectionTableView),
new TitledPane("Cashflows", cashflowView),
new HBox(10, change, reset)
);
vbox.setPadding(new Insets(10));
stage.setScene(new Scene(vbox));
stage.show();
}
private void changeData(Button change) {
accounts.get(accounts.size() - 1);
// Paul dies.
accounts.removeIf(
account -> account.firstNameProperty().get()
.equals("Paul")
);
// Albert.
Account albert = accounts.stream()
.filter(
account -> account.firstNameProperty().get().equals(
"Albert"
)
).findFirst().orElse(null);
if (albert == null) {
return;
}
// Albert stops receiving alimony.
albert.getCashflows().removeIf(
c -> c.sourceProperty().get().equals(
CashflowSource.ALIMONY
)
);
// Albert's rent increases.
Cashflow albertsRent = albert.getCashflows().stream()
.filter(
cashflow -> cashflow.sourceProperty().get().equals(
CashflowSource.RENT
)
).findFirst().orElse(null);
if (albertsRent == null) {
return;
}
albertsRent.amountProperty().set(
albertsRent.amountProperty().get() + 5
);
// only allow one change.
change.setDisable(true);
}
private void initDataStructures() {
accounts.setAll(
new Account("Ralph", "Alpher", true, "ralph.alpher#example.com",
new Cashflow(CashflowSource.RENT, 10),
new Cashflow(CashflowSource.ALIMONY, 5)
),
new Account("Hans", "Bethe", false, "hans.bethe#example.com"),
new Account("George", "Gammow", true, "george.gammow#example.com",
new Cashflow(CashflowSource.SALARY, 3)
),
new Account("Paul", "Dirac", false, "paul.dirac#example.com",
new Cashflow(CashflowSource.RENT, 17),
new Cashflow(CashflowSource.SALARY, 4)
),
new Account("Albert", "Einstein", true, "albert.einstein#example.com",
new Cashflow(CashflowSource.RENT, 2),
new Cashflow(CashflowSource.ALIMONY, 1),
new Cashflow(CashflowSource.DIVIDENDS, 8)
)
);
}
private void updateCashflows() {
cashflows.setAll(
cashflowAccounts.stream()
.flatMap(a ->
a.getCashflows().stream()
).toList()
);
}
private TableView<Account> createAccountSelectionTableView() {
final TableView<Account> selectionTableView = new TableView<>(accounts);
selectionTableView.setPrefSize(540, 180);
TableColumn<Account, String> firstName = new TableColumn<>("First Name");
firstName.setCellValueFactory(cd -> cd.getValue().firstNameProperty());
selectionTableView.getColumns().add(firstName);
TableColumn<Account, String> lastName = new TableColumn<>("Last Name");
lastName.setCellValueFactory(cd -> cd.getValue().lastNameProperty());
selectionTableView.getColumns().add(lastName);
TableColumn<Account, Boolean> selected = new TableColumn<>("Selected");
selected.setCellValueFactory(cd -> cd.getValue().selectedProperty());
selected.setCellFactory(CheckBoxTableCell.forTableColumn(selected));
selectionTableView.getColumns().add(selected);
TableColumn<Account, String> email = new TableColumn<>("Email");
email.setCellValueFactory(cd -> cd.getValue().emailProperty());
selectionTableView.getColumns().add(email);
TableColumn<Account, Integer> numCashflows = new TableColumn<>("Num Cashflows");
numCashflows.setCellValueFactory(cd -> Bindings.size(cd.getValue().getCashflows()).asObject());
numCashflows.setStyle("-fx-alignment: baseline-right;");
selectionTableView.getColumns().add(numCashflows);
selectionTableView.setEditable(true);
return selectionTableView;
}
private TableView<Cashflow> createCashflowView() {
TableView<Cashflow> cashflowView = new TableView<>();
TableColumn<Cashflow, CashflowSource> source = new TableColumn<>("Source");
source.setCellValueFactory(cd -> cd.getValue().sourceProperty());
cashflowView.getColumns().add(source);
TableColumn<Cashflow, Integer> amount = new TableColumn<>("Amount");
amount.setCellValueFactory(cd -> cd.getValue().amountProperty().asObject());
amount.setStyle("-fx-alignment: baseline-right;");
cashflowView.getColumns().add(amount);
cashflowView.setItems(cashflows);
cashflowView.setPrefHeight(160);
return cashflowView;
}
private static class Account {
private final StringProperty firstName;
private final StringProperty lastName;
private final BooleanProperty selected;
private final StringProperty email;
private final ObservableList<Cashflow> cashflows;
private Account(String fName, String lName, boolean selected, String email, Cashflow... cashflows) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.selected = new SimpleBooleanProperty(selected);
this.email = new SimpleStringProperty(email);
this.cashflows = FXCollections.observableArrayList(cashflows);
}
public StringProperty firstNameProperty() {
return firstName;
}
public StringProperty lastNameProperty() {
return lastName;
}
public BooleanProperty selectedProperty() {
return selected;
}
public StringProperty emailProperty() {
return email;
}
public ObservableList<Cashflow> getCashflows() {
return cashflows;
}
}
class Cashflow {
private final ObjectProperty<CashflowSource> source;
private final IntegerProperty amount;
public Cashflow(CashflowSource source, int amount) {
this.source = new SimpleObjectProperty<>(source);
this.amount = new SimpleIntegerProperty(amount);
}
public ObjectProperty<CashflowSource> sourceProperty() {
return source;
}
public IntegerProperty amountProperty() {
return amount;
}
}
enum CashflowSource {
RENT, SALARY, DIVIDENDS, ALIMONY
}
}

java.lang.IllegalArgumentException: Problem and Hitbox

I have two problems one this that, if i want to show score with the circle object:
layoutV.getChildren().addAll(virus, score);
I get the following error:
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Children: duplicate children added: parent = Pane#6661fc86[styleClass=root].
As far as I understand it is because the Task wants to show multiple scores. So should I use another scene or layout to show score?
My other problem is the hitbox of the object, right know everytime i click the score goes up. I looked up the mouse event getTarget but it does not seem like I can make it so that my object is the only target to use the mouse event on.
public class Main extends Application {
private Stage window;
private Pane layoutV;
private Scene scene;
private Circle virus;
private int score;
private Label scores;
#Override
public void start(Stage primaryStage) {
window = primaryStage;
window.setTitle("Enemy TEST");
this.score = 0;
scores = new Label("Score "+ score);
layoutV = new Pane();
scene = new Scene(layoutV, 600, 600);
window.setScene(scene);
window.show();
Thread th = new Thread(task);
th.setDaemon(true);
th.start();
}
Task task = new Task<Void>() {
#Override
protected Void call() throws Exception {
while (true) {
Platform.runLater(new Runnable() {
#Override
public void run() {
drawCircles();
}
});
Thread.sleep(1000);
}
}
};
public void drawCircles() {
double x = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double y = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double r = (double)(Math.random() * ((30 - 10) + 1)) + 10;
virus = new Circle(x, y, r, Color.VIOLET);
layoutV.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
layoutV.getChildren().remove(e.getTarget());
this.score++;
System.out.println("score: "+ this.score);
}
});
layoutV.getChildren().addAll(virus);
scene.setRoot(layoutV);
window.setScene(scene);
}
public static void main(String[] args) {
launch(args);
}
}
You have lots of issues, not just the ones from your question:
Although it will work as you coded
it, I don't advise spawning a thread to draw your circles, instead
see:
JavaFX periodic background task
You don't need to set the root in the scene and the scene in the
window every time you draw a new circle.
Nor do you need to set the
mouse handler on the layout every time you draw a circle.
Rather than setting a mouse handler on the layout, you are better off setting a mouse handler on the circles themselves (which you can do before you add them to the scene).
score is an int, not a node you can only add nodes to the scene
graph.
See the documentation for the scene package:
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.
How you are adding the node more than once is not clear to me, because you are probably doing it in code different than the Main class you provided.
To add a circle with a score on top, use a StackPane with the score in a label, but make the label mouse transparent, so that it does not register any clicks:
Label scoreLabel = new Label(score + "");
scoreLabel.setMouseTransparent(true);
StackPane balloon = new StackPane(circle, scoreLabel);
layoutV.getChildren.add(balloon);
Add the click handler on the balloon.
And additional issues I don't detail here but are solved in the demo code provided.
To fix all your errors, I would write some code like below. Perhaps you can review it and compare it with your code to help understand one way to create this game.
The example code might not be exactly the functionality you are looking for (that is not really its purpose), but it should be enough to keep you on the right track for implementing your application.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.concurrent.ThreadLocalRandom;
public class Inoculation extends Application {
public static final int W = 600;
public static final int H = 600;
private final IntegerProperty score = new SimpleIntegerProperty(0);
private final Pane playingField = new Pane();
#Override
public void start(Stage stage) {
StackPane overlay = createOverlay();
Pane layout = new StackPane(playingField, overlay);
stage.setScene(new Scene(layout, W, H));
stage.show();
Infection infection = new Infection(playingField, score);
infection.begin();
}
private StackPane createOverlay() {
Label totalScoreLabel = new Label();
totalScoreLabel.textProperty().bind(
Bindings.concat(
"Score ", score.asString()
)
);
StackPane overlay = new StackPane(totalScoreLabel);
StackPane.setAlignment(totalScoreLabel, Pos.TOP_LEFT);
overlay.setMouseTransparent(true);
return overlay;
}
public static void main(String[] args) {
launch(args);
}
}
class Infection {
private static final Duration SPAWN_PERIOD = Duration.seconds(1);
private static final int NUM_SPAWNS = 10;
private final Timeline virusGenerator;
public Infection(Pane playingField, IntegerProperty score) {
virusGenerator = new Timeline(
new KeyFrame(
SPAWN_PERIOD,
event -> spawnVirus(
playingField,
score
)
)
);
virusGenerator.setCycleCount(NUM_SPAWNS);
}
public void begin() {
virusGenerator.play();
}
private void spawnVirus(Pane playingField, IntegerProperty score) {
Virus virus = new Virus();
virus.setOnMouseClicked(
event -> {
score.set(score.get() + virus.getVirusScore());
playingField.getChildren().remove(virus);
}
);
playingField.getChildren().add(virus);
}
}
class Virus extends StackPane {
private static final int MAX_SCORE = 3;
private static final int RADIUS_INCREMENT = 10;
private final int virusScore = nextRandInt(MAX_SCORE) + 1;
public Virus() {
double r = (MAX_SCORE + 1 - virusScore) * RADIUS_INCREMENT;
Circle circle = new Circle(
r,
Color.VIOLET
);
Text virusScoreText = new Text("" + virusScore);
virusScoreText.setBoundsType(TextBoundsType.VISUAL);
virusScoreText.setMouseTransparent(true);
getChildren().setAll(
circle,
virusScoreText
);
setLayoutX(nextRandInt((int) (Inoculation.W - circle.getRadius() * 2)));
setLayoutY(nextRandInt((int) (Inoculation.H - circle.getRadius() * 2)));
setPickOnBounds(false);
}
public int getVirusScore() {
return virusScore;
}
// next random int between 0 (inclusive) and bound (exclusive)
private int nextRandInt(int bound) {
return ThreadLocalRandom.current().nextInt(bound);
}
}
Some additional notes on this implementation that might be useful to know:
The total score is placed in an overlayPane so that it is not obscured by elements added to the playingField (which contains the virus spawns).
The overlayPane is made mouseTransparent, so that it won't intercept any mouse events, and the clicks will fall through to the items in the playing field.
The app currently generates viruses within a fixed field size, regardless of whether you resize the window. That is just the way it is designed and coded, you could code it otherwise if wished. It would be more work to do so.
The Bindings class is used to create a string expression binding which concatenates the static string "Score " with an integer property representing the score. This allows the string representing the score to be bound to the score label text in the overlay so that it automatically updates whenever the score is changed.
The virus generation uses a timeline and is based on the concepts from:
JavaFX periodic background task
The application class is kept deliberately simple to handle mostly just the core application lifecycle, and the actual functionality of the application is abstracted to an Infection class which handles the spawning of the virus and a Virus class that generates a new virus.
This technique is used to center a score for each individual virus on the virus:
how to put a text into a circle object to display it from circle's center?
The virus itself is laid out in a StackPane. The pane has pick on bounds set to false. To remove the virus infection, you must click on the circle which represents the virus and not just anywhere in the square for the stack pane.
Because circle coordinates are in local co-ordinates and the circle is in a parent stack pane representing the virus, the circle itself does not need x and y values set, instead layout x and y values are set on the enclosing pane to allow positioning of the pane representing the entire virus.
The following technique is used to generate random integers in acceptable ranges using ThreadLocalRandom:
How do I generate random integers within a specific range in Java?

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.

How to Generate Message According to Values in Multiple Fields?

I am building an application using JavaFX. What I am trying to do is generate a message according to the user input values. So there are one text-field and one combo-box and one check-box per row and there are many rows like the following.
Let's say I will generate three different messages according to the user values. So I need to check whether those fields are empty or not and check each field's value to generate a specific message. Checking fields are okay for just three rows like the above. But I have 10 fields. So I have to check each and generate or append my own message. And also if the user checked the check-box need to group all checked row values. So what I am asking is there any good way (best practice) to achieve what I need or an easy one also? I have tried with HashMap and ArrayList. But those are not working for this.
Really appreciate it if anybody can help me. Thanks in advance.
I would probably recommend a custom node that you create on your own like below. This example is not supposed to have the same functionality as your application but just to show how to create and use custom nodes. I kept your idea in mind when creating this example it has your textfield combobox and checkbox and they are a little easier to manage. Give it a run and let me know if you have any questions
public class Main extends Application {
#Override
public void start(Stage stage) {
VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
ArrayList<String> itemList = new ArrayList<>(Arrays.asList("Dog", "Cat", "Turkey"));
ArrayList<HBoxRow> hBoxRowArrayList = new ArrayList<>();
for (int i = 0; i<3; i++) {
HBoxRow hBoxRow = new HBoxRow();
hBoxRow.setComboBoxValues(FXCollections.observableList(itemList));
hBoxRowArrayList.add(hBoxRow);
vBox.getChildren().add(hBoxRow.gethBox());
}
Button printTextfieldsButton = new Button("Print Textfields");
printTextfieldsButton.setOnAction(event -> {
for (HBoxRow hBoxRow : hBoxRowArrayList) {
System.out.println("hBoxRow.getTextFieldInput() = " + hBoxRow.getTextFieldInput());
}
});
vBox.getChildren().add(printTextfieldsButton);
stage.setScene(new Scene(vBox));
stage.show();
}
//Below is the custom Node
public class HBoxRow {
HBox hBox = new HBox();
ComboBox<String> comboBox = new ComboBox<>();
TextField textField = new TextField();
CheckBox checkBox = new CheckBox();
public HBoxRow(){
hBox.setAlignment(Pos.CENTER);
textField.setPrefWidth(150);
comboBox.setPrefWidth(150);
checkBox.setOnAction(event -> {
textField.setDisable(!textField.isDisabled());
comboBox.setDisable(!comboBox.isDisabled());
});
hBox.getChildren().addAll(checkBox, textField, comboBox);
}
public void setComboBoxValues(ObservableList observableList) {
comboBox.setItems(observableList);
}
public HBox gethBox(){
return hBox;
}
public String getTextFieldInput(){
return textField.getText();
}
}
}

JavaFX Groups that accept specific object types

Is there a way in JavaFX to make a class that extends Group and limit it to accepting only Shape objects as children?
Consider creating a wrapper class instead of a subclass. Something along the lines of
public class ShapeGroup {
private final Group group = new Group() ;
public void addShape(Shape s) {
group.getChildren().add(s);
}
public void removeShape(Shape s) {
group.getChildren().remove(s);
}
// other methods you want to expose, implemented similarly...
public Parent asParent() {
return group ;
}
}
And now you can use this as follows:
ShapeGroup shapeGroup = new ShapeGroup();
shapeGroup.addShape(new Circle(50, 50, 20));
shapeGroup.addShape(new Rectangle(20, 20, 30, 30));
// ...
Scene scene = new Scene(shapeGroup.asParent());
// etc..

Resources