Remove a row from a GridPane - javafx

I'm using a GridPane to stock informations about cities (in a game), but sometimes I want to delete some lines. This is the Class I use for my GridPane :
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.franckyi.kingsim.city.City;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
public class CityGrid extends GridPane {
private List<City> cities;
public CityGrid() {
super();
cities = new ArrayList<City>();
}
public List<City> getCities() {
return cities;
}
public void deleteCity(int row) {
cities.remove(row - 1);
removeNodes(getNodesFromRow(row));
int i = row;
while (!getNodesFromRow(i + 1).isEmpty()) {
moveNodes(i + 1, getNodesFromRow(i + 1));
removeNodes(getNodesFromRow(i + 1));
i++;
}
}
public void addCity(City city) {
cities.add(city);
int col = 0;
List<Node> rowNodes = new ArrayList<Node>();
Button mod = new Button("Modify");
mod.setOnAction(event -> {
});
Button del = new Button("Delete");
del.setOnAction(event -> {
deleteCity(getRowIndex(del));
});
rowNodes.addAll(Arrays.asList(new CheckBox(), new Text(city.getName()), new Text(city.getStatus().getText()),
new Text(city.getTrade() + ""), new Text(city.getCost() + ""), new Text(city.getTroops() + ""), mod,
del));
for (Node n : rowNodes) {
n.setId(cities.size() + "");
this.add(n, col, cities.size());
col++;
}
}
private List<Node> getNodesFromRow(int i) {
List<Node> list = new ArrayList<Node>();
for (Node n : getChildren()) {
if (getRowIndex(n).equals(i)) {
list.add(n);
}
}
System.out.println(list.size());
return list;
}
private void removeNodes(List<Node> list) {
for (Node node : list) {
this.getChildren().remove(getIndex(getColumnIndex(node), getRowIndex(node)));
}
}
private void moveNodes(int row, List<Node> nodes) {
int i = 0;
for (Node node : getNodesFromRow(row)) {
this.getChildren().set(getIndex(getColumnIndex(node), getRowIndex(node)),
nodes.get(i));
i++;
}
}
private int getIndex(int col, int row) {
int i = 0;
for (Node node : getChildren()) {
if (getColumnIndex(node) == col && getRowIndex(node) == row)
return i;
i++;
}
return 0;
}
}
The addCity function works perfectly. The deleteCity function also works when I delete the last city added. But when I delete a city, it automatically deletes ALL the cities added after the one I delete, and I don't want that.
You should also notice that everytime the getNodesFromRow(int i) method is executed, it prints the number of Nodes in the selected row. When I add two cities and I delete the first one, this is what I get in the console : 8, 0, 8, 8, 8, 8, 8, 0.
Can someone help me ? (Tell me if you want all the code needed to reproduce it at home)

private void moveNodes(int row, List<Node> nodes) {
int i = 0;
for (Node node : getNodesFromRow(row)) {
this.getChildren().set(getIndex(getColumnIndex(node), getRowIndex(node)),
nodes.get(i));
i++;
}
}
This does not work. The index in the child list has no meaning in a GridPaneother then the order in which they are drawn. The row/column index is saved to the properties map of each child. To modify these, you need to use the static GridPane.setRowIndex method.
Example
#Override
public void start(Stage primaryStage) {
Button btn = new Button("Delete");
TextField tf = new TextField();
TextFormatter<Integer> formatter = new TextFormatter<>(new IntegerStringConverter());
formatter.setValue(0);
tf.setTextFormatter(formatter);
btn.disableProperty().bind(IntegerExpression.integerExpression(formatter.valueProperty()).lessThan(0));
GridPane grid = new GridPane();
grid.setHgap(5);
grid.setVgap(5);
btn.setOnAction((ActionEvent event) -> {
deleteRow(grid, formatter.getValue());
});
for (int r = 0; r < 5; r++) {
for (int c = 0; c < 3; c++) {
grid.add(new Text(r+"_"+c), c, r);
}
}
Scene scene = new Scene(new VBox(new HBox(tf, btn), grid));
primaryStage.setScene(scene);
primaryStage.show();
}
static void deleteRow(GridPane grid, final int row) {
Set<Node> deleteNodes = new HashSet<>();
for (Node child : grid.getChildren()) {
// get index from child
Integer rowIndex = GridPane.getRowIndex(child);
// handle null values for index=0
int r = rowIndex == null ? 0 : rowIndex;
if (r > row) {
// decrement rows for rows after the deleted row
GridPane.setRowIndex(child, r-1);
} else if (r == row) {
// collect matching rows for deletion
deleteNodes.add(child);
}
}
// remove nodes from row
grid.getChildren().removeAll(deleteNodes);
}

