Can I place 3D geometry objects in a JavaFXCollection? - javafx

I have a few 3D geometrical objects like sphere, Tube, Cube etc. I am generating using usual way of using classes like Sphere, Cylinder,Box etc inside FXML based-menu in a FXMLcontroller. This means object box1 is local to #FXMLmakeCube sort of method.
Now I wish to perform few operations like boolean operation, copy, mirroring etc. in another method inside this controller. I want to keep all created geometries in JavaFXCollection sort of List so that I may call the handle to those geometries from inside any other method.
My question is how can I do this? How can I refer this handles in other method inside the same FXMLController?
I did not find exact question in the net.

You can place all those 3D objects in one collection, since all of them extend from Shape3D.
You can create an ObservableList<Shape3D> collection, and add each object to it when you create them. Then you can listen to changes in the collection, and add to the scene/subscene all the new objects.
This would be a sample of a controller with four buttons, where you can create random Box or Sphere 3D objects, add them to the collection, and place them in a subscene.
Also you can perform operations with the full collection (translate or rotate them in this case).
public class FXMLDocumentController {
#FXML
private Pane pane;
private Group pane3D;
private PerspectiveCamera camera;
private ObservableList<Shape3D> items;
#FXML
void createBox(ActionEvent event) {
Box box = new Box(new Random().nextInt(200), new Random().nextInt(200), new Random().nextInt(200));
box.setMaterial(new PhongMaterial(new Color(new Random().nextDouble(),
new Random().nextDouble(), new Random().nextDouble(), new Random().nextDouble())));
box.setTranslateX(-100 + new Random().nextInt(200));
box.setTranslateY(-100 + new Random().nextInt(200));
box.setTranslateZ(new Random().nextInt(200));
items.add(box);
}
#FXML
void createSphere(ActionEvent event) {
Sphere sphere = new Sphere(new Random().nextInt(100));
sphere.setMaterial(new PhongMaterial(new Color(new Random().nextDouble(),
new Random().nextDouble(), new Random().nextDouble(), new Random().nextDouble())));
sphere.setTranslateX(-100 + new Random().nextInt(200));
sphere.setTranslateY(-100 + new Random().nextInt(200));
sphere.setTranslateZ(new Random().nextInt(200));
items.add(sphere);
}
public void initialize() {
camera = new PerspectiveCamera(true);
camera.setNearClip(0.1);
camera.setFarClip(10000);
camera.setTranslateZ(-1000);
pane3D = new Group(camera);
SubScene subScene = new SubScene(pane3D, 400, 400, true, SceneAntialiasing.BALANCED);
subScene.setFill(Color.ROSYBROWN);
subScene.setCamera(camera);
pane.getChildren().add(subScene);
items = FXCollections.observableArrayList();
items.addListener((ListChangeListener.Change<? extends Shape3D> c) -> {
while (c.next()) {
if (c.wasAdded()) {
c.getAddedSubList().forEach(i -> pane3D.getChildren().add(i));
}
}
});
}
#FXML
void rotateAll(ActionEvent event) {
items.forEach(s -> {
s.setRotate(new Random().nextInt(360));
s.setRotationAxis(new Point3D(-100 + new Random().nextInt(200),
-100 + new Random().nextInt(200), new Random().nextInt(200)));
});
}
#FXML
void translateAll(ActionEvent event) {
items.forEach(s -> {
s.setTranslateX(-100 + new Random().nextInt(200));
s.setTranslateY(-100 + new Random().nextInt(200));
s.setTranslateZ(new Random().nextInt(200));
});
}
}

Related

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 ensure each Controller can see the model and is only initialized once?

In my mainController I'm using the following method to change views:
#FXML
public void handleChangeView(ActionEvent event) {
try {
String changeButtonID = ((Button) event.getSource()).getId();
FXMLLoader loader = new FXMLLoader(getClass().getResource("../../resources/view/" + changeButtonID + ".fxml"));
mainView.setCenter(loader.load());
} catch (IOException e){
e.printStackTrace();
}
}
Now this is working but I have two problems:
1) With each button click the controller of the loaded FXML file fires again. For example in my other controller I have:
Timeline counterTimeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> System.out.println("Test")));
counterTimeline.setCycleCount(Animation.INDEFINITE);
counterTimeline.play();
With each click it seems a new Timeline is created and set to play - how can I ensure this doesn't happen? It seems like every time a change view button is clicked the controller for it is re-initialized.
2) How can I ensure each controller can see the model while still using the above method to change views? I'd rather avoid dependancy injections because I honestly can't wrap my head around it - after 6 hours trying to get afterburner.fx to work I can't handle it.
You can create a new fxml nodes only once per type:
private final Map <String, Parent> fxmls = new HashMap<>();
#FXML
public void handleChangeView(ActionEvent event) {
try {
String changeButtonID = ((Button) event.getSource()).getId();
Parent newOne = fxmls.get(changeButtonID);
if (newOne == null) {
FXMLLoader loader = new FXMLLoader(getClass().getResource("../../resources/view/" + changeButtonID + ".fxml"));
newOne = loader.load();
fxmls.put(changeButtonID, newOne):
}
mainView.setCenter(newOne);
} catch (IOException e){
e.printStackTrace();
}
}

