Implement dragging multiple selected nodes - javafx

I have a class which extends javafx.scene.Node, say DraggableNode.
I have written the event-handlers for dragging for any such DraggableNode .
Class DraggableNode extends Node
{
...
onMouseDraggedProperty().set(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
offsetX = dragStartPoitionX - event.getSceneX();
offsetY = dragStartPoitionY - event.getSceneY();
setLayoutX (event.getSceneX());
setLayoutY (event.getSceneY());
...
}
}
}
This event-handler works fine for dragging this node individually.
Next, I require to select multiple such "nodes" and, dragging of one of the selected node should change the (x,y) co-ordinates of all the selected nodes by "offsetX" & "offsetY".
Selection algorithm is also implemented(in a Class extending Pane in which these nodes are added as children). But, what I need is to somehow trigger the drag event handlers of the other selected nodes so that the final output would look like a multi-drag.

First you create a selection model, say a Set<Node>. Whenever you add a Node to your selection, you add it to the selection model.
When you drag a node, you simply change the position of all of the other nodes of the selection model in your event handler as well.
As simple as that.
Here's code which also supports rubberband selection, shift & ctrl keypress during selection, etc:
NodeSelection.java
import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeLineCap;
import javafx.stage.Stage;
public class NodeSelection extends Application {
public static Image image = new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/320px-Siberischer_tiger_de_edit02.jpg");
// public Image image = new Image( getClass().getResource( "tiger.jpg").toExternalForm());
SelectionModel selectionModel = new SelectionModel();
DragMouseGestures dragMouseGestures = new DragMouseGestures();
static Random rnd = new Random();
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.setStyle("-fx-background-color:white");
new RubberBandSelection( pane);
double width = 200;
double height = 160;
double padding = 20;
for( int row=0; row < 4; row++) {
for( int col=0; col < 4; col++) {
Selectable selectable = new Selectable( width, height);
selectable.relocate( padding * (col+1) + width * col, padding * (row + 1) + height * row);
pane.getChildren().add(selectable);
dragMouseGestures.makeDraggable(selectable);
}
}
Label infoLabel = new Label( "Drag on scene for Rubberband Selection. Shift+Click to add to selection, CTRL+Click to toggle selection. Drag selected nodes for multi-dragging.");
pane.getChildren().add( infoLabel);
Scene scene = new Scene( pane, 1600, 900);
scene.getStylesheets().add( getClass().getResource("application.css").toExternalForm());
primaryStage.setScene( scene);
primaryStage.show();
}
private class Selectable extends Region {
ImageView view;
public Selectable( double width, double height) {
view = new ImageView( image);
view.setFitWidth(width);
view.setFitHeight(height);
getChildren().add( view);
this.setPrefSize(width, height);
}
}
private class SelectionModel {
Set<Node> selection = new HashSet<>();
public void add( Node node) {
if( !node.getStyleClass().contains("highlight")) {
node.getStyleClass().add( "highlight");
}
selection.add( node);
}
public void remove( Node node) {
node.getStyleClass().remove( "highlight");
selection.remove( node);
}
public void clear() {
while( !selection.isEmpty()) {
remove( selection.iterator().next());
}
}
public boolean contains( Node node) {
return selection.contains(node);
}
public int size() {
return selection.size();
}
public void log() {
System.out.println( "Items in model: " + Arrays.asList( selection.toArray()));
}
}
private class DragMouseGestures {
final DragContext dragContext = new DragContext();
private boolean enabled = false;
public void makeDraggable(final Node node) {
node.setOnMousePressed(onMousePressedEventHandler);
node.setOnMouseDragged(onMouseDraggedEventHandler);
node.setOnMouseReleased(onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// don't do anything if the user is in the process of adding to the selection model
if( event.isControlDown() || event.isShiftDown())
return;
Node node = (Node) event.getSource();
dragContext.x = node.getTranslateX() - event.getSceneX();
dragContext.y = node.getTranslateY() - event.getSceneY();
// clear the model if the current node isn't in the selection => new selection
if( !selectionModel.contains(node)) {
selectionModel.clear();
selectionModel.add( node);
}
// flag that the mouse released handler should consume the event, so it won't bubble up to the pane which has a rubberband selection mouse released handler
enabled = true;
// prevent rubberband selection handler
event.consume();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if( !enabled)
return;
// all in selection
for( Node node: selectionModel.selection) {
node.setTranslateX( dragContext.x + event.getSceneX());
node.setTranslateY( dragContext.y + event.getSceneY());
}
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// prevent rubberband selection handler
if( enabled) {
// set node's layout position to current position,remove translate coordinates
for( Node node: selectionModel.selection) {
fixPosition(node);
}
enabled = false;
event.consume();
}
}
};
/**
* Set node's layout position to current position, remove translate coordinates.
* #param node
*/
private void fixPosition( Node node) {
double x = node.getTranslateX();
double y = node.getTranslateY();
node.relocate(node.getLayoutX() + x, node.getLayoutY() + y);
node.setTranslateX(0);
node.setTranslateY(0);
}
class DragContext {
double x;
double y;
}
}
private class RubberBandSelection {
final DragContext dragContext = new DragContext();
Rectangle rect;
Pane group;
boolean enabled = false;
public RubberBandSelection( Pane group) {
this.group = group;
rect = new Rectangle( 0,0,0,0);
rect.setStroke(Color.BLUE);
rect.setStrokeWidth(1);
rect.setStrokeLineCap(StrokeLineCap.ROUND);
rect.setFill(Color.LIGHTBLUE.deriveColor(0, 1.2, 1, 0.6));
group.addEventHandler(MouseEvent.MOUSE_PRESSED, onMousePressedEventHandler);
group.addEventHandler(MouseEvent.MOUSE_DRAGGED, onMouseDraggedEventHandler);
group.addEventHandler(MouseEvent.MOUSE_RELEASED, onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// simple flag to prevent multiple handling of this event or we'd get an exception because rect is already on the scene
// eg if you drag with left mouse button and while doing that click the right mouse button
if( enabled)
return;
dragContext.mouseAnchorX = event.getSceneX();
dragContext.mouseAnchorY = event.getSceneY();
rect.setX(dragContext.mouseAnchorX);
rect.setY(dragContext.mouseAnchorY);
rect.setWidth(0);
rect.setHeight(0);
group.getChildren().add( rect);
event.consume();
enabled = true;
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if( !event.isShiftDown() && !event.isControlDown()) {
selectionModel.clear();
}
for( Node node: group.getChildren()) {
if( node instanceof Selectable) {
if( node.getBoundsInParent().intersects( rect.getBoundsInParent())) {
if( event.isShiftDown()) {
selectionModel.add( node);
} else if( event.isControlDown()) {
if( selectionModel.contains( node)) {
selectionModel.remove( node);
} else {
selectionModel.add( node);
}
} else {
selectionModel.add( node);
}
}
}
}
selectionModel.log();
rect.setX(0);
rect.setY(0);
rect.setWidth(0);
rect.setHeight(0);
group.getChildren().remove( rect);
event.consume();
enabled = false;
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
double offsetX = event.getSceneX() - dragContext.mouseAnchorX;
double offsetY = event.getSceneY() - dragContext.mouseAnchorY;
if( offsetX > 0)
rect.setWidth( offsetX);
else {
rect.setX(event.getSceneX());
rect.setWidth(dragContext.mouseAnchorX - rect.getX());
}
if( offsetY > 0) {
rect.setHeight( offsetY);
} else {
rect.setY(event.getSceneY());
rect.setHeight(dragContext.mouseAnchorY - rect.getY());
}
event.consume();
}
};
private final class DragContext {
public double mouseAnchorX;
public double mouseAnchorY;
}
}
public static void main(String[] args) {
launch(args);
}
}
application.css
.highlight {
-fx-effect: dropshadow(three-pass-box, red, 4, 4, 0, 0);
}
Screenshot:

Related

Zooming and panning image in scrollpane

I have a problem with zooming and panning image in ScrollPane. So far I have code like this:
Image image = imageView.getImage();
scrollPane.setPrefViewportWidth(0d);
scrollPane.setPrefViewportHeight(0d);
imageView.setFitWidth(0d);
imageView.setFitHeight(0d);
Bounds viewportBounds = scrollPane.getViewportBounds();
boolean vertical = image.getWidth() > image.getHeight();
if (imageView.getRotate() == 90 || imageView.getRotate() == 270d) {
vertical = !vertical;
}
imageView.setPreserveRatio(true);
double propX = viewportBounds.getWidth() / image.getWidth();
double propY = viewportBounds.getHeight() / image.getHeight();
boolean xLead = !(propX > propY);
imageView.setScaleX((xLead) ? propX : propY);
imageView.setScaleY((xLead) ? propX : propY);
scrollPane.setContent(imageView);
scrollPane.setPannable(true);
scrollPane.setHvalue(scrollPane.getHmin() + (scrollPane.getHmax() - scrollPane.getHmin()) / 2); // center the scroll contents.
scrollPane.setVvalue(scrollPane.getVmin() + (scrollPane.getVmax() - scrollPane.getVmin()) / 2);
zoom(imageView);
private void zoom(ImageView imagePannable) {
imagePannable.setOnScroll(
new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double zoomFactor = 1.20;
double deltaY = event.getDeltaY();
if (deltaY < 0) {
zoomFactor = 0.80;
}
imagePannable.setScaleX(imagePannable.getScaleX() * zoomFactor);
imagePannable.setScaleY(imagePannable.getScaleY() * zoomFactor);
event.consume();
}
});
}
What I want to do is align image relative to mouse pointer not to center of image everytime.
I also have a problem with large images (like maps which are 8*A4 size for example). When I zooming this maps pannable function stop working. What is wrong with this code? Thanks for helps!
Several people (including me) have had this same question. I got my answer here.
In the interest of clarity, here is a working example of a panning & zooming pane using a rectangle as the zoomed node. I have implemented this in a slightly more complex way with an ImageView.
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ZoomAndPanExample extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
AnchorPane.setTopAnchor(scrollPane, 10.0d);
AnchorPane.setRightAnchor(scrollPane, 10.0d);
AnchorPane.setBottomAnchor(scrollPane, 10.0d);
AnchorPane.setLeftAnchor(scrollPane, 10.0d);
AnchorPane root = new AnchorPane();
Rectangle rect = new Rectangle(80, 60);
rect.setStroke(Color.NAVY);
rect.setFill(Color.NAVY);
rect.setStrokeType(StrokeType.INSIDE);
group.getChildren().add(rect);
// create canvas
PanAndZoomPane panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
scrollPane.addEventFilter( MouseEvent.MOUSE_CLICKED, sceneGestures.getOnMouseClickedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
root.getChildren().add(scrollPane);
Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.3d;
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(60);
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale( double scale) {
myScale.set(scale);
}
public void setPivot( double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)),
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)),
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale))
);
timeline.play();
}
public void fitWidth () {
double scale = getParent().getLayoutBounds().getMaxX()/getLayoutBounds().getMaxX();
double oldScale = getScale();
double f = scale - oldScale;
double dx = getTranslateX() - getBoundsInParent().getMinX() - getBoundsInParent().getWidth()/2;
double dy = getTranslateY() - getBoundsInParent().getMinY() - getBoundsInParent().getHeight()/2;
double newX = f*dx + getBoundsInParent().getMinX();
double newY = f*dy + getBoundsInParent().getMinY();
setPivot(newX, newY, scale);
}
public void resetZoom () {
double scale = 1.0d;
double x = getTranslateX();
double y = getTranslateY();
setPivot(x, y, scale);
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY( double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures( PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMouseClickedEventHandler() {
return onMouseClickedEventHandler;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale)-1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth()/2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight()/2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f*dx, f*dy, scale);
event.consume();
}
};
/**
* Mouse click handler
*/
private EventHandler<MouseEvent> onMouseClickedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.getButton().equals(MouseButton.PRIMARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.resetZoom();
}
}
if (event.getButton().equals(MouseButton.SECONDARY)) {
if (event.getClickCount() == 2) {
panAndZoomPane.fitWidth();
}
}
}
};
}
}