I had the same task today with the added requirement to not only remove (hide) a single row but multiple rows and also to readd (unhide) them later. (My use case is a form that has to show varying subsets of a set of fields, depending on choices made by the user.)
Put on the right track by fabian, whom I'd like to thank, I designed the GridPaneRowHider class shown below.
To use it with a given GridPane,
GridPane myPane = ...
allocate an instance
GridPaneRowHider h = new GridPaneRowHider();
and call
h.hide(myPane,1,2,6);
to hide the specified rows and
h.unhide(myPane);
to reveal them again.
So here's the code:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.IntStream;
import nnn.util.collection.Pair;
import javafx.scene.Node;
import javafx.scene.layout.GridPane;
/**
* An object that can hide and unhide rows in a GridPane.
*
* #author Tillmann
* #since 1.1.9
*/
public class GridPaneRowHider {
/**
* A container of the currently hidden Nodes, along with information about
* their positions; associates rows with Nodes in row; sorted by rows,
* ascending
*/
private Map<Integer,List<Pair<Node,Integer>>> hidden_ = new TreeMap<Integer,List<Pair<Node,Integer>>>(
(a, b) -> a.compareTo(b));
/**
* From the specified GridPane hides the specified rows.
*
* #param gp the GridPane
* #param rows the rows to hide
* #see #unhide(GridPane)
*/
public void hide(GridPane gp, int... rows) {
// to suit the algorithm, sort the rows in descending fashion (higher row
// numbers before lower, i.e. visually south to north)
Integer[] sRows = IntStream.of(rows).boxed().toArray(Integer[]::new);
Arrays.sort(sRows,(a, b) -> b.compareTo(a));
// row by row
for (int row : sRows) {
// the list of Nodes in this row
List<Pair<Node,Integer>> li = new ArrayList<Pair<Node,Integer>>();
hidden_.put(Integer.valueOf(row),li);
// look at all the Nodes
for (Iterator<Node> it = gp.getChildren().iterator(); it.hasNext();) {
Node n = it.next();
int r = GridPane.getRowIndex(n);
// if it's a Node in the row to hide
if (r == row) {
// save it in the list
li.add(new Pair<>(n,GridPane.getColumnIndex(n)));
// and remove it from the GridPane
it.remove();
}
// if it's a Node further down, move it a row upwards (to the North)
// to fill the visual gap
else if (r > row)
GridPane.setRowIndex(n,r - 1);
}
}
}
/**
* Shows the rows again that have been hidden by a previous call to
* <code>hide()</code>.
* <p>
*
* If no call to hide() took place before (or unhide() has been called
* already), nothing happens. If hide() has been called several times, the
* result is undefined.
*
* #param gp the GridPane
* #see #hide(GridPane, int...)
*/
public void unhide(GridPane gp) {
// walk along the rows from north to south
for (Map.Entry<Integer,List<Pair<Node,Integer>>> me : hidden_.entrySet()) {
// look at the Nodes in the GridPane
for (Node n : gp.getChildren()) {
int r = GridPane.getRowIndex(n);
// if the Node is in the row to unhide or further down, make room
// by moving it a row further down (to the south)
if (r >= me.getKey())
GridPane.setRowIndex(n,r + 1);
}
// readd the hidden Nodes to the GridPane at their saved locations
for (Pair<Node,Integer> p : me.getValue())
gp.add(p.t_,p.u_,me.getKey());
}
// and mark we're done
hidden_.clear();
}
}

Related

JavaFX PieChart Legend Color change

I need to change color of circles in PieChart Legend. I don't know how to get to this property of PieChart. For example I'm able to change color of text in label Legend and I think this is close to the solution.
It shows what I want to change:
#FXML
public PieChart chart;
public ObservableList<PieChart.Data> pieChartData = FXCollections.observableArrayList();
public void chartLoad() {
pieChartData.clear();
List<String> colorList = new ArrayList<>();
for(int i = 0; i < categoryList.getSize(); i++) {
if(categoryList.getByIndex(i).getValue() > 0) {
PieChart.Data data = new PieChart.Data(categoryList.getByIndex(i).getName(),
categoryList.getByIndex(i).getValue());
pieChartData.add(data);
data.getNode().setStyle("-fx-pie-color: " +
categoryList.getByIndex(i).getColor().getName());
colorList.add(categoryList.getByIndex(i).getColor().getName());
}
}
Set<Node> items = chart.lookupAll("Label.chart-legend-item");
int i = 0;
for(Node item : items) {
Label label = (Label) item;
label.setText("sampleText");
label.setStyle("-fx-text-fill: " + colorList.get(i));
System.out.println(label.getChildrenUnmodifiable().toString());
i++;
}
chart.setData(pieChartData);
}
Thank you for your future comments and answers.
Dynamically allocating colors to charts is a bit of a pain. If you have a fixed set of colors, without a predefined mapping from your data to the colors, you can just use an external style sheet, but doing anything else needs (as far as I know) a bit of a hack.
The default modena.css style sheet defines eight constant colors, CHART_COLOR_1 to CHART_COLOR_8. Nodes in a pie chart, including both the "pie slices" and the color swatches in the legend, are assigned a style class from the eight classes default-color0 to default-color7. Each of these style classes by default has -fx-pie-color set to one of the constants. Unfortunately, if the data in the pie chart are changed, these mappings from default-colorx to CHART_COLOR_y change in a way that isn't documented.
So the best approach for your scenario that I can find is:
add the new data to the chart
once all the data are added, for each datum look up the style class that was added to the datum's node
look up all the nodes in the chart that have that style class (this will also give the legend swatches)
update the -fx-pie-color for those nodes to the desired color
The last trap here is that you need to make sure the legend is added to the chart, and that CSS is applied to the chart, so that the lookup works.
public void chartLoad() {
pieChartData.clear();
List<String> colors = new ArrayList<>();
for(int i = 0; i < categoryList.getSize(); i++) {
if(categoryList.getByIndex(i).getValue() > 0) {
PieChart.Data data = new PieChart.Data(categoryList.getByIndex(i).getName(),
categoryList.getByIndex(i).getValue());
pieChartData.add(data);
colors.add(categoryList.getByIndex(i).getColor().getName());
}
}
chart.setData(pieChartData);
chart.requestLayout();
chart.applyCSS();
for (int i = 0 ; i < pieChartData.size() ; i++) {
PieChart.Data d = pieChartData.get(i);
String colorClass = "" ;
for (String cls : d.getNode().getStyleClass()) {
if (cls.startsWith("default-color")) {
colorClass = cls ;
break ;
}
}
for (Node n : chart.lookupAll("."+colorClass)) {
n.setStyle("-fx-pie-color: "+colors.get(i));
}
}
}
Here's a quick, complete demo of this approach:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class PieChartTest extends Application {
private final Random rng = new Random();
#Override
public void start(Stage primaryStage) throws Exception {
PieChart chart = new PieChart();
Button button = new Button("Generate Data");
button.setOnAction(e -> updateChart(chart));
BorderPane root = new BorderPane(chart);
HBox controls = new HBox(button);
controls.setAlignment(Pos.CENTER);
controls.setPadding(new Insets(5));
root.setTop(controls);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private void updateChart(PieChart chart) {
chart.getData().clear();
int numValues = 4 + rng.nextInt(10);
List<String> colors = new ArrayList<>();
List<PieChart.Data> data = new ArrayList<>();
for (int i = 0 ; i < numValues ; i++) {
colors.add(getRandomColor());
PieChart.Data d = new PieChart.Data("Item "+i, rng.nextDouble() * 100);
data.add( d );
chart.getData().add(d) ;
}
chart.requestLayout();
chart.applyCss();
for (int i = 0 ; i < data.size() ; i++) {
String colorClass = "" ;
for (String cls : data.get(i).getNode().getStyleClass()) {
if (cls.startsWith("default-color")) {
colorClass = cls ;
break ;
}
}
for (Node n : chart.lookupAll("."+colorClass)) {
n.setStyle("-fx-pie-color: "+colors.get(i));
}
}
}
private String getRandomColor() {
Color color = Color.hsb(rng.nextDouble() * 360, 1, 1);
int r = (int) (255 * color.getRed()) ;
int g = (int) (255 * color.getGreen());
int b = (int) (255 * color.getBlue()) ;
return String.format("#%02x%02x%02x", r, g, b) ;
}
public static void main(String[] args) {
Application.launch(args);
}
}
This really is a bit of a hack, so better solutions are obviously welcome.

