I'm trying to make app that spawns new draggable nodes on pretty big pane(which is child of scrollpane), but this node should be spawned in the center of the screen.
Q is: Are there any methods to pre-set X,Y coordinates of these new imageviews?
For example:
button.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent e) {
Bounds bounds = scrollPane.getBoundsInLocal();
Bounds screenBounds = scrollPane.localToScreen(bounds);
int mX = (int) screenBounds.getMinX();
int mY = (int) screenBounds.getMinY();
Rectangle2D primScreenBounds = Screen.getPrimary().getVisualBounds();
int x = (int) ((primScreenBounds.getWidth() - mX) /4);
int y = (int) ((primScreenBounds.getHeight() - mY) /4);
/*
System.out.println("X coords:" +x);
System.out.println("Y coords:" +y);
*/
pane.getChildren().addAll(new ImageView(imgvw.getImage()));
//somehow set coordinates of new ImageView
}
});
The way you'd set the position deĀ“pends on the layout you use. However assuming you use Pane, you could use
layoutX and layoutY or
translateX and translateY
ImageView iv = new ImageView(imgvw.getImage());
iv.setLayoutX(x);
iv.setLayoutY(y);
pane.getChildren().add(iv);
Other layouts, e.g. StackPane automatically set layoutX and layoutY. In this case you could set managed to false
iv.setManaged(false);
or use the appropriate parameters for the layout type.
This might depend on the parent the ImageViews are placed in, though in general you should be able to position Nodes with Region#positionInArea() (Region is superclass of Parent). Note that this is a protected method, meaning you might want to create your own parent (e.g. by extending StackPane for example).
That being said, there are plenty of Parent Nodes, each providing their own unique behavior. Try to make sure that the desired behavior cannot be achieved using any of those before creating your own implementation. (And since you dont specify any positioning behavior, its hard to make recommendations)
Related
I am trying to set restrictions on the movement of a camera in JavaFX such that when it moves, it does not allow the user to move in such a way that only the content of the subscene is visible. Currently, my movement code looks as follows and has no checks to prevent this, I have tried limiting the movement of the camera by checking its coordinates and approximating if it will or will not show the content of the subscene, but that has problems in that it is purely an approximation and when zooming. TLDR, the problem involves 1 detecting when the camera moves away from the content of it, and 2 preventing a transformation from occurring if it will result in the camera moving away from the content.
mapView.addEventFilter(MouseEvent.MOUSE_PRESSED, e->{
startX = e.getX();
startY = e.getY();
});
mapView.addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
camera.setTranslateX(camera.getTranslateX() + (startX - e.getX()));
camera.setTranslateY(camera.getTranslateY() + (startY - e.getY()));
});
mapView is a MeshView if that is relevant.
If you would like me to clarify anything or need further information I will provide it. Thanks for the help and good day.
The camera has a viewport that you can imagine as a movable overlay above the contents (with some background being displayed in areas where no contents are placed). For the sake of simplicity, I would separate scrolling (i.e. moving the viewport) from content transformations (e.g. zooming).
Based on this mental model, you can define the scrollable bounds to be the bounds of your contents as well as a possibly empty portion of the current viewport (e.g. in case of contents smaller than viewport). The scrollable bounds needs to be recomputed after every scroll operation (increasing/reducing empty space within the current viewport) or content manipulation (transformations and bounds changes). If you restrict scrolling to the scrollable bounds, then you can ensure that empty space within the viewport is never increased by a scroll operation.
You can create an ObjectBinding scrollableBounds that is bound to the contents' bounds-in-local and local-to-parent-transform properties, as well the viewport-bounds. Then you can create a scollableBoundsProperty that is bound to the binding. That property can be accessed when scrolling to restrict the translation before applying it, thus preventing an increase of empty space within the viewport.
ObjectBinding<Bounds> scrollableBoundsBinding = new ObjectBinding<>() {
{
// TODO: bind to dependencies: viewport bounds and content bounds
// TODO: (transformed to the same coordinate system)
bind(camera.boundsInParentProperty(),
contentPane.boundsInLocalProperty(),
contentPane.localToParentTransformProperty());
}
#Override protected Bounds computeValue() {
// TODO: compute union of viewport and content bounds
return unionBounds(viewportBounds, contentBounds);
}
};
ObjectProperty<Bounds> scrollableBoundsProperty = new SimpleObjectProperty<>(
scrollableBoundsBinding);
// ...
// on mouse drag:
// dx, dy: relative mouse movement
// tx, ty: current scrolling
// mintx, maxtx, minty, maxty: translation range
// (taken from scrollable bounds and viewport size)
if (dx < 0) { tx = max(mintx, tx + dx); }
else { tx = min(maxtx, tx + dx); }
if (dy < 0) { ty = max(minty, ty + dy); }
else { ty = min(maxty, ty + dy); }
You might want to further restrict scrolling when the contents fully fit within the viewport, e.g. by placing the contents at the top left corner. You could also restrict the minimal zoom level in that case so that the contents are displayed as big as possible.
Note on usability: As already pointed out by another answer, you might want to consider allowing to drag over the contents by a bit, possibly with decreasing efficiency the further away one tries to scroll from the contents, comparable to the behavior of scrolling via touchpad in Safari. Then, when the interaction finishes, you could transition back instead of snapping in order to restrict the viewport to the contents again.
that's pretty common: just move and after you moved check if you're out of bounds... in that case go back into scene... this usually feels natural as when you try to pan an image on your phone.. it doesn't just block: it appears as it's making resistance and when you end your gesture it goes back... that's the simplest thing to do
I need to zoom in / out on a scroll pane, relative to the mouse position.
I currently achieve the zooming functionality by wrapping my content in a Group, and scaling the group itself. I create a new Scale object with a custom pivot. (Pivot is set to the mouse position)
This works perfectly for where the Group's initial scale is 1.0, however scaling afterwards does not scale in the correct direction - I believe this is because the relative mouse position changes when the Group has been scaled.
My code:
#Override
public void initialize(URL location, ResourceBundle resources) {
Delta initial_mouse_pos = new Delta();
anchorpane.setOnScrollStarted(event -> {
initial_mouse_pos.x = event.getX();
initial_mouse_pos.y = event.getY();
});
anchorpane.setOnScroll(event -> {
double zoom_fac = 1.05;
double delta_y = event.getDeltaY();
if(delta_y < 0) {
zoom_fac = 2.0 - zoom_fac;
}
Scale newScale = new Scale();
newScale.setPivotX(initial_mouse_pos.x);
newScale.setPivotY(initial_mouse_pos.y);
newScale.setX( content_group.getScaleX() * zoom_fac );
newScale.setY( content_group.getScaleY() * zoom_fac );
content_group.getTransforms().add(newScale);
event.consume();
});
}
private class Delta { double x, y; }
How do I get the correct mouse position at different levels of scaling? Is there a completely different way to zooming the ScrollPane that is easier?
This is a scalable, pannable JavaFX ScrollPane :
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
public class ZoomableScrollPane extends ScrollPane {
private double scaleValue = 0.7;
private double zoomIntensity = 0.02;
private Node target;
private Node zoomNode;
public ZoomableScrollPane(Node target) {
super();
this.target = target;
this.zoomNode = new Group(target);
setContent(outerNode(zoomNode));
setPannable(true);
setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
setFitToHeight(true); //center
setFitToWidth(true); //center
updateScale();
}
private Node outerNode(Node node) {
Node outerNode = centeredNode(node);
outerNode.setOnScroll(e -> {
e.consume();
onScroll(e.getTextDeltaY(), new Point2D(e.getX(), e.getY()));
});
return outerNode;
}
private Node centeredNode(Node node) {
VBox vBox = new VBox(node);
vBox.setAlignment(Pos.CENTER);
return vBox;
}
private void updateScale() {
target.setScaleX(scaleValue);
target.setScaleY(scaleValue);
}
private void onScroll(double wheelDelta, Point2D mousePoint) {
double zoomFactor = Math.exp(wheelDelta * zoomIntensity);
Bounds innerBounds = zoomNode.getLayoutBounds();
Bounds viewportBounds = getViewportBounds();
// calculate pixel offsets from [0, 1] range
double valX = this.getHvalue() * (innerBounds.getWidth() - viewportBounds.getWidth());
double valY = this.getVvalue() * (innerBounds.getHeight() - viewportBounds.getHeight());
scaleValue = scaleValue * zoomFactor;
updateScale();
this.layout(); // refresh ScrollPane scroll positions & target bounds
// convert target coordinates to zoomTarget coordinates
Point2D posInZoomTarget = target.parentToLocal(zoomNode.parentToLocal(mousePoint));
// calculate adjustment of scroll position (pixels)
Point2D adjustment = target.getLocalToParentTransform().deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
// convert back to [0, 1] range
// (too large/small values are automatically corrected by ScrollPane)
Bounds updatedInnerBounds = zoomNode.getBoundsInLocal();
this.setHvalue((valX + adjustment.getX()) / (updatedInnerBounds.getWidth() - viewportBounds.getWidth()));
this.setVvalue((valY + adjustment.getY()) / (updatedInnerBounds.getHeight() - viewportBounds.getHeight()));
}
}
Did you try to remove the setOnScrollStarted-event and move its content to the setOnScroll-event?
Doing so reduces the need of your extra Delta-class and the computations of your mouse-positions are always on par with the current zoom factor.
I implemented the same thing and it works the way you are describing it.
Somehow like this:
#Override
public void initialize(URL location, ResourceBundle resources) {
anchorpane.setOnScroll(event -> {
double zoom_fac = 1.05;
if(delta_y < 0) {
zoom_fac = 2.0 - zoom_fac;
}
Scale newScale = new Scale();
newScale.setPivotX(event.getX);
newScale.setPivotY(event.getY);
newScale.setX( content_group.getScaleX() * zoom_fac );
newScale.setY( content_group.getScaleY() * zoom_fac );
content_group.getTransforms().add(newScale);
event.consume();
});
}
I believe this is a duplicate of this question which involves the same concepts at work. If you don't really care if it zooms relative to your mouse and just prefer it zoom in the center look at this question. If you need any more help comment below.
Assuming you want to have the following zoom behavior:
When the mouse wheel is pushed forward/backward the object under the cursor will be scaled up/down and the area under the cursor is now centered within the zooming area.
Eg. pushing the wheel forward while pointing at a place left from the center of the zooming area results in a 'up-scale and move right' action.
The scaling thing is as simple as you have already done so far.
The tricky part is the move action. There are some problem you have to consider within your calculations:
You have to calculate the difference from the center and the
position of the cursor. You can calculate this value by subtracting
the center point (cp) from the mouse position (mp).
If your zoom level is 1 and you point 50px left from the center you want to move your object 50px to the right, because 1px of your screen corresponds to one 1px of your object (picture). But if you doubled the size of your object, than 2 screen pixel are equal to on object pixel. You have to consider this when moving the object, because the translating part is always done before the scaling part. In other words you are moving your object in original size and the scaling is only the second independent step.
How is the scaling done? In JavaFX you simply set some scale-properties and JavaFX does the rest for you. But what does it exactly? The important thing to know is, where the fixed point is while zooming the object. If you scale an object there will be one point which stays fixed at its position while all other points are moving towards this point or moving way from it.
As the documentation of JavaFX says the center of the zoomed object will be the fixed point.
Defines the factor by which coordinates are scaled about the center of
the object along the X axis of this Node.
That means you have to ensure that your visual center point is equal to the one JavaFX uses while scaling you object. You can achieve this if you wrap your object within a container. Now zoom the container instead of the object and position the object within the container to fit your needs.
I hope this helps. If you need more help please offer a short working example project.
Am I correct in thinking it is not possible to drag Shapes within a GridPane? Here's a link to some code that allows the user to drag shapes around the screen: Drag and Drop Shapes
I want my shapes to have the same behavior as above, but I want them in a GridPane (eventually I'd like their movement to be locked to the X or Y axis to be moved on to adjacent shapes).
I added the below code to the "start" method. It creates circles using the same method as the example code but instead adds them to a grid. Surprisingly, this removed the ability for them to be dragged around.
GridPane grid = new GridPane();
grid.setLayoutX(300);
grid.setLayoutY(100);
int n = 3;
int m = 3;
for (int r = 0; r < n; r++) {
for (int c = 0; c < m; c++) {
Circle circle = createCircle(100, 50, 30, Color.BLACK);
grid.add(circle, c, r);
}
}
root.getChildren().add(grid);
If you want to test this just add the above code to the "start" method of the example code, just above these lines:
primaryStage.setScene(scene);
primaryStage.show();
My theory is the GridPane, because it locks the circles to certain positions, doesn't allow this dragging behavior.
Any input on how I can achieve movement of the circles along the X and Y axis when dragged?
In general, layout panes such as GridPane manage the placement of their content. Changing the layout coordinates will not affect nodes that are placed in these panes. You may find it better to use a plain Pane and manage the layout yourself for functionality such as this.
If you do want to use a GridPane, transformations (such as translations, etc) are applied after layout coordinates are computed, so you can use a translation (e.g. the one built-in with the translateX and translateY properties) to manage dragging in a layout pane.
So you can do:
circle.setOnMouseDragged((t) -> {
double offsetX = t.getSceneX() - orgSceneX;
double offsetY = t.getSceneY() - orgSceneY;
// No idea why they are doing this. c is just circle
Circle c = (Circle) (t.getSource());
c.setTranslateX(c.getTranslateX() + offsetX);
c.setTranslateY(c.getTranslateY() + offsetY);
orgSceneX = t.getSceneX();
orgSceneY = t.getSceneY();
});
I am trying to achieve effect similar to marquee - line of long (in my case) text which is moved in horizontal axis. I managed to get it work, but I can't call it satisfactory.
My Controller class looks as below:
#FXML
private Text newsFeedText;
(...)
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
TranslateTransition transition = TranslateTransitionBuilder.create()
.duration(new Duration(7500))
.node(newsFeedText)
.interpolator(Interpolator.LINEAR)
.cycleCount(Timeline.INDEFINITE)
.build();
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
int width = gd.getDisplayMode().getWidth();
transition.setFromX(width);
transition.setToX(-width);
transition.play();
}
newsFeedText is binded to some text source which is dynamically updated, so it contains various amount of text.
My code has at least two drawbacks:
Transition goes from -width to +width; width is monitor's resolution width
There will be moments when text will not be visible at all if window is not full-screened.
If text will be longer and newsFeedText width will be greater than monitor's resolution width then transition will disappear "in half" (still being on a screen).
Currently Duration is not dependent on a width of newsFeedText.
Now, it's nothing worng, but if transition's fromX and toX were be dynamically calculated then it will result in various speeds of marquee.
How to get rid of these drawbacks?
I have managed it to work, any recalculations can happen only after transition is stopped so we cannot set its cycleCount to Timeline.INDEFINITE. My requirement was that I could change text inside component so there are fxml wirings:
#FXML
private Text node; // text to marquee
#FXML
private Pane parentPane; // pane on which text is placed
The code which works is:
transition = TranslateTransitionBuilder.create()
.duration(new Duration(10))
.node(node)
.interpolator(Interpolator.LINEAR)
.cycleCount(1)
.build();
transition.setOnFinished(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
rerunAnimation();
}
});
rerunAnimation();
where rerunAnimation() is:
private void rerunAnimation() {
transition.stop();
// if needed set different text on "node"
recalculateTransition();
transition.playFromStart();
}
and recalculateTransition() is:
private void recalculateTransition() {
transition.setToX(node.getBoundsInLocal().getMaxX() * -1 - 100);
transition.setFromX(parentPane.widthProperty().get() + 100);
double distance = parentPane.widthProperty().get() + 2 * node.getBoundsInLocal().getMaxX();
transition.setDuration(new Duration(distance / SPEED_FACTOR));
}
You should be able to do this by listening to your scene's widthProperty. You can either access this via newsFeedText.getScene().widthProperty() or get a reference from your main class and expose it from there or pass it to a method or constructor to access within your class that declares newsFeedText.
The benefit of this approach is that now your logic is dependent upon the width of your scene (a dynamic dependency) rather than the width of your monitor (a static dependency). Note that I have not tested this approach but at the moment see no reason (perhaps naively) it shouldn't work.
As for your duration dependency, you can solve that by performing some sort of calculation based on the length of the text in newsFeedText. Something like Duration.seconds(newsFeedText.get Text().length()/denominator) where denominator is some value you specify (such as 7500, as in your code). This will make your duration dynamically computed based on the length of your text.
If you want to operate with the width of newsFeedText itself, rather than the length of its text, then simply replace newsFeedText.getText().length() with newsFeedText.getWidth(). Ensure you perform this computation after newsFeedText has been laid out so a call to get its width returns the actual width. You can also replace the call with any of getPrefWidth(), getMinWidth(), or getMaxWidth().
I am trying to achieve effect similar to marquee - line of long (in my case) text which is moved in horizontal axis. I managed to get it work, but I can't call it satisfactory.
My Controller class looks as below:
#FXML
private Text newsFeedText;
(...)
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
TranslateTransition transition = TranslateTransitionBuilder.create()
.duration(new Duration(7500))
.node(newsFeedText)
.interpolator(Interpolator.LINEAR)
.cycleCount(Timeline.INDEFINITE)
.build();
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
int width = gd.getDisplayMode().getWidth();
transition.setFromX(width);
transition.setToX(-width);
transition.play();
}
newsFeedText is binded to some text source which is dynamically updated, so it contains various amount of text.
My code has at least two drawbacks:
Transition goes from -width to +width; width is monitor's resolution width
There will be moments when text will not be visible at all if window is not full-screened.
If text will be longer and newsFeedText width will be greater than monitor's resolution width then transition will disappear "in half" (still being on a screen).
Currently Duration is not dependent on a width of newsFeedText.
Now, it's nothing worng, but if transition's fromX and toX were be dynamically calculated then it will result in various speeds of marquee.
How to get rid of these drawbacks?
I have managed it to work, any recalculations can happen only after transition is stopped so we cannot set its cycleCount to Timeline.INDEFINITE. My requirement was that I could change text inside component so there are fxml wirings:
#FXML
private Text node; // text to marquee
#FXML
private Pane parentPane; // pane on which text is placed
The code which works is:
transition = TranslateTransitionBuilder.create()
.duration(new Duration(10))
.node(node)
.interpolator(Interpolator.LINEAR)
.cycleCount(1)
.build();
transition.setOnFinished(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
rerunAnimation();
}
});
rerunAnimation();
where rerunAnimation() is:
private void rerunAnimation() {
transition.stop();
// if needed set different text on "node"
recalculateTransition();
transition.playFromStart();
}
and recalculateTransition() is:
private void recalculateTransition() {
transition.setToX(node.getBoundsInLocal().getMaxX() * -1 - 100);
transition.setFromX(parentPane.widthProperty().get() + 100);
double distance = parentPane.widthProperty().get() + 2 * node.getBoundsInLocal().getMaxX();
transition.setDuration(new Duration(distance / SPEED_FACTOR));
}
You should be able to do this by listening to your scene's widthProperty. You can either access this via newsFeedText.getScene().widthProperty() or get a reference from your main class and expose it from there or pass it to a method or constructor to access within your class that declares newsFeedText.
The benefit of this approach is that now your logic is dependent upon the width of your scene (a dynamic dependency) rather than the width of your monitor (a static dependency). Note that I have not tested this approach but at the moment see no reason (perhaps naively) it shouldn't work.
As for your duration dependency, you can solve that by performing some sort of calculation based on the length of the text in newsFeedText. Something like Duration.seconds(newsFeedText.get Text().length()/denominator) where denominator is some value you specify (such as 7500, as in your code). This will make your duration dynamically computed based on the length of your text.
If you want to operate with the width of newsFeedText itself, rather than the length of its text, then simply replace newsFeedText.getText().length() with newsFeedText.getWidth(). Ensure you perform this computation after newsFeedText has been laid out so a call to get its width returns the actual width. You can also replace the call with any of getPrefWidth(), getMinWidth(), or getMaxWidth().