I am attempting to create a "draggable" histogram UI with JavaFX. I have a ScrollPane containing a GridPane with 1 column and lots of rows. In each row is an HBox containing a label. Every 10 rows, there is also an HBox containing a Line.
I tried to make the HBoxes containing lines draggable by setting onMousePressed, onMouseDragged, and onMouseReleased event handlers (shown below). It works if I drag and release an hbox-line above its starting point - it ends up in whatever grid row I put it in, and I can click and drag it again. However, if I drag and release a line below its starting point, I can't get any more mouseEvents for that hBox. I tried adding log statements everywhere, nothing. I tried setting onMouseOver, it also was not fired.
Why would moving an hbox around the grid like this work for dragging up but not down?
lineContainer.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
EventTarget target = mouseEvent.getTarget();
lastY = mouseEvent.getSceneY();
}
});
lineContainer.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
Node target = (Node) mouseEvent.getTarget();
HBox hBox = null;
if (target instanceof HBox) {
hBox = (HBox) target;
}
else if (target instanceof Line) {
hBox = (HBox) target.getParent();
}
else { //should never happen
log.info("target not hbox or line: " + target.getClass());
}
if (mouseEvent.getSceneY() <= (lastY - 15)) {
int row = GridPane.getRowIndex(hBox);
GridPane.setRowIndex(hBox, --row);
lastY = mouseEvent.getSceneY();
lastRow = row - 1;
} else if (mouseEvent.getSceneY() >= (lastY + 15)) {
int row = GridPane.getRowIndex(hBox);
GridPane.setRowIndex(hBox, ++row);
lastRow = row - 1;
lastY = mouseEvent.getSceneY();
}
}
});
lineContainer.setOnMouseReleased(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
Node tar = (Node) mouseEvent.getTarget();
HBox hBox = null;
if (tar instanceof HBox) {
hBox = (HBox) tar;
}
else if (tar instanceof Line && tar.getParent() instanceof HBox) {
hBox = (HBox) tar.getParent();
}
else { //should never happen
log.info(mouseEvent.getTarget().getClass().toString());
}
}
});
UPDATE: I managed to get it working by creating a new HBox, resetting the onMouse... handlers, and copying its children every time the mouse is released. But I still don't know what was causing the original issue...
The following isn't a direct solution for you, but I wanted to say I have a similar problem and share my observations.
My application allows dragging in both axes (X, Y). All I've been able to figure out that some invisible element is 'obscuring' the MouseEvent hitbox. Testing it using an 'MS minesweeper' approach, shows this interfering area to extend from coords (0,0) of the root to (maxX,maxY) of another Node I have in the scene which is a layer above.
My problem was solved by changing z-order of Parent objects (let's call them layers) containing the Nodes that didn't receive their MouseEvents.
JavaFX MouseEvent doc explains that it is only the top level node which receives the event:
https://docs.oracle.com/javafx/2/api/javafx/scene/input/MouseEvent.html
Also look at the pickOnBounds property: JavaFX: How to make a Node partially mouse transparent?
Related
I currently have some stack panes on a pane and when dragged with a mouse they move around the pane. I do this by getting the coordinate of the mouse
and the translate x and y of the stack pane when I press the the stack pane. Then when I start to drag the stack pane I set the the translation x and y of the stack pane to the mouse coordinates when I pressed the stack pane + the difference of the new mouse coordinates and the old mouse coordinates.
My problem is after dragging the StackPane the layout x and y stays the same I want to update this as I used this else where.
My event handler when you press the StackPane:
EventHandler<MouseEvent> circleOnMousePressedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
currentStackPane = ((StackPane)(t.getSource()));
orgSceneX = t.getSceneX();
orgSceneY = t.getSceneY();
layoutX = currentStackPane.getLayoutX();
layoutY = currentStackPane.getLayoutY();
}
};
My event handler when i drag the StackPane:
EventHandler<MouseEvent> circleOnMouseDraggedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
double offsetX = t.getSceneX() - orgSceneX;
double offsetY = t.getSceneY() - orgSceneY;
currentStackPane.setTranslateX(offsetX);
currentStackPane.setTranslateY(offsetY);
}
};
I tried make a event handler after the drag is finished:
EventHandler<MouseEvent> circleOnMouseReleasedEventHandler =
new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
currentStackPane.setLayoutX(layoutX + ((StackPane)(t.getSource())).getTranslateX());
currentStackPane.setLayoutY(layoutY + ((StackPane)(t.getSource())).getTranslateY());
currentStackPane.setTranslateX(0);
currentStackPane.setTranslateY(0);
}
};
But this doesn't seem to work. Any help would be appreciated thanks!
EDIT:
I have changed my event handlers. It seems to be updating the layout x and y correctly for the first time I drag the stack pane but when i first drag the stack pane and then release the mouse the stackpane moves to a different position then every time i drag after it messes up completely. Not sure why, any help appreciated!
EDIT2: I realised I set translate x to 0 but didnt set translate y to 0 in the mouse released event. It all works now!
To understand the problem, I would first recommend to have a look at the documentation of layoutX/layoutY properties of a Node.
public final DoubleProperty layoutXProperty
Defines the x coordinate of the translation that is added to this
Node's transform for the purpose of layout. The value should be
computed as the offset required to adjust the position of the node
from its current layoutBounds minX position (which might not be 0) to
the desired location.
For example, if textnode should be positioned at finalX
textnode.setLayoutX(finalX - textnode.getLayoutBounds().getMinX());
Failure to subtract layoutBounds minX may result in misplacement of
the node. The relocate(x, y) method will automatically do the correct
computation and should generally be used over setting layoutX
directly.
The node's final translation will be computed as layoutX + translateX,
where layoutX establishes the node's stable position and translateX
optionally makes dynamic adjustments to that position.
If the node is managed and has a Region as its parent, then the layout
region will set layoutX according to its own layout policy. If the
node is unmanaged or parented by a Group, then the application may set
layoutX directly to position it.
In short,for every node that is rendered in the scene, its position is actually a sum of its layoutX/Y and translateX/Y values in relative to its parent node. The layoutX/Y are initially updated as per its parents layout policy. For that reason, there is no point in updating/relying on layoutX/Y values of node, IF its parent (eg,.StackPane,HBox,VBox,..etc) manages it position.
Pane will not manage/decide its children layout. For that reason the default layoutX/Y values of its children is always 0.
From the above info,if we now look into your code, you are updating the translate values and setting the layout values wrongly. Instead what you have to actually do is:
Take intial values of layoutX/Y.
Update the translateX/Y while dragging.
And on mouse released recompute layoutX/Y values and reset
translateX/Y values.
Below is a quick demo of what I have described.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class PaneLayoutDemo extends Application {
double sceneX, sceneY, layoutX, layoutY;
#Override
public void start(Stage stage) throws Exception {
Pane root = new Pane();
Scene sc = new Scene(root, 600, 600);
stage.setScene(sc);
stage.show();
root.getChildren().addAll(getBox("green"), getBox("red"), getBox("yellow"));
}
private StackPane getBox(String color) {
StackPane box = new StackPane();
box.getChildren().add(new Label("Drag me !!"));
box.setStyle("-fx-background-color:" + color + ";-fx-border-width:2px;-fx-border-color:black;");
box.setPrefSize(150, 150);
box.setMaxSize(150, 150);
box.setMinSize(150, 150);
box.setOnMousePressed(e -> {
sceneX = e.getSceneX();
sceneY = e.getSceneY();
layoutX = box.getLayoutX();
layoutY = box.getLayoutY();
System.out.println(color.toUpperCase() + " Box onStart :: layoutX ::" + layoutX + ", layoutY::" + layoutY);
});
box.setOnMouseDragged(e -> {
double offsetX = e.getSceneX() - sceneX;
double offsetY = e.getSceneY() - sceneY;
box.setTranslateX(offsetX);
box.setTranslateY(offsetY);
});
box.setOnMouseReleased(e -> {
// Updating the new layout positions
box.setLayoutX(layoutX + box.getTranslateX());
box.setLayoutY(layoutY + box.getTranslateY());
// Resetting the translate positions
box.setTranslateX(0);
box.setTranslateY(0);
});
return box;
}
public static void main(String[] args) {
Application.launch(args);
}
}
Once you are familiar with the demo, try changing the root from Pane to StackPane and see the behaviour difference.
In my GUI I create some dices (ImageView) and I can drag and drop them in one specific GridPane. When I drop a dice into the specific GridPane, the dice disappears from the initial location and it moves to the right position. This works fine only if I choose the right drop location.
The problem is how can I manage the wrong drop location?
Actually if I drop a dice in a wrong location (like outside the Gridpane) the dice disappears like it was moved to the right position.
I want to restore the dice to the original location if the dice isn't placed to the GridPane.
Is there a method can help me to check if I drop into the right location? Or something can prevent to drop into the wrong location?
You can check the transferMode property of the DragEvent passed to the onDragDone event:
dragSource.setOnDragDone(evt -> {
if (evt.getTransferMode() == null) {
System.out.println("drag aborted");
} else {
System.out.println("drag successfully completed");
}
});
Note: this requires you to mark the drag gesture as completed in the onDragDropped event handler using setDropCompleted. Example:
#Override
public void start(Stage primaryStage) {
Button source = new Button("Not dragged yet");
Button target = new Button("target");
HBox root = new HBox(20, source, target);
source.setOnDragDetected(evt -> {
Dragboard db = source.startDragAndDrop(TransferMode.COPY);
ClipboardContent content = new ClipboardContent();
content.putString(source.getText());
db.setContent(content);
});
source.setOnDragDone(evt -> {
source.setText(evt.getTransferMode() == null ? "failure" : "success");
});
target.setOnDragOver(evt -> {
if (evt.getDragboard().hasString()) {
evt.acceptTransferModes(TransferMode.COPY);
evt.consume();
}
});
target.setOnDragDropped(evt -> {
String value = evt.getDragboard().getString();
target.setText(value);
evt.setDropCompleted(true);
evt.consume();
});
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
I am creating a javafx application with a drag and drop feature. I need to be able to drag from one pane and drop to the next pane. After dropping, all dropped nodes need to be accessible. The drag and drop is working smoothly. However, I am having a problem in which not all nodes are accessible after being dropped. I would drag a node and click on it and sometimes it would respond, but sometimes a random node would no longer exhibit capabilities when I clicked on it. There was no noticeable pattern (that I could see) about which nodes stopped working and which always worked. If anyone has any idea as to why this might be a problem or how to fix it, it would be greatly appreciated. I have looked everywhere and found only one similar issue a few years ago but the page with their solution had been removed.
I will briefly explain how I set up my code structure.
The nodes I am dragging extend StackPane and I drag these nodes onto a center Pane from an ImageView in VBox. However, both of these are children of a Main layout that extends BorderPane which I have called MainEntrance.
In my node's class I have onMousePressed and onMouseDragged features as well as onContextMenuRequested features. In the class of the MainEntrance, I have drag features, as well, that handle when an object is first dragged onto the child Pane from the other child VBox.
The following are the relevant classes/methods:
VBox class: (where nodes are dragged from)
ImageView iV= new ImageView(image);
iV.setId("image");
iV.setOnDragDetected(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
dragAndDrop(iV, iV.getId(), event);
}
});
I have drag three ImageViews and these ImageViews are children of a VBox.
MainEntrance:
In this class I have a method that registers my drag events to my pane as such:
PView is my class extension of Pane
public void registerOnDragDropped(PView pView){
pView.setOnDragOver(new EventHandler<DragEvent>() {
#Override
public void handle(DragEvent event) {
event.acceptTransferModes(TransferMode.ANY);
System.out.println("drag over registered");
}
});
pView.setOnDragDropped(new EventHandler<DragEvent>() {
#Override
public void handle(DragEvent event) {
Dragboard db = event.getDragboard();
final String id = db.getString();
double mousePosY = event.getY();
double mousePosX = event.getX();
deviceCounter += 1;
pView.createBoardView(id, deviceCounter, mousePosX, mousePosY);
event.setDropCompleted(true);
}
});
pView.setOnDragExited(new EventHandler<DragEvent>() {
#Override
public void handle(DragEvent event) {
event.acceptTransferModes(TransferMode.ANY);
//computerView.setEffect(null);
event.consume();
}
});
}
Note: this method is in my MainEntrance class (BorderPane). In this class is when I initialize all other child nodes of MainEntrance.
Node: (draggable nodes extend StackPane)
my node's handlers are in a method that I called makeDraggable()
public void makeDraggable(Node node) {
this.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
node.getScene().setCursor(Cursor.MOVE);
System.out.println("makeDraggable mouse pressed");
}
});
node.setOnMouseReleased(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
node.getScene().setCursor(Cursor.DEFAULT);
}
});
node.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
node.setTranslateX(node.getTranslateX() + event.getX() - 35);
node.setTranslateY(node.getTranslateY() + event.getY() - 35);
event.consume();
node.toFront();
}
});
}
If any other information is needed please let me know. Thank you in advance!
I have figured out the solution to this problem.
The problem was in the rendering order of an attachment I had placed on the node. So when I drag the node on to the screen, I have another node (a line placed on a Pane) that binds the center of the screen to the node. I did not initially think that this was the problem, so I did not even include that class or snippet. However, after playing around with it I realized that this indeed was the problem. I had to add the line to the top of the rendering order. This can be done in the following way in whatever method allows you to create a new node:
Pane line = new Pane();
//add whatever code binds your line to desired places
getChildren.add(0, line);
//add the other node as child
I have an Accordian with multiple TitledPanes. When a TitledPane is expanded, there are "dead areas" on the pane that do not have sub-components (e.g., buttons, text, etc.).
Right now, when I check MouseEvent.getSource(), it returns an instance of TitledPane for all areas. Is there a way to specifically constrain/check for a mouse-click on the "title" section of the TitledPane?
I don't think there is a public API to detect mouse click on title region, however it is possible to do that this way:
titledPane.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
EventTarget target = event.getTarget();
String clazz = "class com.sun.javafx.scene.control.skin.TitledPaneSkin$TitleRegion";
if(target.getClass().toString().equals(clazz) || // anywhere on title region except title Text
(target instanceof Node && ((Node) target).getParent().getClass().toString().equals(clazz))) // title Text
System.out.println("title was clicked");
}
});
But this method is highly discouraged as it relies on some internal implementation detail that may be subject to change.
May be it's better to think more about what you actually need. Maybe your actual requirement can be fulfilled by uisng TitledPane's public API methods and properties.
For insatnce expandedProperty()'s value gets changed every time mouse click occurs on title region (if isCollapsible() is set to true).
titledPane.expandedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
System.out.println("mouse click changed expanded from " + oldValue + " to " + newValue);
}
});
In the css reference you can find out that there is a child of the TitledPane that has the style class title. It isn't hard to guess that this part is the title. You can go from the pick result up through the scene graph until you either find a node with the style class title or until you reach the Accordion.
The following code colors the rect below the Accordion green, iff the mouse is on a title and red otherwise:
#Override
public void start(Stage primaryStage) {
TitledPane tp1 = new TitledPane("foo", new Rectangle(100, 100));
TitledPane tp2 = new TitledPane("bar", new Circle(100));
Accordion accordion = new Accordion(tp1, tp2);
Rectangle rect = new Rectangle(100, 20, Color.RED);
accordion.addEventFilter(MouseEvent.MOUSE_MOVED, evt -> {
Node n = evt.getPickResult().getIntersectedNode();
boolean title = false;
while (n != accordion) {
if (n.getStyleClass().contains("title")) {
// we're in the title
title = true;
break;
}
n = n.getParent();
}
rect.setFill(title ? Color.LIME : Color.RED);
});
Scene scene = new Scene(new VBox(accordion, rect), 100, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
Note that with an event filter for MouseEvent.MOUSE_CLICKED you could simply consume the event, if the pick result is not in a title...
I am trying to check collision detection on the nodes which are inside StackPane. Below is my code:
public void start(Stage primaryStage) throws Exception {
StackPane pane = new StackPane();
Scene scene = new Scene(pane,300,300,Color.GREEN);
primaryStage.setScene(scene);
primaryStage.show();
Rectangle rect1 = new Rectangle(50, 50);
rect1.setFill(Color.BLUE);
Rectangle rect2 = new Rectangle(50, 50);
pane.getChildren().add(rect1);
pane.getChildren().add(rect2);
TranslateTransition translateTransitionEnemyCar = new TranslateTransition();
translateTransitionEnemyCar.setDuration(Duration.millis(2000));
translateTransitionEnemyCar.setNode(rect2);
translateTransitionEnemyCar.setFromY(-150);
translateTransitionEnemyCar.setToY(150);
translateTransitionEnemyCar.setAutoReverse(true);
translateTransitionEnemyCar.setCycleCount(Timeline.INDEFINITE);
translateTransitionEnemyCar.play();
checkCollision(pane,rect1,rect2);
}
//Collision Detection
void checkCollision(StackPane pane, final Rectangle rect1, Rectangle rect2){
rect2.boundsInParentProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> arg0,Bounds oldValue, Bounds newValue) {
if(rect1.intersects(newValue)){
System.out.println("Collide ============= Collide");
}
}
});
}
}
Here the collision detection is working if I use AnchorPane. But with StackPane I am not able to achieve it. I guess it is because of the co-ordinate system of the stack pane (Correct me if I am wrong).
So please help me out to achieve collision detection of the above two rectangles. Also please provide some suggestion (if you know) to implement such collision detection on nodes inside StackPane if I want to change the the co-ordinate of the nodes.
In your test condition you compare the local bounds of one rectangle with the bounds in parent of the other rectangle. You want to compare the same bounds types.
So change:
rect1.intersects(newValue)
To:
rect1.getBoundsInParent().intersects(newValue)
To understand bounds types better, you might want to play with this intersection demo application.
Well in My experience, if creating an environment (collision space) a group should be used as opposed to a container..
I think in your situation the stackPane is the culprit.
plus I think your collision check should be more along the lines of this:
public void checkCollisions(){
if(rect1.getBoundsInParent().intersects(rect2.getBoundsInParent)){
Shape intersect = Shape.intersect(rect1, rect2);
if(intersect.getBoundsInLocal().getWidth() != -1){
// Collision handling here
System.out.println("Collision occured");
}
}
}
Or you could do something more along these lines: in a class that extends "Node"
public boolean collided(Node other) {
if(this.getBoundsInParent().intersects(other.getBoundsInParent())){
Shape intersect = Shape.intersect(this.getShape(), other.getShape());
if(intersect.getBoundsInLocal().getWidth() != -1){
System.out.println(this.getId() + " : " + other.getId());
return true;
}
}
return false;
}
public void checkCollisions(){
for(Node n : root.getChildren().filtered(new Predicate(){
#Override
public boolean test(Object t) {
//makes sure object does not collide with itself
return !t.equals(this);
// for shapes
// return t instanceof Shape;
}
}){
if(collided(n)){
// handle code here
}
}
}
well thats my 2 cents...
Another thing i just noticed is you are registering a new changeListener each time your check collision method is called without un-registering it.. that could lead to problems later imo...