JavaFX: Fit Columns of TableView to content to avoid cut the content

I have following problem: When i create a TableView and put data in the columns, the columns fit to content automatically. But if there many rows (more than 30) JavaFX optimize the columnwidth to the average length of all content.
In the first example i put the long strings first in the table and everything is fine.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Example extends Application {
#Override
public void start(Stage stage) {
// Init data
ObservableList<Person> persons = FXCollections.observableArrayList();
persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power#test.com"));
persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool#test.com"));
persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool#test.com"));
for (int i = 0; i < 100; i++) {
persons.add(new Person("Max", "Power", "Max.Power#test.com"));
}
// Init table
TableView<Person> table = new TableView<Person>();
table.setMaxWidth(Double.MAX_VALUE);
table.setMaxHeight(Double.MAX_VALUE);
table.setItems(persons);
// Init columns
TableColumn<Person, String> firstname = new TableColumn<Person, String>("Firstname");
firstname.setCellValueFactory(new PropertyValueFactory<Person, String>("firstname"));
table.getColumns().add(firstname);
TableColumn<Person, String> lastname = new TableColumn<Person, String>("Lastname");
lastname.setCellValueFactory(new PropertyValueFactory<Person, String>("lastname"));
table.getColumns().add(lastname);
TableColumn<Person, String> email = new TableColumn<Person, String>("E-Mail");
email.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
table.getColumns().add(email);
// Init Stage
Scene scene = new Scene(table, 400, 150);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
public class Person {
private String firstname;
private String lastname;
private String email;
public Person(String firstname, String lastname, String email) {
this.firstname = firstname;
this.lastname = lastname;
this.email = email;
}
// Getters and Setters
}
}
Looks good...
In the second example i put short strings first in the table and at least the long strings. In this example JavaFX choose columnwidth to small and the content gets cut.
public class Example extends Application {
#Override
public void start(Stage stage) {
// Init data
ObservableList<Person> persons = FXCollections.observableArrayList();
for (int i = 0; i < 100; i++) {
persons.add(new Person("Max", "Power", "Max.Power#test.com"));
}
persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power#test.com"));
persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool#test.com"));
persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool#test.com"));
[...]
Looks bad...
How can i avoid this?​
EDIT 19.05.2018
The links in the comments didn`t work.
So, i have found the problem in the source of JavaFx:
In the updateScene() method the maxRows are set to 30. There is no way to change the value because every methods that are involved are protected or private.
The comment is right. It can be take much time to create a table if there many rows in the table. But sometimes a developer knows the much possible rows of a table or it´s okay to risk higher loadingtime.
One solution is to contact Oracle to create a setter() for the value of max. rows. So the developer can choose the max. rows for each column individually.
public class TableColumnHeader extends Region {
[...]
private void updateScene() {
// RT-17684: If the TableColumn widths are all currently the default,
// we attempt to 'auto-size' based on the preferred width of the first
// n rows (we can't do all rows, as that could conceivably be an unlimited
// number of rows retrieved from a very slow (e.g. remote) data source.
// Obviously, the bigger the value of n, the more likely the default
// width will be suitable for most values in the column
final int n = 30; // ------------------------------------------> This is the problem!
if (! autoSizeComplete) {
if (getTableColumn() == null || getTableColumn().getWidth() != DEFAULT_COLUMN_WIDTH || getScene() == null) {
return;
}
doColumnAutoSize(getTableColumn(), n);
autoSizeComplete = true;
}
}
[...]
private void doColumnAutoSize(TableColumnBase<?,?> column, int cellsToMeasure) {
double prefWidth = column.getPrefWidth();
// if the prefWidth has been set, we do _not_ autosize columns
if (prefWidth == DEFAULT_COLUMN_WIDTH) {
getTableViewSkin().resizeColumnToFitContent(column, cellsToMeasure);
}
}
[...]
}
public class TableViewSkin<T> extends TableViewSkinBase<T, T, TableView<T>, TableViewBehavior<T>, TableRow<T>, TableColumn<T, ?>> {
[...]
#Override protected void resizeColumnToFitContent(TableColumn<T, ?> tc, int maxRows) {
if (!tc.isResizable()) return;
// final TableColumn<T, ?> col = tc;
List<?> items = itemsProperty().get();
if (items == null || items.isEmpty()) return;
Callback/*<TableColumn<T, ?>, TableCell<T,?>>*/ cellFactory = tc.getCellFactory();
if (cellFactory == null) return;
TableCell<T,?> cell = (TableCell<T, ?>) cellFactory.call(tc);
if (cell == null) return;
// set this property to tell the TableCell we want to know its actual
// preferred width, not the width of the associated TableColumnBase
cell.getProperties().put(TableCellSkin.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE);
// determine cell padding
double padding = 10;
Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
if (n instanceof Region) {
Region r = (Region) n;
padding = r.snappedLeftInset() + r.snappedRightInset();
}
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows); // ------------------> if maxRows equals -1 every item will be checked
double maxWidth = 0;
for (int row = 0; row < rows; row++) {
cell.updateTableColumn(tc);
cell.updateTableView(tableView);
cell.updateIndex(row);
if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
getChildren().add(cell);
cell.applyCss();
maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
getChildren().remove(cell);
}
}
// dispose of the cell to prevent it retaining listeners (see RT-31015)
cell.updateIndex(-1);
// RT-36855 - take into account the column header text / graphic widths.
// Magic 10 is to allow for sort arrow to appear without text truncation.
TableColumnHeader header = getTableHeaderRow().getColumnHeaderFor(tc);
double headerTextWidth = Utils.computeTextWidth(header.label.getFont(), tc.getText(), -1);
Node graphic = header.label.getGraphic();
double headerGraphicWidth = graphic == null ? 0 : graphic.prefWidth(-1) + header.label.getGraphicTextGap();
double headerWidth = headerTextWidth + headerGraphicWidth + 10 + header.snappedLeftInset() + header.snappedRightInset();
maxWidth = Math.max(maxWidth, headerWidth);
// RT-23486
maxWidth += padding;
if(tableView.getColumnResizePolicy() == TableView.CONSTRAINED_RESIZE_POLICY) {
maxWidth = Math.max(maxWidth, tc.getWidth());
}
tc.impl_setWidth(maxWidth);
}
[...]
}
Another solution is to fire a MouseEvent on the header of the rect of the TableColumn.
If there a MouseEvent with a ClickCount equals 2 and the PrimaryButton is down the resizeColumnToFitContent() method is called with a value for maxRows of -1.
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows);
-1 means all rows that are in the TableView.
public class NestedTableColumnHeader extends TableColumnHeader {
[...]
private static final EventHandler<MouseEvent> rectMousePressed = new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent me) {
Rectangle rect = (Rectangle) me.getSource();
TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
if (! header.isColumnResizingEnabled()) return;
if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
// the user wants to resize the column such that its
// width is equal to the widest element in the column
header.getTableViewSkin().resizeColumnToFitContent(column, -1); // -----------------------> this method should be call and everything is fine
} else {
// rather than refer to the rect variable, we just grab
// it from the source to prevent a small memory leak.
Rectangle innerRect = (Rectangle) me.getSource();
double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2;
header.dragAnchorX = me.getSceneX();
header.columnResizingStarted(startX);
}
me.consume();
}
};
[...]
}
So, is it possible to create a new MouseEvent with a ClickCount of 2 and the PrimaryButtonDown-boolean is true and fire this to the TableColumn?
And: How can i contact Oracle to please them to create a setter() for the maxRows in the next release?

gridpane javafx clickable cells that get toggled and untoggled

I am trying to create a gridpane that is toggled/untoggled as the user clicks on specific cells. For example, when the user clicks on a cell with the content "*", I would like the content of that specific cell to be changed into a blank cell " ". Similarly, if the user clicks on a cell with a blank content " ", I would like the content of that cell to be changed into a cell with the content "*".
Basically, I am starting out with a grid that is partly input as follows:
My gridpane is initially input as follows, I am keeping track of a matrix of booleans that represents whether there should be a "*" in one cell of the matrix. If there is a "*" in cell [i, j] of the gridpane, then the value of matrix[i,j] should be true, otherwise it should be false.
boolean matrix[][] = new boolean[StateList.size()+1][RequirementList.size()+1];
for( InstanceArtifact li: ListOfLinks) {
int y=1;
for(InstanceArtifact re: RequirementList) {
int x=1;
for(InstanceArtifact state: StateList) {
if(li.getPropertyValue("linksource").equals(re) && li.getPropertyValue("linktarget").equals(state)) {
link= new Label(" * ");
//tick in cell (1,1)
grid.add(link, y, x);
matrix[x][y]= true;
}
else {
link= new Label(" ");
//tick in cell (1,1)
grid.add(link, y, x);
}
x++;
}
y++;
}
}
}
What I would like to do is toggle/untoggle the stars, this is what I am trying to do with the code below, as we are clicking on a cell containing "*", meaning that matrix[i][j]=true, I am removing the corresponding label in the grid and I am adding a new label with an empty text. I also do the same thing in the opposite situation in which the label text is blank and I need to replace it with a label containing a star.
grid.getChildren().forEach(element -> {
element.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
int matrixX= GridPane.getRowIndex(element);
int matrixY= GridPane.getColumnIndex(element);
if(element!=null && matrixX!=0 && matrixY!=0) {
System.out.println("matrixX: "+matrixX+" matrixY: "+matrixY);
System.out.println("matrixY: "+matrixY+" "+ matrix[matrixX][matrixY]);
if(matrix[matrixX][matrixY]==true && matrixX!=0 && matrixY!=0) {
System.out.println("HEY I AM HERE FIRSTTTTTTTTT");
Node newnode= new Label(" ");
GridPane.clearConstraints(element);
// grid.getChildren().remove(element);
grid.add(newnode, matrixY, matrixX);
matrix[matrixX][matrixY]=false;
/*for(int l=0; l<RequirementList.size(); l++) {
for(int p=0; p<StateList.size(); p++) {
System.out.println(l + " "+p +" "+matrix[l][p]);
}
}*/
//grid.add(mynode, matrixY+1, matrixX+1, 1, 1);
}
else if(matrix[matrixX][matrixY]==false && matrixX!=0 && matrixY!=0){
System.out.println("HEY I AM HERE SECONDDDDDDD ");
/* for(int l=0; l<RequirementList.size(); l++) {
for(int p=0; p<StateList.size(); p++) {
System.out.println(l + " "+p +" "+matrix[l][p]);
}
}*/
Node falsenode= new Label(" * ");
GridPane.clearConstraints(element);
// grid.getChildren().remove(element);
grid.add(falsenode, matrixY, matrixX);
matrix[matrixX][matrixY]=true;
}
// System.out.println("Row: " + GridPane.getRowIndex(element));
// System.out.println("Column: " + GridPane.getColumnIndex(element));
}
}
});
});
My code is not behaving as expected, I would like the cell to be toggled/untoggled whenever the user clicks on a cell, the code is executed only the first time the user clicks on a given cell, if the user clicks on the same cell multiple times (more than once), then nothing happens.
Also, the line of code in which I am trying to remove a label is not working:
grid.getChildren().remove(element);
This could be better served with ToggleButton. Comments in code.
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
/**
*
* #author Sedrick
*/
public class JavaFXApplication60 extends Application {
#Override
public void start(Stage primaryStage) {
//Start create GUI
Label lblRequirement11 = new Label("Requirement1");
Label lblRequirement12 = new Label("Requirement2");
Label lblRequirement13 = new Label("Requirement3");
GridPane.setConstraints(lblRequirement11, 1, 0);
GridPane.setConstraints(lblRequirement12, 2, 0);
GridPane.setConstraints(lblRequirement13, 3, 0);
Label lblState1 = new Label("State1");
ToggleButton toggleButton11 = new ToggleButton();
toggleButton11.setMaxWidth(Double.MAX_VALUE);
ToggleButton toggleButton12 = new ToggleButton();
toggleButton12.setMaxWidth(Double.MAX_VALUE);
ToggleButton toggleButton13 = new ToggleButton();
toggleButton13.setMaxWidth(Double.MAX_VALUE);
GridPane.setConstraints(lblState1, 0, 1);
GridPane.setConstraints(toggleButton11, 1, 1);
GridPane.setConstraints(toggleButton12, 2, 1);
GridPane.setConstraints(toggleButton13, 3, 1);
Label lblState2 = new Label("State2");
ToggleButton toggleButton21 = new ToggleButton();
toggleButton21.setMaxWidth(Double.MAX_VALUE);
ToggleButton toggleButton22 = new ToggleButton();
toggleButton22.setMaxWidth(Double.MAX_VALUE);
ToggleButton toggleButton23 = new ToggleButton();
toggleButton23.setMaxWidth(Double.MAX_VALUE);
GridPane.setConstraints(lblState2, 0, 2);
GridPane.setConstraints(toggleButton21, 1, 2);
GridPane.setConstraints(toggleButton22, 2, 2);
GridPane.setConstraints(toggleButton23, 3, 2);
Label lblState3 = new Label("State3");
ToggleButton toggleButton31 = new ToggleButton();
toggleButton31.setMaxWidth(Double.MAX_VALUE);
ToggleButton toggleButton32 = new ToggleButton();
toggleButton32.setMaxWidth(Double.MAX_VALUE);
ToggleButton toggleButton33 = new ToggleButton();
toggleButton33.setMaxWidth(Double.MAX_VALUE);
GridPane.setConstraints(lblState3, 0, 3);
GridPane.setConstraints(toggleButton31, 1, 3);
GridPane.setConstraints(toggleButton32, 2, 3);
GridPane.setConstraints(toggleButton33, 3, 3);
GridPane root = new GridPane();
root.setVgap(5);
root.setHgap(5);
root.getChildren().addAll(lblRequirement11, lblRequirement12, lblRequirement13);
root.getChildren().addAll(lblState1, toggleButton11, toggleButton12, toggleButton13);
root.getChildren().addAll(lblState2, toggleButton21, toggleButton22, toggleButton23);
root.getChildren().addAll(lblState3, toggleButton31, toggleButton32, toggleButton33);
//End create GUI
//Start create ToggleButtons' event handlers.
List<ToggleButton> toggleButtonList = new ArrayList();
toggleButtonList.add(toggleButton11);
toggleButtonList.add(toggleButton12);
toggleButtonList.add(toggleButton13);
toggleButtonList.add(toggleButton21);
toggleButtonList.add(toggleButton22);
toggleButtonList.add(toggleButton23);
toggleButtonList.add(toggleButton31);
toggleButtonList.add(toggleButton32);
toggleButtonList.add(toggleButton33);
for(ToggleButton tempToggleButton : toggleButtonList)
{
tempToggleButton.setOnAction(actionEvent -> {
if(tempToggleButton.isSelected())
{
tempToggleButton.setText("*");
}
else
{
tempToggleButton.setText("");
}
});
}
////End create ToggleButtons' event handlers.
Scene scene = new Scene(root, 400, 300);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}

How to detect mouse movement over node while button is pressed?

Problem
You can add an event listener to a node which detects mouse movement over it. This doesn't work if a mouse button was pressed before you moved over the node.
Question
Does anyone know how to detect mouse movement while the button is pressed? So far I've only found a solution by using the MOUSE_DRAGGED event and then instead of using getSource() using getPickResult() and evaluating the PickResult data.
Here's the code including Uluk's solution. The old and new solution are switchable via the useNewVersion (Uluk's version) boolean:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.PickResult;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
boolean useNewVersion= true;
int rows = 10;
int columns = 20;
double width = 1024;
double height = 768;
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = new BorderPane();
// create grid
Grid grid = new Grid( columns, rows, width, height);
MouseGestures mg = new MouseGestures();
// fill grid
for (int row = 0; row < rows; row++) {
for (int column = 0; column < columns; column++) {
Cell cell = new Cell(column, row);
mg.makePaintable(cell);
grid.add(cell, column, row);
}
}
root.setCenter(grid);
// create scene and stage
Scene scene = new Scene(root, width, height);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
private class Grid extends Pane {
int rows;
int columns;
double width;
double height;
Cell[][] cells;
public Grid( int columns, int rows, double width, double height) {
this.columns = columns;
this.rows = rows;
this.width = width;
this.height = height;
cells = new Cell[rows][columns];
}
/**
* Add cell to array and to the UI.
*/
public void add(Cell cell, int column, int row) {
cells[row][column] = cell;
double w = width / columns;
double h = height / rows;
double x = w * column;
double y = h * row;
cell.setLayoutX(x);
cell.setLayoutY(y);
cell.setPrefWidth(w);
cell.setPrefHeight(h);
getChildren().add(cell);
}
}
private class Cell extends StackPane {
int column;
int row;
public Cell(int column, int row) {
this.column = column;
this.row = row;
getStyleClass().add("cell");
Label label = new Label(this.toString());
getChildren().add(label);
}
public void highlight() {
getStyleClass().add("cell-highlight");
}
public void unhighlight() {
getStyleClass().remove("cell-highlight");
}
public String toString() {
return this.column + "/" + this.row;
}
}
public class MouseGestures {
public void makePaintable( Node node) {
if( useNewVersion) {
node.setOnMousePressed( onMousePressedEventHandler);
node.setOnDragDetected( onDragDetectedEventHandler);
node.setOnMouseDragEntered( onMouseDragEnteredEventHandler);
} else {
node.setOnMousePressed( onMousePressedEventHandler);
node.setOnMouseDragged( onMouseDraggedEventHandler);
node.setOnMouseReleased( onMouseReleasedEventHandler);
}
}
/* old version */
EventHandler<MouseEvent> onMousePressedEventHandler = event -> {
Cell cell = (Cell) event.getSource();
if( event.isPrimaryButtonDown()) {
cell.highlight();
} else if( event.isSecondaryButtonDown()) {
cell.unhighlight();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = event -> {
PickResult pickResult = event.getPickResult();
Node node = pickResult.getIntersectedNode();
if( node instanceof Cell) {
Cell cell = (Cell) node;
if( event.isPrimaryButtonDown()) {
cell.highlight();
} else if( event.isSecondaryButtonDown()) {
cell.unhighlight();
}
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = event -> {
};
EventHandler<MouseEvent> onDragDetectedEventHandler = event -> {
Cell cell = (Cell) event.getSource();
cell.startFullDrag();
};
EventHandler<MouseEvent> onMouseDragEnteredEventHandler = event -> {
Cell cell = (Cell) event.getSource();
if( event.isPrimaryButtonDown()) {
cell.highlight();
} else if( event.isSecondaryButtonDown()) {
cell.unhighlight();
}
};
}
}
In the end you should be able to paint via primary mouse button and erase the paint via secondary mouse button:
One solution is to add an event filter to the scene which enables the sourceNode.startFullDrag(). This will work even if you start dragging the mouse outside of your canvas (if you want any space without nodes in your application).
Like this:
scene.addEventFilter(MouseEvent.DRAG_DETECTED , new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
scene.startFullDrag();
}
});
And then you could:
node.setOnMouseDragEntered(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
led.setOn(true);
}
});
The (source) node which handles the initial DRAG_DETECTED event should invoke sourceNode.startFullDrag(), then the target node will able to handle one of MouseDragEvents, for instance MOUSE_DRAG_OVER or MOUSE_DRAG_ENTERED event with respective targetNode.setOn<MouseDragEvent>() method.

JavaFx 8: Moving component between parents while staying in place

I need to move Nodes (mosty clipped ImageViews) between containers. During the "parent change" I have to keep the components visually in place, so from the users' perspective this reorganization should be transparent.
The schema of my scene is like this:
The Nodes I have to move are clipped, translated some effects applied to them and originally reside under the Board pane. The contents group of the desk is scaled, clipped and translated.
I would like to move them to the MoverLayer. It works fine, due to the moverLayer is bound to the Board:
moverLayer.translateXProperty().bind(board.translateXProperty());
moverLayer.translateYProperty().bind(board.translateYProperty());
moverLayer.scaleXProperty().bind(board.scaleXProperty());
moverLayer.scaleYProperty().bind(board.scaleYProperty());
moverLayer.layoutXProperty().bind(board.layoutXProperty());
moverLayer.layoutYProperty().bind(board.layoutYProperty());
so I can simply move the nodes between them:
public void start(MouseEvent me) {
board.getContainer().getChildren().remove(node);
desk.getMoverLayer().getChildren().add(node);
}
public void finish(MouseEvent me) {
desk.getMoverLayer().getChildren().remove(node);
board.getContainer().getChildren().add(node);
}
However, when moving nodes between the contents of tray and the MoverLayer it starts to get complicated. I tried to play with different coordinates (local, parent, scene, screen), but somehow it is always misplaced. It seems, that when the scale is 1.0 for desk.contents, it works to map the coordinates of translateX and translateY to screen coordinates, switch the parent and then map back the screen coordinates to local and use as translation. But with non-identical scaling, the coordinates differs (and the node moves). I also tried to map the coordinates to the common parent (desk) recursively, but works neither.
My generalized question is, what is the best practice to calculate coordinates of the same point relative to different parents?
Here is the MCVE code. Sorry, I simply couldn't make it more simple.
package hu.vissy.puzzlefx.stackoverflow;
import javafx.application.Application;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Mcve extends Application {
// The piece to move
private Rectangle piece;
// The components representing the above structure
private Pane board;
private Group moverLayer;
private Pane trayContents;
private Pane desk;
private Group deskContents;
// The zoom scale
private double scale = 1;
// Drag info
private double startDragX;
private double startDragY;
private Point2D dragAnchor;
public Mcve() {
}
public void init(Stage primaryStage) {
// Added only for simulation
Button b = new Button("Zoom");
b.setOnAction((ah) -> setScale(0.8));
desk = new Pane();
desk.setPrefSize(800, 600);
desk.setBackground(new Background(new BackgroundFill(Color.LIGHTGREEN, CornerRadii.EMPTY, Insets.EMPTY)));
deskContents = new Group();
desk.getChildren().add(deskContents);
board = new Pane();
board.setPrefSize(700, 600);
board.setBackground(new Background(new BackgroundFill(Color.LIGHTCORAL, CornerRadii.EMPTY, Insets.EMPTY)));
// Symbolize the piece to be dragged
piece = new Rectangle();
piece.setTranslateX(500);
piece.setTranslateY(50);
piece.setWidth(50);
piece.setHeight(50);
piece.setFill(Color.BLACK);
board.getChildren().add(piece);
// Mover layer is always on top and is bound to the board (used to display the
// dragged items above all desk contents during dragging)
moverLayer = new Group();
moverLayer.translateXProperty().bind(board.translateXProperty());
moverLayer.translateYProperty().bind(board.translateYProperty());
moverLayer.scaleXProperty().bind(board.scaleXProperty());
moverLayer.scaleYProperty().bind(board.scaleYProperty());
moverLayer.layoutXProperty().bind(board.layoutXProperty());
moverLayer.layoutYProperty().bind(board.layoutYProperty());
board.setTranslateX(50);
board.setTranslateY(50);
Pane tray = new Pane();
tray.setPrefSize(400, 400);
tray.relocate(80, 80);
Pane header = new Pane();
header.setPrefHeight(30);
header.setBackground(new Background(new BackgroundFill(Color.LIGHTSLATEGRAY, CornerRadii.EMPTY, Insets.EMPTY)));
trayContents = new Pane();
trayContents.setBackground(new Background(new BackgroundFill(Color.BEIGE, CornerRadii.EMPTY, Insets.EMPTY)));
VBox layout = new VBox();
layout.getChildren().addAll(header, trayContents);
VBox.setVgrow(trayContents, Priority.ALWAYS);
layout.setPrefSize(400, 400);
tray.getChildren().add(layout);
deskContents.getChildren().addAll(board, tray, moverLayer, b);
Scene scene = new Scene(desk);
// Piece is draggable
piece.setOnMousePressed((me) -> startDrag(me));
piece.setOnMouseDragged((me) -> doDrag(me));
piece.setOnMouseReleased((me) -> endDrag(me));
primaryStage.setScene(scene);
}
// Changing the scale
private void setScale(double scale) {
this.scale = scale;
// Reseting piece position and parent if needed
if (piece.getParent() != board) {
piece.setTranslateX(500);
piece.setTranslateY(50);
trayContents.getChildren().remove(piece);
board.getChildren().add(piece);
}
deskContents.setScaleX(getScale());
deskContents.setScaleY(getScale());
}
private double getScale() {
return scale;
}
private void startDrag(MouseEvent me) {
// Saving drag options
startDragX = piece.getTranslateX();
startDragY = piece.getTranslateY();
dragAnchor = new Point2D(me.getSceneX(), me.getSceneY());
// Putting the item into the mover layer -- works fine with all zoom scale level
board.getChildren().remove(piece);
moverLayer.getChildren().add(piece);
me.consume();
}
// Doing the drag
private void doDrag(MouseEvent me) {
double newTranslateX = startDragX + (me.getSceneX() - dragAnchor.getX()) / getScale();
double newTranslateY = startDragY + (me.getSceneY() - dragAnchor.getY()) / getScale();
piece.setTranslateX(newTranslateX);
piece.setTranslateY(newTranslateY);
me.consume();
}
private void endDrag(MouseEvent me) {
// For MCVE's sake I take that the drop is over the tray.
Bounds op = piece.localToScreen(piece.getBoundsInLocal());
moverLayer.getChildren().remove(piece);
// One of my several tries: mapping the coordinates till the common parent.
// I also tried to use localtoScreen -> change parent -> screenToLocal
Bounds b = localToParentRecursive(trayContents, desk, trayContents.getBoundsInLocal());
Bounds b2 = localToParentRecursive(board, desk, board.getBoundsInLocal());
trayContents.getChildren().add(piece);
piece.setTranslateX(piece.getTranslateX() + b2.getMinX() - b.getMinX() * getScale());
piece.setTranslateY(piece.getTranslateY() + b2.getMinY() - b.getMinY() * getScale());
me.consume();
}
public static Point2D localToParentRecursive(Node n, Parent parent, double x, double y) {
// For simplicity I suppose that the n node is on the path of the parent
Point2D p = new Point2D(x, y);
Node cn = n;
while (true) {
if (cn == parent) {
break;
}
p = cn.localToParent(p);
cn = cn.getParent();
}
return p;
}
public static Bounds localToParentRecursive(Node n, Parent parent, Bounds bounds) {
Point2D p = localToParentRecursive(n, parent, bounds.getMinX(), bounds.getMinY());
return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Well. After a lot of debugging, calculation and try I was able to find a solution.
Here is the utility function I wrote for doing the parent swap:
/**
* Change the parent of a node.
*
* <p>
* The node should have a common ancestor with the new parent.
* </p>
*
* #param item
* The node to move.
* #param newParent
* The new parent.
*/
#SuppressWarnings("unchecked")
public static void changeParent(Node item, Parent newParent) {
try {
// HAve to use reflection, because the getChildren method is protected in common ancestor of all
// parent nodes.
// Checking old parent for public getChildren() method
Parent oldParent = item.getParent();
if ((oldParent.getClass().getMethod("getChildren").getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) {
throw new IllegalArgumentException("Old parent has no public getChildren method.");
}
// Checking new parent for public getChildren() method
if ((newParent.getClass().getMethod("getChildren").getModifiers() & Modifier.PUBLIC) != Modifier.PUBLIC) {
throw new IllegalArgumentException("New parent has no public getChildren method.");
}
// Finding common ancestor for the two parents
Parent commonAncestor = findCommonAncestor(oldParent, newParent);
if (commonAncestor == null) {
throw new IllegalArgumentException("Item has no common ancestor with the new parent.");
}
// Bounds of the item
Bounds itemBoundsInParent = item.getBoundsInParent();
// Mapping coordinates to common ancestor
Bounds boundsInParentBeforeMove = localToParentRecursive(oldParent, commonAncestor, itemBoundsInParent);
// Swapping parent
((Collection<Node>) oldParent.getClass().getMethod("getChildren").invoke(oldParent)).remove(item);
((Collection<Node>) newParent.getClass().getMethod("getChildren").invoke(newParent)).add(item);
// Mapping coordinates back from common ancestor
Bounds boundsInParentAfterMove = parentToLocalRecursive(newParent, commonAncestor, boundsInParentBeforeMove);
// Setting new translation
item.setTranslateX(
item.getTranslateX() + (boundsInParentAfterMove.getMinX() - itemBoundsInParent.getMinX()));
item.setTranslateY(
item.getTranslateY() + (boundsInParentAfterMove.getMinY() - itemBoundsInParent.getMinY()));
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new IllegalStateException("Error while switching parent.", e);
}
}
/**
* Finds the topmost common ancestor of two nodes.
*
* #param firstNode
* The first node to check.
* #param secondNode
* The second node to check.
* #return The common ancestor or null if the two node is on different
* parental tree.
*/
public static Parent findCommonAncestor(Node firstNode, Node secondNode) {
// Builds up the set of all ancestor of the first node.
Set<Node> parentalChain = new HashSet<>();
Node cn = firstNode;
while (cn != null) {
parentalChain.add(cn);
cn = cn.getParent();
}
// Iterates down through the second ancestor for common node.
cn = secondNode;
while (cn != null) {
if (parentalChain.contains(cn)) {
return (Parent) cn;
}
cn = cn.getParent();
}
return null;
}
/**
* Transitively converts the coordinates from the node to an ancestor's
* coordinate system.
*
* #param node
* The node the starting coordinates are local to.
* #param ancestor
* The ancestor to map the coordinates to.
* #param x
* The X of the point to be converted.
* #param y
* The Y of the point to be converted.
* #return The converted coordinates.
*/
public static Point2D localToParentRecursive(Node node, Parent ancestor, double x, double y) {
Point2D p = new Point2D(x, y);
Node cn = node;
while (cn != null) {
if (cn == ancestor) {
return p;
}
p = cn.localToParent(p);
cn = cn.getParent();
}
throw new IllegalStateException("The node is not a descedent of the parent.");
}
/**
* Transitively converts the coordinates of a bound from the node to an
* ancestor's coordinate system.
*
* #param node
* The node the starting coordinates are local to.
* #param ancestor
* The ancestor to map the coordinates to.
* #param bounds
* The bounds to be converted.
* #return The converted bounds.
*/
public static Bounds localToParentRecursive(Node node, Parent ancestor, Bounds bounds) {
Point2D p = localToParentRecursive(node, ancestor, bounds.getMinX(), bounds.getMinY());
return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}
/**
* Transitively converts the coordinates from an ancestor's coordinate
* system to the nodes local.
*
* #param node
* The node the resulting coordinates should be local to.
* #param ancestor
* The ancestor the starting coordinates are local to.
* #param x
* The X of the point to be converted.
* #param y
* The Y of the point to be converted.
* #return The converted coordinates.
*/
public static Point2D parentToLocalRecursive(Node n, Parent parent, double x, double y) {
List<Node> parentalChain = new ArrayList<>();
Node cn = n;
while (cn != null) {
if (cn == parent) {
break;
}
parentalChain.add(cn);
cn = cn.getParent();
}
if (cn == null) {
throw new IllegalStateException("The node is not a descedent of the parent.");
}
Point2D p = new Point2D(x, y);
for (int i = parentalChain.size() - 1; i >= 0; i--) {
p = parentalChain.get(i).parentToLocal(p);
}
return p;
}
/**
* Transitively converts the coordinates of the bounds from an ancestor's
* coordinate system to the nodes local.
*
* #param node
* The node the resulting coordinates should be local to.
* #param ancestor
* The ancestor the starting coordinates are local to.
* #param bounds
* The bounds to be converted.
* #return The converted coordinates.
*/
public static Bounds parentToLocalRecursive(Node n, Parent parent, Bounds bounds) {
Point2D p = parentToLocalRecursive(n, parent, bounds.getMinX(), bounds.getMinY());
return new BoundingBox(p.getX(), p.getY(), bounds.getWidth(), bounds.getHeight());
}
The above solution works well, but I wonder if it is the simpliest way to do the task. For the sake of generality I had to use some reflection: the getChildren() method of the Parent class is protected, only its descedents make it public if they wish, so I can't call it directly through Parent.
The use of the above utility is simple: call changeParent( node, newParent ).
This utilities gives also function to find the common ancestor of two nodes and to recursively convert coordinates through the nodes ancestal chain to and from.

Resources