JavaFX Polygons share Points

I have a class "Vertex" that contains double x, y.
Another class "Face" holds a list of "Vertex" objects. Neighboring faces share the same vertices.
At the moment I'm creating a javafx.scene.shape.Polygon for every Face and add them all to my scene, which looks like this:
Screenshot
Now I'm planning to modify the polygons, similar to this: JavaFX modify polygons
The problem is that the polygons don't save references to my Vertex objects but double values. When I change the position of one point, the same point in the neighboring polygons is still at the old position. How can I link those points to each other? And also how to save the changes back to my "Face" object?
Code example as requested: pastebin.com/C3JHb2nM
Here you go. Calculated common anchor positions then adjusted all of the common ones when one was moved.
Anchor contains addCommon function which adds an anchor that is common to it. The common variable stores all the common anchors. Then when handle is called, all of the common ones x and y positions change as well.
Also, I will suggest that you hold the common points in Faces. I've created a simple method that will calculate all the Faces that share a vertex in said class. But following a MVC guideline, you need to have a model that provides all of necessary data to create a GUI. I would suggest Mesh, Face, and Vertex should provide all necessary information to create the GUI, and not as a two way street. Basically, Anchor should not alter your models in any way.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
public class Main extends Application {
public class Vertex {
private double x, y;
public Vertex(double x, double y) {
this.x = x;
this.y = y;
}
public Double[] getPoint() {
return new Double[]{x, y};
}
}
public class Face {
private List<Vertex> verts;
public Face(Vertex... verts) {
this.verts = new ArrayList<>(Arrays.asList(verts));
}
public Polygon getPolygon() {
Polygon polygon = new Polygon();
polygon.setFill(Color.GRAY);
polygon.setStroke(Color.BLACK);
for (Vertex vertex : verts) {
polygon.getPoints().addAll(vertex.getPoint());
}
return polygon;
}
public boolean containsVertex(Vertex ver) {
for (Vertex v : this.verts) {
if (v.x == ver.x && v.y == ver.y) {
return true;
}
}
return false;
}
}
public class Mesh {
private List<Vertex> verts = new ArrayList<Vertex>();
private List<Face> faces = new ArrayList<Face>();
private Map<Vertex, List<Face>> commonVertices = new HashMap<>();
public List<Polygon> getPolygons() {
List<Polygon> polygons = new ArrayList<Polygon>();
for (Face face : faces) {
polygons.add(face.getPolygon());
}
return polygons;
}
public Mesh() {
verts.add(new Vertex(50, 50));
verts.add(new Vertex(300, 50));
verts.add(new Vertex(500, 50));
verts.add(new Vertex(50, 300));
verts.add(new Vertex(250, 300));
verts.add(new Vertex(500, 300));
verts.add(new Vertex(50, 600));
verts.add(new Vertex(300, 700));
verts.add(new Vertex(500, 700));
faces.add(new Face(verts.get(0), verts.get(1), verts.get(4), verts.get(3)));
faces.add(new Face(verts.get(4), verts.get(1), verts.get(2), verts.get(5)));
faces.add(new Face(verts.get(3), verts.get(4), verts.get(7), verts.get(6)));
faces.add(new Face(verts.get(7), verts.get(4), verts.get(5)));
faces.add(new Face(verts.get(7), verts.get(5), verts.get(8)));
findCommonVertices();
}
private void findCommonVertices() {
for (Vertex ver : this.verts) {
List<Face> share = new ArrayList<>();
for (Face face : this.faces) {
if (face.containsVertex(ver)) {
share.add(face);
}
}
commonVertices.put(ver, share);
}
}
public Map<Vertex, List<Face>> getCommonVertices() {
return this.commonVertices;
}
}
#Override
public void start(Stage stage) {
Group root = new Group();
Scene scene = new Scene(root, 1024, 768);
stage.setScene(scene);
Group g = new Group();
Mesh mesh = new Mesh();
List<Polygon> polygons = mesh.getPolygons();
g.getChildren().addAll(polygons);
List<Anchor> anchors = new ArrayList<>();
for (Polygon p : polygons) {
ObservableList<Anchor> temp = createControlAnchorsFor(p.getPoints());
g.getChildren().addAll(temp);
for (Anchor kk : temp) {
anchors.add(kk);
}
}
for (int i = 0; i < anchors.size(); i++) {
List<Anchor> common = new ArrayList<>();
for (int j = 0; j < anchors.size(); j++) {
if (i != j) {
if (anchors.get(i).x.doubleValue() == anchors.get(j).x.doubleValue() && anchors.get(i).y.doubleValue() == anchors.get(j).y.doubleValue()) {
anchors.get(i).addCommon(anchors.get(j));
System.out.println("COMMON " + i + " " + j);
}
}
}
// anchors.get(i).setCommon(common);
}
scene.setRoot(g);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
// Everything below was copied from here: https://gist.github.com/jewelsea/5375786
// #return a list of anchors which can be dragged around to modify points in the format [x1, y1, x2, y2...]
private ObservableList<Anchor> createControlAnchorsFor(final ObservableList<Double> points) {
ObservableList<Anchor> anchors = FXCollections.observableArrayList();
for (int i = 0; i < points.size(); i += 2) {
final int idx = i;
DoubleProperty xProperty = new SimpleDoubleProperty(points.get(i));
DoubleProperty yProperty = new SimpleDoubleProperty(points.get(i + 1));
xProperty.addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> ov, Number oldX, Number x) {
points.set(idx, (double) x);
}
});
yProperty.addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> ov, Number oldY, Number y) {
points.set(idx + 1, (double) y);
}
});
anchors.add(new Anchor(Color.GOLD, xProperty, yProperty));
}
return anchors;
}
// a draggable anchor displayed around a point.
class Anchor extends Circle {
private final DoubleProperty x, y;
List<Anchor> common = new ArrayList<>();
public void setCommon(List<Anchor> common) {
this.common = common;
}
public void addCommon(Anchor com) {
common.add(com);
enableDrag();
}
Anchor(Color color, DoubleProperty x, DoubleProperty y) {
super(x.get(), y.get(), 10);
setFill(color.deriveColor(1, 1, 1, 0.5));
setStroke(color);
setStrokeWidth(2);
setStrokeType(StrokeType.OUTSIDE);
this.x = x;
this.y = y;
x.bind(centerXProperty());
y.bind(centerYProperty());
enableDrag();
}
// make a node movable by dragging it around with the mouse.
private void enableDrag() {
final Delta dragDelta = new Delta();
setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
// record a delta distance for the drag and drop operation.
dragDelta.x = getCenterX() - mouseEvent.getX();
dragDelta.y = getCenterY() - mouseEvent.getY();
getScene().setCursor(Cursor.MOVE);
}
});
setOnMouseReleased(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
getScene().setCursor(Cursor.HAND);
}
});
setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
double newX = mouseEvent.getX() + dragDelta.x;
if (newX > 0 && newX < getScene().getWidth()) {
setCenterX(newX);
if (common != null) {
for (Anchor an : common) {
an.setCenterX(newX);
System.out.println("CALLED");
}
}
}
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
setCenterY(newY);
if (common != null) {
for (Anchor an : common) {
an.setCenterY(newY);
}
}
}
}
});
setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.HAND);
}
}
});
setOnMouseExited(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.DEFAULT);
}
}
});
}
// records relative x and y co-ordinates.
private class Delta {
double x, y;
}
}
}
Make the x and y coordinates of the vertices properties. You could then add listeners to those properties that modify the points of the Polygons:
public class Vertex {
public DoubleProperty xProperty() {
...
}
public DoubleProperty yProperty() {
...
}
}
public class VertexListener implements InvalidationListerner {
private final Vertex vertex;
private final int firstIndex;
private final List<Double> points;
public VertexListener(Vertex vertex, Polygon polygon, int pointIndex) {
this.firstIndex = 2 * pointIndex;
this.vertex = vertex;
this.points = polygon.getPoints();
vertex.xProperty().addListener(this);
vertex.yProperty().addListener(this);
}
public void dispose() {
vertex.xProperty().removeListener(this);
vertex.yProperty().removeListener(this);
}
#Override
public void invalidated(Observable observable) {
double x = vertex.getX();
double y = vertex.getY();
points.set(firstIndex, x);
points.set(firstIndex+1, y);
}
}
This way you only need to adjust the property values in Vertex and the listeners will add all incident faces...

