I am trying to do a simple Demo about using JavaFX on Mobile using GluonFX Pluging & Attche.
The App, Will be Simply WebApp. The Main problem now is When the user click on any text input field, the Mobile keyboard is shown but most of times the View is not moved to keep the edited text field still shown.
I checked the Attach Service (Keyboard), The code by "José_Pereda" is great. but is not suitable for the full screen webview. because it shift the all view to be visible when the keyboard is up. but this mean that the input field may be out of the screen!
How can i solve this problem
I tried to make touch/click event to check the last X,Y from the user and use this value in the keyboard service handler and this works great on Desktop (linux) but on Android phone, no Touchevent on the webview are fired !!
Any help
This is the Main Controller which have all the magic. but the Webview Object is shared in the App
package com.sam.stv.parentsapp;
import com.gluonhq.attach.keyboard.KeyboardService;
import com.gluonhq.attach.util.Platform;
import com.gluonhq.attach.util.Services;
import javafx.animation.Interpolator;
import javafx.animation.TranslateTransition;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TouchEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebView;
import javafx.util.Duration;
public class PrimaryController {
WebView webview;
private EventHandler<MouseEvent> handlerForMouse;
private EventHandler<TouchEvent> handlerForTouch;
double lastX;
double lastY;
SimpleStringProperty msg = new SimpleStringProperty("Good Luck!");
ProgressIndicator progressIndicator;// = new ProgressIndicator(); // or you can use ImageView with animated gif
#FXML
StackPane webcontainer;
#FXML
StackPane paneBottom;
#FXML
StackPane paneTop;
#FXML
private void initialize() {
// ========================================= Logging lbl in Mobile
Label lbl = new Label();
lbl.textProperty().bind(msg);
lbl.prefHeight(50);
lbl.prefWidth(200);
paneTop.getChildren().addAll(lbl);
// ========================================= WebView Setup
this.webview = App.getWebview();
webcontainer.getChildren().add(webview);
// ========================================= handling Touching and clicking by
// adding listeners
initHandlers();
handlingTouching();
// ========================================= Attache-Service:=> Virtual Keyboard
// Handling
Services.get(KeyboardService.class).ifPresent(service -> {
service.visibleHeightProperty().addListener((obs, ov, nv) -> doSomethingForKeyboardBasedOnTouch(nv));
});
}
/**
* This method shit the View (webview) upward Based on the last user touch/click
* point position.
*
* #param nv
*/
void doSomethingForKeyboardBasedOnTouch(Number nv) {
Node node = this.webview;
double kh = nv.doubleValue();
double padding = 20;
if (node == null || node.getScene() == null || node.getScene().getWindow() == null) {
return;
}
double tTot = node.getScene().getHeight();
// Original Code :-> node.getLocalToSceneTransform().getTy() + node.getBoundsInParent().getHeight() + 2;
double ty = lastY + padding; // Can not update this value using the last touch event
double y = 1;
Parent root = node.getScene().getRoot();
if (ty > tTot - kh) {
y = tTot - ty - kh;
} else if (kh == 0 && root.getTranslateY() != 0) {
y = 0;
}
if (y <= 0) {
System.out.println(String.format("Moving %s %.2f pixels", root, y));
final TranslateTransition transition = new TranslateTransition(Duration.millis(50), root);
transition.setFromY(root.getTranslateY());
transition.setToY(y);
transition.setInterpolator(Interpolator.EASE_OUT);
transition.playFromStart();
}
}
private void handlingTouching() {
applyTouchClickInput(webcontainer);
applyTouchClickInput(paneBottom);
applyTouchClickInput(paneTop);
applyTouchClickInput(webview);
}
private void applyTouchClickInput(Node drawSurface) {
if (Platform.isAndroid() || Platform.isIOS()) {
// end the path when mouse released event
// drawSurface.setOnTouchReleased(handlerForTouch);
// drawSurface.setOnTouchPressed(handlerForTouch);
// drawSurface.setOnTouchMoved(handlerForTouch);
// drawSurface.addEventFilter(TouchEvent.ANY, handlerForTouch);
// drawSurface.addEventHandler(TouchEvent.ANY, handlerForTouch);
drawSurface.addEventFilter(TouchEvent.TOUCH_MOVED, handlerForTouch);
drawSurface.addEventFilter(TouchEvent.TOUCH_PRESSED, handlerForTouch);
drawSurface.addEventFilter(TouchEvent.TOUCH_RELEASED, handlerForTouch);
// drawSurface.addEventHandler(TouchEvent.TOUCH_MOVED, handlerForTouch);
// drawSurface.addEventHandler(TouchEvent.TOUCH_PRESSED, handlerForTouch);
// drawSurface.addEventHandler(TouchEvent.TOUCH_RELEASED, handlerForTouch);
} else if (Platform.isDesktop()) {
drawSurface.addEventFilter(MouseEvent.MOUSE_CLICKED, handlerForMouse);
}
}
void initHandlers() {
this.handlerForMouse = new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
String log = "Clicking: " + event.getEventType().getName() + " on "
+ event.getTarget().getClass().getSimpleName() + ")--->(" + event.getX() + "," + event.getY()
+ ")";
logging(log);
logTouch(event.getX(), event.getY());
}
};
this.handlerForTouch = new EventHandler<TouchEvent>() {
#Override
public void handle(TouchEvent event) {
String log = "Touching: " + event.getEventType().getName() + " on "
+ event.getTarget().getClass().getSimpleName() + ")--->(" + event.getTouchPoint().getX() + ","
+ event.getTouchPoint().getY() + ")";
logging(log);
logTouch(event.getTouchPoint().getX(), event.getTouchPoint().getY());
}
};
}
void logTouch(double x, double y) {
System.out.println("Set New X & Y (" + lastX + "," + lastY + ")--->(" + x + "," + y + ")");
lastX = x;
lastY = y;
}
public void logging(String log) {
System.out.println("===============================================> General Logging Handler.");
System.out.println(log);
msg.set(log);
}
}
Based on #JosePereda Confirmation that we can not receive the touch event on the Mobile devices.
Then, my work around to bypass that is to inject a Javascript in the webview loading time. this script is recording the touch event and then call a Java method whenever this touch event is done.
The Javascript-Java bridging is not work flexibly as expected, but works
Are you testing on a Samsung device?
Try applying this setting in your POM.
<plugin>
<groupId>com.gluonhq</groupId>
<artifactId>gluonfx-maven-plugin</artifactId>
<version>${gluonfx.plugin.version}</version>
<configuration>
<runtimeArgs>
<arg>-Dmonocle.input.touchRadius=4</arg>
</runtimeArgs>
Related
I'm trying to come up with a solution to allow multiple Pane nodes handle mouse events independently when assembled into a StackPane
StackPane
Pane 1
Pane 2
Pane 3
I'd like to be able to handle mouse events in each child, and the first child calling consume() stops the event going to the next child.
I'm also aware of setPickOnBounds(false), but this does not solve all cases as some of the overlays will be pixel based with Canvas, i.e. not involving the scene graph.
I've tried various experiments with Node.fireEvent(). However these always lead to recursion ending in stack overflow. This is because the event is propagated from the root scene and triggers the same handler again.
What I'm looking for is some method to trigger the event handlers on the child panes individually without the event travelling through its normal path.
My best workaround so far is to capture the event with a filter and manually invoke the handler. I'd need to repeat this for MouseMoved etc
parent.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
for (Node each : parent.getChildren()) {
if (!event.isConsumed()) {
each.getOnMouseClicked().handle(event);
}
}
event.consume();
});
However this only triggers listeners added with setOnMouseClicked, not addEventHandler, and only on that node, not child nodes.
Another sort of solution is just to accept JavaFX doesn't work like this, and restructure the panes like this, this will allow normal event propagation to take place.
Pane 1
Pane 2
Pane 3
Example
import javafx.application.Application;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
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.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class EventsInStackPane extends Application {
public static void main(String[] args) {
launch(args);
}
private static class DebugPane extends Pane {
public DebugPane(Color color, String name) {
setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
setOnMouseClicked(event -> {
System.out.println("setOnMouseClicked " + name + " " + event);
});
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventHandler " + name + " " + event);
});
addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
System.out.println("addEventFilter " + name + " " + event);
});
}
}
#Override
public void start(Stage primaryStage) throws Exception {
DebugPane red = new DebugPane(Color.RED, "red");
DebugPane green = new DebugPane(Color.GREEN, "green");
DebugPane blue = new DebugPane(Color.BLUE, "blue");
setBounds(red, 0, 0, 400, 400);
setBounds(green, 25, 25, 350, 350);
setBounds(blue, 50, 50, 300, 300);
StackPane parent = new StackPane(red, green, blue);
eventHandling(parent);
primaryStage.setScene(new Scene(parent));
primaryStage.show();
}
private void eventHandling(StackPane parent) {
parent.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (!event.isConsumed()) {
for (Node each : parent.getChildren()) {
Event copy = event.copyFor(event.getSource(), each);
parent.fireEvent(copy);
if (copy.isConsumed()) {
break;
}
}
}
event.consume();
});
}
private void setBounds(DebugPane panel, int x, int y, int width, int height) {
panel.setLayoutX(x);
panel.setLayoutY(y);
panel.setPrefWidth(width);
panel.setPrefHeight(height);
}
}
Using the hint from #jewelsea I was able to use a custom chain. I've done this from a "catcher" Pane which is added to the front of the StackPane. This then builds a chain using all the children, in reverse order, excluding itself.
private void eventHandling(StackPane parent) {
Pane catcher = new Pane() {
#Override
public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) {
EventDispatchChain chain = super.buildEventDispatchChain(tail);
for (int i = parent.getChildren().size() - 1; i >= 0; i--) {
Node child = parent.getChildren().get(i);
if (child != this) {
chain = chain.prepend(child.getEventDispatcher());
}
}
return chain;
}
};
parent.getChildren().add(catcher);
}
The ask: How do I get the viewing rectangle in the coordinates of a transformed and scaled node?
The code is attached below, it is based upon the code from this answer: JavaFX 8 Dynamic Node scaling
The details:
I have a simple pane, BigGridPane that contains a collection of squares, all 50x50.
I have it within this PanAndZoomPane construct that was lifted from the answer referenced above. I can not honestly say I fully understand the PanAndZoomPane implementation. For example, it's not clear to me why it needs a ScrollPane at all, but I have not delved in to trying without it.
The PanAndZoomPane lets me pan and zoom my BigGridPane. This works just dandy.
There are 4 Panes involved in this total construct, in this heirarchy: ScrollPane contains PanAndZoomPane which contains Group which contains BigGridPane.
ScrollPane
PanAndZoomPane
Group
BigGridPane
I have put listeners on the boundsInLocalProperty and boundsInParentProperty of all of these, and the only one of these that changes while panning and zooming, is the boundsInParentProperty of the PanAndZoomPane. (For some reason I've seen it trigger on the scroll pane, but all of the values are the same, so I don't include that here).
Along with the boundsInParentProperty changes, the translateX, translateY, and myScale properties of the PanAndZoomPane change as things move around. This is expected, of course. myScale is bound to the scaleX and scaleY properties of the PanAndZoomPane.
This is what it looks like at startup.
If I pan the grid as shown, putting 2-2 in the upper left:
We can see the properties of the PanAndZoomPane.
panAndZoom in parent: BoundingBox [minX:-99.5, minY:-99.5, minZ:0.0,
width:501.5, height:501.5, depth:0.0,
maxX:402.0, maxY:402.0, maxZ:0.0]
paz scale = 1.0 - tx: -99.0 - ty: -99.0
Scale is 1 (no zoom), and we've translated ~100x100. That is, the origin of the BigGridPane is at -100,-100. This all makes complete sense. Similarly, the bounding box shows the same thing. The origin is at -100,-100.
In this scenario, I would like to derive a rectangle that shows me what I'm seeing in the window, in the coordinates of the BigGridPane. That would mean a rectangle of
x:100 y:100 width:250 height:250
Normally, I think, this would be the viewport of the ScrollPane, but since this code isn't actually using the ScrollPane for scrolling (again, I'm not quite exactly what it's role is here), the ScrollPane viewport never changes.
I should note that there are shenanigans happening right now because of the retina display on my mac. If you look at the rectangles, showing 5x5, they're 50x50 rectangles, so we should be seeing 10x10, but because of the retina display on my iMac, everything is doubled. What we're seeing in BigGridPane coordinates is a 250x250 block of 5 squares, offset by 100x100. The fact that this is being showing in a window of 500x500 is a detail (but unlikely one we can ignore).
But to reiterate what my question is, that's what I'm trying to get: that 250x250 square at 100x100.
It's odd that it's offset by 100x100 even though the frame is twice as big (500 vs 250). If I pan to where 1-1 is the upper left, the offset is -50,-50, like it should be.
Now, let's add zooming, and pan again to 2-2.
1 click of the scroll wheel and the scale jumps to 1.5.
panAndZoom in parent: BoundingBox [minX:-149.375, minY:-150.375, minZ:0.0,
width:752.25, height:752.25, depth:0.0,
maxX:602.875, maxY:601.875, maxZ:0.0]
paz scale = 1.5 - tx: -23.375 - ty: -24.375
What I want, again, in this case, is a rectangle in BigGridPane coordinates. Roughly:
x:100 y:100 w:150 h:150
We see we're offset by 2x2 boxes (100x100) and we see 3+ boxes (150x150).
So. Back to the bounding box. MinX and minY = -150,-150. This is good. 100 x 1.5 = 150. Similarly the width and height are 750. 500 x 1.5 = 750. So, that is good.
The translates are where we go off the rails. -23.375, -24.375. I have no idea where these numbers come from. I can't seem to correlate them to anything in regards to 100, 150, 1.5 zoom, etc.
Worse, if we pan (while still at 1.5 scale) to "0,0", before, at scale=1, tx and ty were both 0. That's good.
panAndZoom in parent: BoundingBox [minX:0.625, minY:0.625, minZ:0.0,
width:752.25, height:752.25, depth:0.0,
maxX:752.875, maxY:752.875, maxZ:0.0]
paz scale = 1.5 - tx: 126.625 - ty: 126.625
Now, they're 126.625 (probably should be rounded to 125). I have no idea where those numbers come from.
I've tried all sorts of runs on the numbers to see where these numbers come from.
JavaFX knows what the numbers are! (even if the whole retina thing is kind of messing with my head, I'm going to ignore it for the moment).
And I don't see anything in the transforms of any of the panes.
So, my coordinate systems are all over the map, and I'd like to know what part of my BigGridPane is being shown in my panned and scaled view.
Code:
package pkg;
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.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class PanZoomTest 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();
PanAndZoomPane panAndZoomPane = null;
BigGridPane1 bigGridPane = new BigGridPane1(10, 10, 50);
#Override
public void start(Stage primaryStage) throws Exception {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
group.getChildren().add(bigGridPane);
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();
addListeners("panAndZoom", panAndZoomPane);
scrollPane.addEventFilter(MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter(MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter(ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
AnchorPane anchorPane = new AnchorPane();
anchorPane.getChildren().add(scrollPane);
anchorPane.setTopAnchor(scrollPane, 1.0d);
anchorPane.setRightAnchor(scrollPane, 1.0d);
anchorPane.setBottomAnchor(scrollPane, 1.0d);
anchorPane.setLeftAnchor(scrollPane, 1.0d);
BorderPane root = new BorderPane(anchorPane);
Label label = new Label("Pan and Zoom Test");
root.setTop(label);
Scene scene = new Scene(root, 250, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private void addListeners(String label, Node node) {
node.boundsInLocalProperty().addListener((o) -> {
System.out.println(label + " in local: " + node.getBoundsInLocal());
});
node.boundsInParentProperty().addListener((o) -> {
System.out.println(label + " in parent: " + node.getBoundsInParent());
System.out.println("paz scale = " + panAndZoomPane.getScale() + " - "
+ panAndZoomPane.getTranslateX() + " - "
+ panAndZoomPane.getTranslateY());
System.out.println(group.getTransforms());
});
}
class BigGridPane extends Region {
int rows;
int cols;
int size;
Font numFont = Font.font("sans-serif", 8);
FontMetrics numMetrics = new FontMetrics(numFont);
public BigGridPane(int cols, int rows, int size) {
this.rows = rows;
this.cols = cols;
this.size = size;
int sizeX = cols * size;
int sizeY = rows * size;
setMinSize(sizeX, sizeY);
setMaxSize(sizeX, sizeY);
setPrefSize(sizeX, sizeY);
populate();
}
#Override
protected void layoutChildren() {
System.out.println("grid layout");
super.layoutChildren();
}
private void populate() {
ObservableList<Node> children = getChildren();
children.clear();
for (int i = 0; i < cols; i++) {
for (int j = 0; j < rows; j++) {
Rectangle r = new Rectangle(i * size, j * size, size, size);
r.setFill(null);
r.setStroke(Color.BLACK);
String label = i + "-" + j;
Point2D p = new Point2D(r.getBoundsInLocal().getCenterX(), r.getBoundsInLocal().getCenterY());
Text t = new Text(label);
t.setX(p.getX() - numMetrics.computeStringWidth(label) / 2);
t.setY(p.getY() + numMetrics.getLineHeight() / 2);
t.setFont(numFont);
children.add(r);
children.add(t);
}
}
}
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.5d; //1.3d
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(30);//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)), //200
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)), //200
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale)) //200
);
timeline.play();
}
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> 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();
}
};
}
class FontMetrics {
final private Text internal;
public float lineHeight;
public FontMetrics(Font fnt) {
internal = new Text();
internal.setFont(fnt);
Bounds b = internal.getLayoutBounds();
lineHeight = (float) b.getHeight();
}
public float computeStringWidth(String txt) {
internal.setText(txt);
return (float) internal.getLayoutBounds().getWidth();
}
public float getLineHeight() {
return lineHeight;
}
}
}
Generally, you can get the bounds of node1 in the coordinate system of node2 if both are in the same scene using
node2.sceneToLocal(node1.localToScene(node1.getBoundsInLocal()));
I don't understand all the code you posted; I don't really know why you are using a scroll pane when you seem to be implementing all the panning and zooming yourself. Here is a simpler version of a PanZoomPane and then a test which shows how to use the idea above to get the bounds of the viewport in the coordinate system of the panning/zooming content. The "viewport" is just the bounds of the panning/zooming pane in the coordinate system of the content.
If you need the additional functionality in your version of panning and zooming, you should be able to adapt this idea to that; but it would take me too long to understand everything you are doing there.
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
public class PanZoomPane extends Region {
private final Node content ;
private final Rectangle clip ;
private Affine transform ;
private Point2D mouseDown ;
private static final double SCALE = 1.01 ; // zoom factor per pixel scrolled
public PanZoomPane(Node content) {
this.content = content ;
getChildren().add(content);
clip = new Rectangle();
setClip(clip);
transform = Affine.affine(1, 0, 0, 1, 0, 0);
content.getTransforms().setAll(transform);
content.setOnMousePressed(event -> mouseDown = new Point2D(event.getX(), event.getY()));
content.setOnMouseDragged(event -> {
double deltaX = event.getX() - mouseDown.getX();
double deltaY = event.getY() - mouseDown.getY();
translate(deltaX, deltaY);
});
content.setOnScroll(event -> {
double pivotX = event.getX();
double pivotY = event.getY();
double scale = Math.pow(SCALE, event.getDeltaY());
scale(pivotX, pivotY, scale);
});
}
public Node getContent() {
return content ;
}
#Override
protected void layoutChildren() {
clip.setWidth(getWidth());
clip.setHeight(getHeight());
}
public void scale(double pivotX, double pivotY, double scale) {
transform.append(Transform.scale(scale, scale, pivotX, pivotY));
}
public void translate(double x, double y) {
transform.append(Transform.translate(x, y));
}
public void reset() {
transform.setToIdentity();
}
}
import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.beans.binding.ObjectBinding;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.RowConstraints;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class PanZoomTest extends Application {
private Binding<Bounds> viewport ;
#Override
public void start(Stage stage) {
Node content = createContent(50, 50, 50) ;
PanZoomPane pane = new PanZoomPane(content);
viewport = new ObjectBinding<>() {
{
bind(
pane.localToSceneTransformProperty(),
pane.boundsInLocalProperty(),
content.localToSceneTransformProperty()
);
}
#Override
protected Bounds computeValue() {
return content.sceneToLocal(pane.localToScene(pane.getBoundsInLocal()));
}
};
viewport.addListener((obs, oldViewport, newViewport) -> System.out.println(newViewport));
BorderPane root = new BorderPane(pane);
Button reset = new Button("Reset");
reset.setOnAction(event -> pane.reset());
HBox buttons = new HBox(reset);
buttons.setAlignment(Pos.CENTER);
buttons.setPadding(new Insets(10));
root.setTop(buttons);
Scene scene = new Scene(root, 800, 800);
stage.setScene(scene);
stage.show();
}
private Node createContent(int columns, int rows, double cellSize) {
GridPane grid = new GridPane() ;
ColumnConstraints cc = new ColumnConstraints();
cc.setMinWidth(cellSize);
cc.setPrefWidth(cellSize);
cc.setMaxWidth(cellSize);
cc.setFillWidth(true);
cc.setHalignment(HPos.CENTER);
for (int column = 0 ; column < columns ; column++) {
grid.getColumnConstraints().add(cc);
}
RowConstraints rc = new RowConstraints();
rc.setMinHeight(cellSize);
rc.setPrefHeight(cellSize);
rc.setMaxHeight(cellSize);
rc.setFillHeight(true);
rc.setValignment(VPos.CENTER);
for (int row = 0 ; row < rows ; row++) {
grid.getRowConstraints().add(rc);
}
for (int x = 0 ; x < columns ; x++) {
for (int y = 0 ; y < rows ; y++) {
Label label = new Label(String.format("[%d, %d]", x, y));
label.setBackground(new Background(
new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY),
new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, new Insets(1,1,0,0))
));
label.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
grid.add(label, x, y);
}
}
return grid ;
}
public static void main(String[] args) {
launch();
}
}
I am trying to create a table in javafx that allows a user to click on a row to go to one page or double click the row to go to a different page. The problem is that the application registers the event of the single click, but does not wait to see if there is another double click. Is there a way to have the program wait and see if there is another click?
what i have so far looks similar to something like
TableView searchResults;
ObservableList<MovieRow> rows = FXCollections.observableArrayList();
private TableColumn<MovieRow, String> title;
title.setCellValueFactory(new PropertyValueFactory<>("mTitle"));
rows.add(new MovieRow("The cat in the hat"));
searchResults.setItems(rows);
searchResults.setRowFactory(tv -> {
TableRow<MovieRow> row = new TableRow<>();
row.setOnMouseClicked(event -> {
MovieRow tempResult = row.getItem();
if (event.getClickCount() == 1) {
System.out.println(tempResult.getMTitle + " was clicked once");
}else{
System.out.println(tempResult.getMTitle + " was clicked twice");
}
});
return row;
});
public class MovieRow{
private String mTitle;
public MovieRow(String title){
mTitle = title;
}
public String getMTitle() {
return mTitle;
}
}
actual output
single click: The cat in the hat was clicked once
double click: The cat in the hat was clicked once
desired output
single click: The cat in the hat was clicked once
double click: The cat in the hat was clicked twice
I've only found results on handling double clicks by themselves or single clicks by themselves but not having both, so I'm not sure if this is even possible. Any help would be much appreciated. Thanks.
There's no way to do this that's part of the API: you just have to code "have the program wait and see if there is another click" yourself. Note that this means that the single-click action has to have a slight pause before it's executed; there's no way around this (your program can't know what's going to happen in the future). You might consider a different approach (e.g. left button versus right button) to avoid this slightly inconvenient user experience.
However, a solution could look something like this:
public class DoubleClickHandler {
private final PauseTransition delay ;
private final Runnable onSingleClick ;
private final Runnable onDoubleClick ;
private boolean alreadyClickedOnce ;
public DoubleClickHandler(
Duration maxTimeBetweenClicks,
Runnable onSingleClick,
Runnable onDoubleClick) {
alreadyClickedOnce = false ;
this.onSingleClick = onSingleClick ;
this.onDoubleClick = onDoubleClick ;
delay = new PauseTransition(maxTimeBetweenClicks);
delay.setOnFinished(e -> {
alreadyClickedOnce = false ;
onSingleClick.run()
});
}
public void applyToNode(Node node) {
node.setOnMouseClicked(e -> {
delay.stop();
if (alreadyClickedOnce) {
alreadyClickedOnce = false ;
onDoubleClick.run();
} else {
alreadyClickedOnce = true ;
delay.playFromStart();
}
});
}
}
Which you can use with:
searchResults.setRowFactory(tv -> {
TableRow<MovieRow> row = new TableRow<>();
DoubleClickHandler handler = new DoubleClickHandler(
Duration.millis(500),
() -> {
MovieRow tempResult = row.getItem();
System.out.println(tempResult.getMTitle + " was clicked once");
},
() -> {
MovieRow tempResult = row.getItem();
System.out.println(tempResult.getMTitle + " was clicked twice");
}
);
handler.applyToNode(row);
return row ;
});
I encountered the same requirement once and worked on developing a custom event dispatcher. The solution what #James_D provided is clean, simple and works great. But if you want to generalize this behavior on a large scale, you can define a new custom mouse event and an event dispatcher.
The advantage of this approach is its usage will be just like other mouse events and can be handled in both event filters and handlers.
Please check the below demo and the appropriate code:
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.*;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class DoubleClickEventDispatcherDemo extends Application {
#Override
public void start(Stage stage) throws Exception {
Rectangle box1 = new Rectangle(150, 150);
box1.setStyle("-fx-fill:red;-fx-stroke-width:2px;-fx-stroke:black;");
addEventHandlers(box1, "Red Box");
Rectangle box2 = new Rectangle(150, 150);
box2.setStyle("-fx-fill:yellow;-fx-stroke-width:2px;-fx-stroke:black;");
addEventHandlers(box2, "Yellow Box");
HBox pane = new HBox(box1, box2);
pane.setSpacing(10);
pane.setAlignment(Pos.CENTER);
addEventHandlers(pane, "HBox");
Scene scene = new Scene(new StackPane(pane), 450, 300);
stage.setScene(scene);
stage.show();
// THIS IS THE PART OF CODE SETTING CUSTOM EVENT DISPATCHER
scene.setEventDispatcher(new DoubleClickEventDispatcher(scene.getEventDispatcher()));
}
private void addEventHandlers(Node node, String nodeId) {
node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> System.out.println("" + nodeId + " mouse clicked filter"));
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("" + nodeId + " mouse clicked handler"));
node.addEventFilter(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e -> System.out.println("" + nodeId + " mouse double clicked filter"));
node.addEventHandler(CustomMouseEvent.MOUSE_DOUBLE_CLICKED, e -> System.out.println(nodeId + " mouse double clicked handler"));
}
/**
* Custom MouseEvent
*/
interface CustomMouseEvent {
EventType<MouseEvent> MOUSE_DOUBLE_CLICKED = new EventType<>(MouseEvent.ANY, "MOUSE_DBL_CLICKED");
}
/**
* Custom EventDispatcher to differentiate from single click with double click.
*/
class DoubleClickEventDispatcher implements EventDispatcher {
/**
* Default delay to fire a double click event in milliseconds.
*/
private static final long DEFAULT_DOUBLE_CLICK_DELAY = 215;
/**
* Default event dispatcher of a node.
*/
private final EventDispatcher defaultEventDispatcher;
/**
* Timeline for dispatching mouse clicked event.
*/
private Timeline clickedTimeline;
/**
* Constructor.
*
* #param initial Default event dispatcher of a node
*/
public DoubleClickEventDispatcher(final EventDispatcher initial) {
defaultEventDispatcher = initial;
}
#Override
public Event dispatchEvent(final Event event, final EventDispatchChain tail) {
final EventType<? extends Event> type = event.getEventType();
if (type == MouseEvent.MOUSE_CLICKED) {
final MouseEvent mouseEvent = (MouseEvent) event;
final EventTarget eventTarget = event.getTarget();
if (mouseEvent.getClickCount() > 1) {
if (clickedTimeline != null) {
clickedTimeline.stop();
clickedTimeline = null;
final MouseEvent dblClickedEvent = copy(mouseEvent, CustomMouseEvent.MOUSE_DOUBLE_CLICKED);
Event.fireEvent(eventTarget, dblClickedEvent);
}
return mouseEvent;
}
if (clickedTimeline == null) {
final MouseEvent clickedEvent = copy(mouseEvent, mouseEvent.getEventType());
clickedTimeline = new Timeline(new KeyFrame(Duration.millis(DEFAULT_DOUBLE_CLICK_DELAY), e -> {
Event.fireEvent(eventTarget, clickedEvent);
clickedTimeline = null;
}));
clickedTimeline.play();
return mouseEvent;
}
}
return defaultEventDispatcher.dispatchEvent(event, tail);
}
/**
* Creates a copy of the provided mouse event type with the mouse event.
*
* #param e MouseEvent
* #param eventType Event type that need to be created
* #return New mouse event instance
*/
private MouseEvent copy(final MouseEvent e, final EventType<? extends MouseEvent> eventType) {
return new MouseEvent(eventType, e.getSceneX(), e.getSceneY(), e.getScreenX(), e.getScreenY(),
e.getButton(), e.getClickCount(), e.isShiftDown(), e.isControlDown(), e.isAltDown(),
e.isMetaDown(), e.isPrimaryButtonDown(), e.isMiddleButtonDown(),
e.isSecondaryButtonDown(), e.isSynthesized(), e.isPopupTrigger(),
e.isStillSincePress(), e.getPickResult());
}
}
}
I have been trying to show subscript and superscript text in HTMLEditor. there are two buttons for sub and sup mode. the user types the (sub/sup)text in a textfield and press the OK button which allows the textfield text to be rendered as sub or sup in HTMLEditor. The code is as follows:
import java.util.List;
import java.util.regex.Pattern;
import javafx.application.*;
import javafx.collections.FXCollections;
import javafx.event.*;
import javafx.geometry.Orientation;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.web.HTMLEditor;
import javafx.stage.Stage;
public class HTMLEditorCustomizationSample extends Application {
// limits the fonts a user can select from in the html editor.
private static final List<String> limitedFonts = FXCollections.observableArrayList("Arial", "Times New Roman", "Courier New", "Comic Sans MS");
String sup = " ⁺⁻⁼⁽⁾⁰¹²³⁴⁵⁶⁷⁸⁹ᴬᵃᴭᵆᵄᵅᶛᴮᵇᶜᶝᴰᵈᶞᴱᵉᴲᵊᵋᶟᵌᶠᴳᵍᶢˠʰᴴʱᴵⁱᶦᶤᶧᶥʲᴶᶨᶡᴷᵏˡᴸᶫᶪᶩᴹᵐᶬᴺⁿᶰᶮᶯᵑᴼᵒᵓᵔᵕᶱᴽᴾᵖᶲʳᴿʴʵʶˢᶳᶴᵀᵗᶵᵁᵘᶸᵙᶶᶣᵚᶭᶷᵛⱽᶹᶺʷᵂˣʸᶻᶼᶽᶾꝰᵜᵝᵞᵟᶿᵠᵡᵸჼˤⵯ";
String supchars = " +−=()0123456789AaÆᴂɐɑɒBbcɕDdðEeƎəɛɜɜfGgɡɣhHɦIiɪɨᵻɩjJʝɟKklLʟᶅɭMmɱNnɴɲɳŋOoɔᴖᴗɵȢPpɸrRɹɻʁsʂʃTtƫUuᴜᴝʉɥɯɰʊvVʋʌwWxyzʐʑʒꝯᴥβγδθφχнნʕⵡ";
String subchars=" +−=()0123456789aeəhijklmnoprstuvxβγρφχ";
String sub=" ₊₋₌₍₎₀₁₂₃₄₅₆₇₈₉ₐₑₔₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵦᵧᵨᵩᵪ";
char[] csup = sup.toCharArray();
char[] characters = supchars.toCharArray();
char[] csub = sub.toCharArray();
char[] character = subchars.toCharArray();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
// create a new html editor and show it before we start modifying it.
final HTMLEditor htmlEditor = new HTMLEditor();
stage.setScene(new Scene(htmlEditor));
stage.show();
// hide controls we don't need.
hideImageNodesMatching(htmlEditor, Pattern.compile(".*(Cut|Copy|Paste).*"), 0);
Node seperator = htmlEditor.lookup(".separator");
seperator.setVisible(false);
seperator.setManaged(false);
// modify font selections.
int i = 0;
for (Node candidate : (htmlEditor.lookupAll("MenuButton"))) {
// fonts are selected by the second menu in the htmlEditor.
if (candidate instanceof MenuButton && i == 1) {
// limit the font selections to our predefined list.
MenuButton menuButton = (MenuButton) candidate;
List<MenuItem> removalList = FXCollections.observableArrayList();
final List<MenuItem> fontSelections = menuButton.getItems();
for (MenuItem item : fontSelections) {
if (!limitedFonts.contains(item.getText())) {
removalList.add(item);
}
}
fontSelections.removeAll(removalList);
// Select a font from out limited font selection.
// Selection done in Platform.runLater because if you try to do
// the selection immediately, it won't take place.
Platform.runLater(new Runnable() {
#Override
public void run() {
boolean fontSelected = false;
for (final MenuItem item : fontSelections) {
if ("Comic Sans MS".equals(item.getText())) {
if (item instanceof RadioMenuItem) {
((RadioMenuItem) item).setSelected(true);
fontSelected = true;
}
}
}
if (!fontSelected && fontSelections.size() > 0 && fontSelections.get(0) instanceof RadioMenuItem) {
((RadioMenuItem) fontSelections.get(0)).setSelected(true);
}
}
});
}
i++;
}
// add a custom button to the top toolbar.
Node node = htmlEditor.lookup(".top-toolbar");
if (node instanceof ToolBar) {
ToolBar bar = (ToolBar) node;
ToggleButton supButton = new ToggleButton("x²");
ToggleButton subButton = new ToggleButton("x₂");
TextField txt = new TextField();
Button okBtn = new Button("OK");
Button clrBtn = new Button("CLEAR");
ToggleGroup group = new ToggleGroup();
supButton.setToggleGroup(group);
subButton.setToggleGroup(group);
Separator v1=new Separator();
v1.setOrientation(Orientation.VERTICAL);
Separator v2=new Separator();
v2.setOrientation(Orientation.VERTICAL);
txt.setDisable(true);
okBtn.setDisable(true);;
clrBtn.setDisable(true);
bar.getItems().add(v1);
bar.getItems().add(supButton);
bar.getItems().add(subButton);
bar.getItems().add(v2);
bar.getItems().add(txt);
bar.getItems().add(okBtn);
bar.getItems().add(clrBtn);
okBtn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
System.out.println(htmlEditor.getHtmlText());
if (supButton.isSelected()) {
txt.setPromptText(" Enter the superscript text ");
String text = htmlEditor.getHtmlText().replaceAll("</p></body></html>", "");
text = text.replaceAll("<html dir=\"ltr\"><head></head><body contenteditable=\"true\"><p>", "");
System.out.println(text);
text="<p>"+text + "<sup>"+ txt.getText()+"</sup></p>";
System.out.println(text);
htmlEditor.setHtmlText(text);
}
else if (subButton.isSelected()) {
txt.setPromptText(" Enter the superscript text ");
String text = htmlEditor.getHtmlText().replaceAll("</p></body></html>", "");
text = text.replaceAll("<html dir=\"ltr\"><head></head><body contenteditable=\"true\"><p>", "");
System.out.println(text);
text=text + "<sub>"+ txt.getText()+"</sup></p>";
System.out.println(text);
htmlEditor.setHtmlText(text);
}
}
});
clrBtn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
txt.clear();
}
});
supButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
if (supButton.isSelected()) {
txt.setPromptText(" Enter the superscript text ");
txt.setDisable(false);
okBtn.setDisable(false);;
clrBtn.setDisable(false);
}
}
});
subButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
if (subButton.isSelected()) {
txt.setPromptText(" Enter the subscript text ");
txt.setDisable(false);
okBtn.setDisable(false);;
clrBtn.setDisable(false);
}
}
});
}
}
private String convertSupText(String dsup) {
char[] cdsup = dsup.toCharArray();
String data="";
for (int i = 0; i < cdsup.length; i++) {
for (int j = 0; j < characters.length; j++) {
if (cdsup[i] == characters[j]) {
data = data + csup[j];
}
}
}
return data;
}
private String convertSubText(String dsup) {
char[] cdsup = dsup.toCharArray();
String data="";
for (int i = 0; i < cdsup.length; i++) {
for (int j = 0; j < character.length; j++) {
if (cdsup[i] == character[j]) {
data = data + csub[j];
}
}
}
return data;
}
// hide buttons containing nodes whose image url matches a given name pattern.
public void hideImageNodesMatching(Node node, Pattern imageNamePattern, int depth) {
if (node instanceof ImageView) {
ImageView imageView = (ImageView) node;
String url = imageView.getImage().impl_getUrl();
if (url != null && imageNamePattern.matcher(url).matches()) {
Node button = imageView.getParent().getParent();
button.setVisible(false);
button.setManaged(false);
}
}
if (node instanceof Parent) {
for (Node child : ((Parent) node).getChildrenUnmodifiable()) {
hideImageNodesMatching(child, imageNamePattern, depth + 1);
}
}
}
}
The problem is that after adding the subscript or superscript text, the cursor still remains in subscript or superscript mode and every time the text is added it goes on a newline.
#Manoj I think your problem is that you don't know what the HTMLeditor is doing with any text you enter in the textfield (aka WebPage). Appearantly it is applying the your <sub> tag to the next text you enter (adding 1 and typing a normal 2 afterwards results in 12):
<html dir="ltr"><head></head><body contenteditable="true"><p><br><sup>1</sup></p></body></html>
<html dir="ltr"><head></head><body contenteditable="true"><p><br><sup>1<font size="2">1</font></sup></p></body></html>
I looked into the files (HTMLEditor>HTMLEditorSkin>WebPage>twkExecuteCommand) and in the end commands like bold/italic are executed in a dll (jfxwebkit). My knowledge is exceeded here. I see no solution which would not involve rewriting the whole HTMLEditor + native libraries.
(just included this in an answer bc comment length was exceeded)
thought so. I have done a work around using webview along with html editor. And it works fine for now. The code is as follows:
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Separator;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.control.Tooltip;
import javafx.scene.web.HTMLEditor;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
public class FXMLDocumentController implements Initializable {
#FXML
private HTMLEditor HE;
#FXML
private WebView WV;
WebEngine webEngine;
Button supButton;
Button subButton;
Tooltip sup;
Tooltip sub;
Alert info= new Alert(Alert.AlertType.INFORMATION);;
#Override
public void initialize(URL url, ResourceBundle rb) {
// TODO
webEngine = WV.getEngine();
supButton = new Button("x²");
subButton = new Button("x₂");
supButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
info.setTitle("SUCCESS");
info.setHeaderText("Information");
info.setContentText("Use <sup>Text to to superscripted</sup> to use superscript fuction.\n Press Preview button to preview the changes");
info.showAndWait();
}});
subButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
info.setTitle("SUCCESS");
info.setHeaderText("Information");
info.setContentText("Use <sub>Text to to subscripted</sub> to use subscript fuction.\n Press Preview button to preview the changes");
info.showAndWait();
}});
sup = new Tooltip();
sub = new Tooltip();
sup.setText(" Use <sup>Text to to superscripted</sup> to use superscript fuction.\n Press Preview button to preview the changes ");
sub.setText(" Use <sub>Text to to subscripted</sub> to use subscript fuction.\n Press Preview button to preview the changes ");
Node node = HE.lookup(".top-toolbar");
if (node instanceof ToolBar) {
ToolBar bar = (ToolBar) node;
Separator v2 = new Separator();
v2.setOrientation(Orientation.VERTICAL);
bar.getItems().add(supButton);
bar.getItems().add(subButton);
bar.getItems().add(v2);
}
supButton.setTooltip(sup);
subButton.setTooltip(sub);
}
#FXML
private void handleKeyTyped(ActionEvent event) {
String text = HE.getHtmlText();
text = text.replaceAll("<sup>", "<sup>");
text = text.replaceAll("</sup>", "</sup>");
text = text.replaceAll("<sub>", "<sub>");
text = text.replaceAll("</sub>", "</sub>");
webEngine.loadContent(text);
}
}
I'm using UndoFX & ReactFX for implementing the Undo/Redo function for my 2D shape application.
The problem is when i move my shape the EventStream records every X/Y pixel of movement. I just want to record the last position (when the user releases the drag).
What i have tried so far:
Instead of using changesOf(rect.xProperty()).map(c -> new xChange(c)); and
changesOf(rect.yProperty()).map(c -> new yChange(c));
I created a DoubleProperty x,y, and saved the shape x,y Property to these variables when the user mouse is released.
Lastly i change the changesOf to: changesOf(this.x).map(c -> new xChange(c)); and changesOf(this.y).map(c -> new yChange(c));
But that did not work, it behaved just like before.
....
private class xChange extends RectangleChange<Double> {
public xChange(Double oldValue, Double newValue) {
super(oldValue, newValue);
}
public xChange(Change<Number> c) {
super(c.getOldValue().doubleValue(), c.getNewValue().doubleValue());
}
#Override void redo() { rect.setX(newValue); }
#Override xChange invert() { return new xChange(newValue, oldValue); }
#Override Optional<RectangleChange<?>> mergeWith(RectangleChange<?> other) {
if(other instanceof xChange) {
return Optional.of(new xChange(oldValue, ((xChange) other).newValue));
} else {
return Optional.empty();
}
}
#Override
public boolean equals(Object other) {
if(other instanceof xChange) {
xChange that = (xChange) other;
return Objects.equals(this.oldValue, that.oldValue)
&& Objects.equals(this.newValue, that.newValue);
} else {
return false;
}
}
}
...
EventStream<xChange> xChanges = changesOf(rect.xProperty()).map(c -> new xChange(c));
EventStream<yChange> yChanges = changesOf(rect.yProperty()).map(c -> new yChange(c));
changes = merge(widthChanges, heightChanges, xChanges, yChanges);
undoManager = UndoManagerFactory.unlimitedHistoryUndoManager(
changes, // stream of changes to observe
c -> c.invert(), // function to invert a change
c -> c.redo(), // function to undo a change
(c1, c2) -> c1.mergeWith(c2)); // function to merge two changes
You need to merge the changes in x with the changes in y. At present, a change in x followed by a change in y cannot be merged, so if you move the shape so that it alternates x and y changes (e.g. moving it diagonally), then each individual change will not merge with the previous one.
One way to do this is to generate changes whose old and new values are the locations, e.g. represented by Point2D objects. Here's a quick example:
import java.util.Objects;
import java.util.Optional;
import org.fxmisc.undo.UndoManager;
import org.fxmisc.undo.UndoManagerFactory;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.SuspendableEventStream;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class UndoRectangle extends Application {
#Override
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(50, 50, 150, 100);
rect.setFill(Color.CORNFLOWERBLUE);
EventStream<PositionChange> xChanges = EventStreams.changesOf(rect.xProperty()).map(c -> {
double oldX = c.getOldValue().doubleValue();
double newX = c.getNewValue().doubleValue();
double y = rect.getY();
return new PositionChange(new Point2D(oldX, y), new Point2D(newX, y));
});
EventStream<PositionChange> yChanges = EventStreams.changesOf(rect.yProperty()).map(c -> {
double oldY = c.getOldValue().doubleValue();
double newY = c.getNewValue().doubleValue();
double x = rect.getX();
return new PositionChange(new Point2D(x, oldY), new Point2D(x, newY));
});
SuspendableEventStream<PositionChange> posChanges = EventStreams.merge(xChanges, yChanges)
.reducible(PositionChange::merge);
UndoManager undoManager = UndoManagerFactory.unlimitedHistoryUndoManager(posChanges,
PositionChange::invert,
c -> posChanges.suspendWhile(() -> {
rect.setX(c.getNewPosition().getX());
rect.setY(c.getNewPosition().getY());
}),
(c1, c2) -> Optional.of(c1.merge(c2))
);
class MouseLoc { double x, y ; }
MouseLoc mouseLoc = new MouseLoc();
rect.setOnMousePressed(e -> {
mouseLoc.x = e.getSceneX();
mouseLoc.y = e.getSceneY();
});
rect.setOnMouseDragged(e -> {
rect.setX(rect.getX() + e.getSceneX() - mouseLoc.x);
rect.setY(rect.getY() + e.getSceneY() - mouseLoc.y);
mouseLoc.x = e.getSceneX();
mouseLoc.y = e.getSceneY();
});
rect.setOnMouseReleased(e -> undoManager.preventMerge());
Pane pane = new Pane(rect);
Button undo = new Button("Undo");
undo.disableProperty().bind(Bindings.not(undoManager.undoAvailableProperty()));
undo.setOnAction(e -> undoManager.undo());
Button redo = new Button("Redo");
redo.disableProperty().bind(Bindings.not(undoManager.redoAvailableProperty()));
redo.setOnAction(e -> undoManager.redo());
HBox buttons = new HBox(5, undo, redo);
buttons.setAlignment(Pos.CENTER);
BorderPane.setMargin(buttons, new Insets(5));
BorderPane root = new BorderPane(pane, null, null, buttons, null);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class PositionChange {
private final Point2D oldPosition ;
private final Point2D newPosition ;
public PositionChange(Point2D oldPos, Point2D newPos) {
this.oldPosition = oldPos ;
this.newPosition = newPos ;
}
public Point2D getOldPosition() {
return oldPosition;
}
public Point2D getNewPosition() {
return newPosition;
}
public PositionChange merge(PositionChange other) {
return new PositionChange(oldPosition, other.newPosition);
}
public PositionChange invert() {
return new PositionChange(newPosition, oldPosition);
}
#Override
public boolean equals(Object o) {
if (o instanceof PositionChange) {
PositionChange other = (PositionChange) o ;
return Objects.equals(oldPosition, other.oldPosition)
&& Objects.equals(newPosition, other.newPosition);
} else return false ;
}
#Override
public int hashCode() {
return Objects.hash(oldPosition, newPosition);
}
}
public static void main(String[] args) {
launch(args);
}
}
Note that it's important the "undo" is implemented as an "atomic" change, so the undo manager sees (and ignores) a single change when you implement the undo. This can be achieved by suspending the event stream during the undo.