Creating Groups of specific types

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...
}

How can I use .setText on a non-Static Label from a different Class [duplicate]

I have written a controller for two windows /stages.
The first window is opened in the MainClass. The second in the Controller, if the user clicks onto a button.
How can I get the TextFields from second.fxml in the applyFor()-method?
Thanks.
#FXML
protected void requestNewAccount(ActionEvent event) {
try {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("second.fxml")); // TextFields in there
Parent root = (Parent) fxmlLoader.load();
Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.setTitle("Second Window");
Scene scene = new Scene(root);
String css = MainOnlineCustomer.class.getResource("/style.css").toExternalForm();
scene.getStylesheets().clear();
scene.getStylesheets().add(css);
stage.setScene(scene);
stage.show();
} catch (IOException e) {
logger.error(e);
}
}
/**
* closes the "second"-Window
* #param event
*/
#FXML
protected void cancel(ActionEvent event) {
final Node source = (Node) event.getSource();
final Stage stage = (Stage) source.getScene().getWindow();
stage.close();
}
#FXML
protected void applyFor(ActionEvent event) {
// get values from TextField in second.fxml here!!!
}
It's not good to share controllers between fxmls unless they serve the same purpose. Here both fxml seem to serve a different purpose (account management, login or something similar for one of them and creating a new account for the other). What is even worse is that those classes do not share the same controller instance, which means the small (and probably only) benefit you could get from using the same controller, is not used here. You should better use different controllers.
Since you use Modality.APPLICATION_MODAL as modality, I'd recommend using showAndWait instead of show to open the new stage. This will enter a nested event loop, which allows the UI to remain responsive and continues after the invocation of showAndWait once the stage is closed.
Furthermore add a method to the controller of second.fxml that allows you to retrieve the result.
Example
This creates a Person object with given name and family name.
"primary window (opening the "inner" stage)
FXMLLoader loader = new FXMLLoader(getClass().getResource("second.fxml"));
Stage subStage = new Stage();
subStage.initModality(Modality.APPLICATION_MODAL);
subStage.setTitle("Second Window");
Scene scene = new Scene(loader.load());
subStage.setScene(scene);
subStage.showAndWait();
Optional<Person> result = loader.<Supplier<Optional<Person>>>getController().get();
if (result.isPresent()) {
// do something with the result
}
controller for "inner" content
public class SecondController implements Supplier<Optional<Person>> {
#FXML
private TextField givenName;
#FXML
private TextField familyName;
private boolean submitted = false;
// handler for submit action
#FXML
private void submit() {
submitted = true;
givenName.getScene().getWindow().hide();
}
// handler for cancel action
#FXML
private void cancel() {
givenName.getScene().getWindow().hide();
}
#Override
public Optional<Person> get() {
return submitted ? Optional.of(new Person(givenName.getText(), familyName.getText())) : Optional.empty();
}
}
Note that you can gain access to any data available to the controller this way. I wouldn't recommend accessing any nodes (like TextFields) directly though, since this makes changing the UI harder.
Using the Supplier interface here is not necessary, but I chose to do this to achieve a loose coupling between SecondController and the main window.

How to add a listener in a particular cell in a grid in GXT

I would like to add a listener when I am clicking the cell for categories only.
this is the declaration of my columnConfig
ColumnConfig<UserRights, Boolean> unlockConfig = new ColumnConfig<UserRights, Boolean>(properties.hasUnlock(), 50);
unlockConfig.setHeader("Unlock");
cfgs.add(unlockConfig);
ColumnConfig<UserRights, String> catConfig = new ColumnConfig<UserRights, String>(properties.categories(), 150);
catConfig.setHeader("Categories");
cfgs.add(catConfig);
cm = new ColumnModel<UserRights>(cfgs);
grid = new Grid<UserRights>(store, cm);
grid.getView().setAutoFill(true);
grid.addStyleName("margin-10");
grid.setLayoutData(new VerticalLayoutContainer.VerticalLayoutData(1, 1));
grid.addRowClickHandler(new RowClickEvent.RowClickHandler() {
#Override
public void onRowClick(RowClickEvent event) {
index = event.getRowIndex();
}
});
rowEditing = new GridRowEditing<UserRights>(grid);
rowEditing.addEditor(unlockConfig, new CheckBox());
how could I add a listener in the category column?
Thanks in advance.
There is nothing in your categories cell that prompts a user to click, it just contains text. What you should be doing is using the columnConfig.setCell(Cell cell) method to specify a cell that contains an interactive component.
You could still try something along these lines:
columnConfig.setCell(new SimpleSafeHtmlCell<String>(SimpleSafeHtmlRenderer.getInstance(), "click") {
#Override
public void onBrowserEvent(Context context, Element parent, String value, NativeEvent event, ValueUpdater<String> valueUpdater) {
super.onBrowserEvent(context, parent, value, event, valueUpdater);
if (event.getType().equals("click")) {
}
}
});

Resources