How to detect double click on a tableview row in javafx while using fxml with scene builder [duplicate]

I need to detect double clicks on a row of a TableView.
How can I listen for double clicks on any part of the row and get all data of this row to print it to the console?
TableView<MyType> table = new TableView<>();
//...
table.setRowFactory( tv -> {
TableRow<MyType> row = new TableRow<>();
row.setOnMouseClicked(event -> {
if (event.getClickCount() == 2 && (! row.isEmpty()) ) {
MyType rowData = row.getItem();
System.out.println(rowData);
}
});
return row ;
});
Here is a complete working example:
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class TableViewDoubleClickOnRow extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.setRowFactory(tv -> {
TableRow<Item> row = new TableRow<>();
row.setOnMouseClicked(event -> {
if (event.getClickCount() == 2 && (! row.isEmpty()) ) {
Item rowData = row.getItem();
System.out.println("Double click on: "+rowData.getName());
}
});
return row ;
});
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
Random rng = new Random();
for (int i = 1 ; i <= 50 ; i++) {
table.getItems().add(new Item("Item "+i, rng.nextInt(1000)));
}
Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
private static <S,T> TableColumn<S,T> column(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public StringProperty nameProperty() {
return name ;
}
public final String getName() {
return nameProperty().get();
}
public final void setName(String name) {
nameProperty().set(name);
}
public IntegerProperty valueProperty() {
return value ;
}
public final int getValue() {
return valueProperty().get();
}
public final void setValue(int value) {
valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
Example:
table.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.isPrimaryButtonDown() && event.getClickCount() == 2) {
System.out.println(table.getSelectionModel().getSelectedItem());
}
}
});
If you are using custom selection model, then you can get the row from event, example:
table.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if (event.isPrimaryButtonDown() && event.getClickCount() == 2) {
Node node = ((Node) event.getTarget()).getParent();
TableRow row;
if (node instanceof TableRow) {
row = (TableRow) node;
} else {
// clicking on text part
row = (TableRow) node.getParent();
}
System.out.println(row.getItem());
}
}
});
This works for me:
table.setOnMouseClicked((MouseEvent event) -> {
if (event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() == 2){
System.out.println(table.getSelectionModel().getSelectedItem());
}
});
}
If you are using SceneBuilder you can set your table's OnMouseClicked to handleRowSelect() method as shown below:
MyType temp;
Date lastClickTime;
#FXML
private void handleRowSelect() {
MyType row = myTableView.getSelectionModel().getSelectedItem();
if (row == null) return;
if(row != temp){
temp = row;
lastClickTime = new Date();
} else if(row == temp) {
Date now = new Date();
long diff = now.getTime() - lastClickTime.getTime();
if (diff < 300){ //another click registered in 300 millis
System.out.println("Edit dialog");
} else {
lastClickTime = new Date();
}
}
}
Extending the previous answer:
The extra check ensures the selected row was double clicked - ignoring double clicks on empty rows or the column header
table.setRowFactory(param -> {
TableRow<MyType> row = new TableRow<>();
row.setOnMouseClicked(event -> Optional.ofNullable(row.getItem()).ifPresent(rowData-> {
if(event.getClickCount() == 2 && rowData.equals(table.getSelectionModel().getSelectedItem())){
System.out.println(rowData);
}
}));
return row;
});
```
This answer has been tested:
table.setOnMouseClicked( event -> {
if( event.getClickCount() == 2 ) {
System.out.println( table.getSelectionModel().getSelectedItem());
}});
table.getSelectionModel().getSelectedItem() can be use since we catch a double-click. One the first click the selection moves, on the second this handler is executed.
I had similar situation not to detect mouse double click event on TableView.
Above all samples worked perfectly. but my application did not detect double click event at all.
But I found that if TableView is on editable, mouse double click event can not be detected !!
check your application if TableView is on editable like this.
tableView.setEditable( true );
if then, double click event only raises on same row selected.

How to select multiple components of Canvas using ctrl key in javafx?

I want to select few images available on my Canvas. How to select multiple of them using control key?
You create a model, e. g.
List<Node> selectionModel = new ArrayList<Node>();
and add a listener to each node, e. g.
imageView.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if( event.isControlDown()) {
selectionModel.add( (Node) event.getSource());
// logging
System.out.println("Items in model:");
selectionModel.forEach(n -> System.out.println(n));
}
});
Of course you need to provide an option to clear the selection model, e. g. by clicking outside on the scene instead of an ImageView.
Here's a more sophisticated example. Supports rubberband selection, ctrl for selection toggle, shift for adding to selection and click on background to clear the selection.
public class ImageSelection extends Application {
Image image = new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/320px-Siberischer_tiger_de_edit02.jpg");
SelectionModel selectionModel = new SelectionModel();
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
double width = image.getWidth();
double height = image.getHeight();
double padding = 20;
for( int row=0; row < 4; row++) {
for( int col=0; col < 4; col++) {
ImageView imageView = new ImageView( image);
imageView.relocate( padding * (col+1) + width * col, padding * (row + 1) + height * row);
pane.getChildren().add(imageView);
}
}
Scene scene = new Scene( pane, 1800, 1200);
primaryStage.setScene( scene);
primaryStage.show();
new RubberBandSelection( pane);
}
public static void main(String[] args) {
launch(args);
}
private class SelectionModel {
Set<Node> selection = new HashSet<>();
public void add( Node node) {
node.setStyle("-fx-effect: dropshadow(three-pass-box, red, 10, 10, 0, 0);");
selection.add( node);
}
public void remove( Node node) {
node.setStyle("-fx-effect: null");
selection.remove( node);
}
public void clear() {
while( !selection.isEmpty()) {
remove( selection.iterator().next());
}
}
public boolean contains( Node node) {
return selection.contains(node);
}
public void log() {
System.out.println( "Items in model: " + Arrays.asList( selection.toArray()));
}
}
private class RubberBandSelection {
final DragContext dragContext = new DragContext();
Rectangle rect;
Pane group;
public RubberBandSelection( Pane group) {
this.group = group;
rect = new Rectangle( 0,0,0,0);
rect.setStroke(Color.BLUE);
rect.setStrokeWidth(1);
rect.setStrokeLineCap(StrokeLineCap.ROUND);
rect.setFill(Color.LIGHTBLUE.deriveColor(0, 1.2, 1, 0.6));
group.addEventHandler(MouseEvent.MOUSE_PRESSED, onMousePressedEventHandler);
group.addEventHandler(MouseEvent.MOUSE_DRAGGED, onMouseDraggedEventHandler);
group.addEventHandler(MouseEvent.MOUSE_RELEASED, onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
dragContext.mouseAnchorX = event.getSceneX();
dragContext.mouseAnchorY = event.getSceneY();
rect.setX(dragContext.mouseAnchorX);
rect.setY(dragContext.mouseAnchorY);
rect.setWidth(0);
rect.setHeight(0);
group.getChildren().add( rect);
event.consume();
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
if( !event.isShiftDown() && !event.isControlDown()) {
selectionModel.clear();
}
for( Node node: group.getChildren()) {
if( node instanceof ImageView) {
if( node.getBoundsInParent().intersects( rect.getBoundsInParent())) {
if( event.isShiftDown()) {
selectionModel.add( node);
} else if( event.isControlDown()) {
if( selectionModel.contains( node)) {
selectionModel.remove( node);
} else {
selectionModel.add( node);
}
} else {
selectionModel.add( node);
}
}
}
}
selectionModel.log();
rect.setX(0);
rect.setY(0);
rect.setWidth(0);
rect.setHeight(0);
group.getChildren().remove( rect);
event.consume();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
double offsetX = event.getSceneX() - dragContext.mouseAnchorX;
double offsetY = event.getSceneY() - dragContext.mouseAnchorY;
if( offsetX > 0)
rect.setWidth( offsetX);
else {
rect.setX(event.getSceneX());
rect.setWidth(dragContext.mouseAnchorX - rect.getX());
}
if( offsetY > 0) {
rect.setHeight( offsetY);
} else {
rect.setY(event.getSceneY());
rect.setHeight(dragContext.mouseAnchorY - rect.getY());
}
event.consume();
}
};
private final class DragContext {
public double mouseAnchorX;
public double mouseAnchorY;
}
}
}

JavaFX modify polygons

Is there a way to modify a polygon in JavaFX? For example if I have a triangle and I press and then drag a point from that triangle, the triangle will modify with the new coordinates of the point.
Layer some control nodes over the corners of the polygon.
Attach appropriate event handlers to the control nodes so that they can be dragged around.
Modify the polygon's points as the control node is moved (using change listeners attached to each of the control node's location properties).
Here is a sample solution:
import javafx.scene.Scene;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.collections.*;
import javafx.event.EventHandler;
import javafx.scene.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
/** Drag the anchors around to change a polygon's points. */
public class TriangleManipulator extends Application {
public static void main(String[] args) throws Exception { launch(args); }
// main application layout logic.
#Override public void start(final Stage stage) throws Exception {
Polygon triangle = createStartingTriangle();
Group root = new Group();
root.getChildren().add(triangle);
root.getChildren().addAll(createControlAnchorsFor(triangle.getPoints()));
stage.setTitle("Triangle Manipulation Sample");
stage.setScene(
new Scene(
root,
400, 400, Color.ALICEBLUE
)
);
stage.show();
}
// creates a triangle.
private Polygon createStartingTriangle() {
Polygon triangle = new Polygon();
triangle.getPoints().setAll(
100d, 100d,
150d, 50d,
250d, 150d
);
triangle.setStroke(Color.FORESTGREEN);
triangle.setStrokeWidth(4);
triangle.setStrokeLineCap(StrokeLineCap.ROUND);
triangle.setFill(Color.CORNSILK.deriveColor(0, 1.2, 1, 0.6));
return triangle;
}
// #return a list of anchors which can be dragged around to modify points in the format [x1, y1, x2, y2...]
private ObservableList<Anchor> createControlAnchorsFor(final ObservableList<Double> points) {
ObservableList<Anchor> anchors = FXCollections.observableArrayList();
for (int i = 0; i < points.size(); i+=2) {
final int idx = i;
DoubleProperty xProperty = new SimpleDoubleProperty(points.get(i));
DoubleProperty yProperty = new SimpleDoubleProperty(points.get(i + 1));
xProperty.addListener(new ChangeListener<Number>() {
#Override public void changed(ObservableValue<? extends Number> ov, Number oldX, Number x) {
points.set(idx, (double) x);
}
});
yProperty.addListener(new ChangeListener<Number>() {
#Override public void changed(ObservableValue<? extends Number> ov, Number oldY, Number y) {
points.set(idx + 1, (double) y);
}
});
anchors.add(new Anchor(Color.GOLD, xProperty, yProperty));
}
return anchors;
}
// a draggable anchor displayed around a point.
class Anchor extends Circle {
private final DoubleProperty x, y;
Anchor(Color color, DoubleProperty x, DoubleProperty y) {
super(x.get(), y.get(), 10);
setFill(color.deriveColor(1, 1, 1, 0.5));
setStroke(color);
setStrokeWidth(2);
setStrokeType(StrokeType.OUTSIDE);
this.x = x;
this.y = y;
x.bind(centerXProperty());
y.bind(centerYProperty());
enableDrag();
}
// make a node movable by dragging it around with the mouse.
private void enableDrag() {
final Delta dragDelta = new Delta();
setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
// record a delta distance for the drag and drop operation.
dragDelta.x = getCenterX() - mouseEvent.getX();
dragDelta.y = getCenterY() - mouseEvent.getY();
getScene().setCursor(Cursor.MOVE);
}
});
setOnMouseReleased(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
getScene().setCursor(Cursor.HAND);
}
});
setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
double newX = mouseEvent.getX() + dragDelta.x;
if (newX > 0 && newX < getScene().getWidth()) {
setCenterX(newX);
}
double newY = mouseEvent.getY() + dragDelta.y;
if (newY > 0 && newY < getScene().getHeight()) {
setCenterY(newY);
}
}
});
setOnMouseEntered(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.HAND);
}
}
});
setOnMouseExited(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (!mouseEvent.isPrimaryButtonDown()) {
getScene().setCursor(Cursor.DEFAULT);
}
}
});
}
// records relative x and y co-ordinates.
private class Delta { double x, y; }
}
}
I derived this solution from: CubicCurve JavaFX

Resources