Allow user to resize an undecorated Stage
Asked Answered
R

13

28

I am working on making a screen recorder in JavaFX and one utility that is mandatory in the screen recorder is to let the user define how much area to record.

I managed to make an undecorated , semi-transparent Stage that can be dragged around to define the area and added a close button to let the user confirm the area which is to be recorded.

Now, how do I let the user resize the stage by dragging it by its edges ?

SSCCE:

package draggable;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBuilder;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.StackPaneBuilder;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class DraggableStage extends Application{

    Button close;
    StackPane holder;
    Rectangle2D maxBounds;
    Scene theScene;

    double pressedX;
    double pressedY;
    double draggedX;
    double draggedY;

    @Override
    public void start(Stage stage) throws Exception {
        final Stage theStage = stage;

        // determine how big the screen is
        maxBounds = Screen.getPrimary().getVisualBounds(); 

        //create the close button
        close = ButtonBuilder
                .create()
                .text("Close")
                .build();

        //create the StackPane holder for the button
        holder = StackPaneBuilder
                    .create()
                    .alignment(Pos.CENTER)
                    .children(close)
                    .build();

        // you cannot resize the screen beyond the max resolution of the screen
        holder.setMaxSize(maxBounds.getWidth(), maxBounds.getHeight());

        //you cannot resize under half the width and height of the screen
        holder.setMinSize(maxBounds.getWidth() / 2,maxBounds.getHeight() / 2);

        //the scene where it all happens
        theScene = SceneBuilder
                      .create()
                      .root(holder)
                      .width(maxBounds.getWidth() / 2)
                      .height(maxBounds.getHeight() / 2)
                      .build();

        // add the button listeners
        close.setOnAction(new EventHandler<ActionEvent>(){
            @Override
            public void handle(ActionEvent event) {
                theStage.close();
            }
        });

        // add the drag and press listener for the StackPane
        holder.setOnMousePressed(new EventHandler<MouseEvent>(){
            @Override
            public void handle(MouseEvent e) {
                pressedX = e.getX();
                pressedY = e.getY();
            }
        });

        holder.setOnMouseDragged(new EventHandler<MouseEvent>(){
            @Override
            public void handle(MouseEvent e) {
                draggedX = e.getX();
                draggedY = e.getY();

                double differenceX = draggedX - pressedX;
                double differenceY = draggedY - pressedY;

                theStage.setX(theStage.getX() + differenceX);
                theStage.setY(theStage.getY() + differenceY); 
            }
        });

        //the mandatory mumbo jumbo
        theScene.setFill(Color.rgb(128, 128, 128, 0.5));
        stage.initStyle(StageStyle.TRANSPARENT);
        stage.setScene(theScene);
        stage.sizeToScene();
        stage.show();
    }

    public static void main(String[] args) {
        Application.launch("draggable.DraggableStage");
    }
}  

Image:
enter image description here

Respondent answered 18/10, 2013 at 16:55 Comment(5)
Look at the source to the Ensemble sample application available in the JavaFX demos and samples link or use the Undecorator.Chamonix
@Chamonix What happened to the Ensemble website ?Respondent
Ensemble is (currently) hosted as an browser embedded application (applet) by Oracle. However, I still recommend downloading the Ensemble source, building a stand-alone application (executable jar file) and running the built stand-alone Ensemble application (via java -jar Ensemble.jar) as that is a more reliable execution model.Chamonix
@LittleChild can you find any solution i have same issue ??Headboard
@LittleChild : have you done with this application ? I am learning to make the same.If you can help me out.Please respond here if you got the solution #23799108Xray
D
49

I created a ResizeHelper class which can help you in that case, usage:

ResizeHelper.addResizeListener(yourStage);

the helper class:

import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

//created by Alexander Berg
public class ResizeHelper {

    public static void addResizeListener(Stage stage) {
        ResizeListener resizeListener = new ResizeListener(stage);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, resizeListener);
        ObservableList<Node> children = stage.getScene().getRoot().getChildrenUnmodifiable();
        for (Node child : children) {
            addListenerDeeply(child, resizeListener);
        }
    }

    public static void addListenerDeeply(Node node, EventHandler<MouseEvent> listener) {
        node.addEventHandler(MouseEvent.MOUSE_MOVED, listener);
        node.addEventHandler(MouseEvent.MOUSE_PRESSED, listener);
        node.addEventHandler(MouseEvent.MOUSE_DRAGGED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, listener);
        if (node instanceof Parent) {
            Parent parent = (Parent) node;
            ObservableList<Node> children = parent.getChildrenUnmodifiable();
            for (Node child : children) {
                addListenerDeeply(child, listener);
            }
        }
    }

    static class ResizeListener implements EventHandler<MouseEvent> {
        private Stage stage;
        private Cursor cursorEvent = Cursor.DEFAULT;
        private int border = 4;
        private double startX = 0;
        private double startY = 0;

        public ResizeListener(Stage stage) {
            this.stage = stage;
        }

        @Override
        public void handle(MouseEvent mouseEvent) {
            EventType<? extends MouseEvent> mouseEventType = mouseEvent.getEventType();
            Scene scene = stage.getScene();

            double mouseEventX = mouseEvent.getSceneX(), 
                   mouseEventY = mouseEvent.getSceneY(),
                   sceneWidth = scene.getWidth(),
                   sceneHeight = scene.getHeight();

            if (MouseEvent.MOUSE_MOVED.equals(mouseEventType) == true) {
                if (mouseEventX < border && mouseEventY < border) {
                    cursorEvent = Cursor.NW_RESIZE;
                } else if (mouseEventX < border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SW_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY < border) {
                    cursorEvent = Cursor.NE_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SE_RESIZE;
                } else if (mouseEventX < border) {
                    cursorEvent = Cursor.W_RESIZE;
                } else if (mouseEventX > sceneWidth - border) {
                    cursorEvent = Cursor.E_RESIZE;
                } else if (mouseEventY < border) {
                    cursorEvent = Cursor.N_RESIZE;
                } else if (mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.S_RESIZE;
                } else {
                    cursorEvent = Cursor.DEFAULT;
                }
                scene.setCursor(cursorEvent);
            } else if(MouseEvent.MOUSE_EXITED.equals(mouseEventType) || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)){
                scene.setCursor(Cursor.DEFAULT);
            } else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType) == true) {
                startX = stage.getWidth() - mouseEventX;
                startY = stage.getHeight() - mouseEventY;
            } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType) == true) {
                if (Cursor.DEFAULT.equals(cursorEvent) == false) {
                    if (Cursor.W_RESIZE.equals(cursorEvent) == false && Cursor.E_RESIZE.equals(cursorEvent) == false) {
                        double minHeight = stage.getMinHeight() > (border*2) ? stage.getMinHeight() : (border*2);
                        if (Cursor.NW_RESIZE.equals(cursorEvent) == true || Cursor.N_RESIZE.equals(cursorEvent) == true || Cursor.NE_RESIZE.equals(cursorEvent) == true) {
                            if (stage.getHeight() > minHeight || mouseEventY < 0) {
                                stage.setHeight(stage.getY() - mouseEvent.getScreenY() + stage.getHeight());
                                stage.setY(mouseEvent.getScreenY());
                            }
                        } else {
                            if (stage.getHeight() > minHeight || mouseEventY + startY - stage.getHeight() > 0) {
                                stage.setHeight(mouseEventY + startY);
                            }
                        }
                    }

                    if (Cursor.N_RESIZE.equals(cursorEvent) == false && Cursor.S_RESIZE.equals(cursorEvent) == false) {
                        double minWidth = stage.getMinWidth() > (border*2) ? stage.getMinWidth() : (border*2);
                        if (Cursor.NW_RESIZE.equals(cursorEvent) == true || Cursor.W_RESIZE.equals(cursorEvent) == true || Cursor.SW_RESIZE.equals(cursorEvent) == true) {
                            if (stage.getWidth() > minWidth || mouseEventX < 0) {
                                stage.setWidth(stage.getX() - mouseEvent.getScreenX() + stage.getWidth());
                                stage.setX(mouseEvent.getScreenX());
                            }
                        } else {
                            if (stage.getWidth() > minWidth || mouseEventX + startX - stage.getWidth() > 0) {
                                stage.setWidth(mouseEventX + startX);
                            }
                        }
                    }
                }

            }
        }
    }
}
Dun answered 3/6, 2014 at 14:18 Comment(9)
@Rahmon, click on the "edit" link to get the raw source.Analphabetic
This is a nice solution, thanks a lot! I've just found a little bug - often when I'm moving the mouse from one side of the stage to the other, the cursor changes to a resize-cursor in the middle of the stage. Do you know what the problem is?Roue
You really don't have to write 'MouseEvent.MOUSE_MOVED.equals(mouseEventType) == true'. You could just write MouseEvent.MOUSE_MOVED.equals(mouseEventType)Reta
@Roue I had the same (or at least similar) issue. In my case, there was a Node that was close enough to the edge of the stage that the mouse events started going to this Node before it actually left the "border". This caused the cursor to stay as a resizer when moving onto the Node. If you have the freedom to do so, just add enough padding to create a gap between the border and the Node. I'm not sure why this was necessary though - I thought the point of addListenerDeeply was to avoid this problem.Unknit
And if you want to add drag functionality on this class you add this fields ` private double xForDrag = 0; private double yForDrag = 0;` on ResizeListener Class and this xForDrag = stage.getX() - mouseEvent.getScreenX(); yForDrag = stage.getY() - mouseEvent.getScreenY(); on the else if clause of MOUSE_PRESSED` and lastly add else { stage.setX(mouseEvent.getScreenX() + xForDrag); stage.setY(mouseEvent.getScreenY() + yForDrag); } on if clause of Cursor.DEFAULT.equals(cursorEvent) == false.Albion
What is the purpose of adding the listener to all children? Just the scene wouldn't be enough?Restate
I know the answer is old, and things might have changed. But, I'm trying to re-use this answer but I'm finding that not all the edges work for resizing (nothing happens) and the ones that do work, makes the window smaller regardless of whether I pull the mouse outwards or inwards. Any idea on what I've missed? (Should mention that I'm very new to JavaFX, but with a fair amount of "regular" Java experience)Pornocracy
@Pornocracy I think your issue was due to the padding of the root container not taken into account. I posted a new solution which does that, in case you are still interested in this issueCharteris
@Yevhenii Kanivets has the best update to this answer. I included funcitional dragging ability that does not break his code in my answer.Arrears
A
7

I updated @Yevhenii.Kanivets ' updated version of @Alexander.Berg 's ResizeHelper class. This includes the ability to drag the scene, with the best implementation I've found of ResizeHelper. Also set minimum stage size to a height of 1 and width of 1 instead of 0, because there are bugs if they stay at 0.

Someone still can improve on this answer. The SW, W, NW, N, and NE edges do not resize smoothly, although it's a minor issue. When resizing from those edges, the scene also can shift slightly. They should resize smoothly like the S, SE, and E edges do. The latter edges do not shift the scene.

  1. @Cripperz 's implementation with dragging was still buggy.
  2. @Alessandro Roaro same issue as above.
  3. @Valeriy Blyus same issue as above.
  4. @Joseph Adomatis 's implementation is not generic.

I cannot speak for @M.K 's implementation as I am not using Kotlin, but I'd give that a shot first if you are.

import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

/**
 * Util class to handle window resizing when a stage style set to StageStyle.UNDECORATED.
 * Includes dragging of the stage.
 * Original on 6/13/14.
 * Updated on 8/15/17.
 * Updated on 12/19/19.
 *
 * @author Alexander.Berg
 * @author Evgenii Kanivets
 * @author Zachary Perales
 */

public class ResizeHelper {

    public static void addResizeListener(Stage stage) {
        addResizeListener(stage, 1, 1, Double.MAX_VALUE, Double.MAX_VALUE);
    }

    public static void addResizeListener(Stage stage, double minWidth, double minHeight, double maxWidth, double maxHeight) {
        ResizeListener resizeListener = new ResizeListener(stage);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, resizeListener);

        resizeListener.setMinWidth(minWidth);
        resizeListener.setMinHeight(minHeight);
        resizeListener.setMaxWidth(maxWidth);
        resizeListener.setMaxHeight(maxHeight);

        ObservableList<Node> children = stage.getScene().getRoot().getChildrenUnmodifiable();
        for (Node child : children) {
            addListenerDeeply(child, resizeListener);
        }
    }

    private static void addListenerDeeply(Node node, EventHandler<MouseEvent> listener) {
        node.addEventHandler(MouseEvent.MOUSE_MOVED, listener);
        node.addEventHandler(MouseEvent.MOUSE_PRESSED, listener);
        node.addEventHandler(MouseEvent.MOUSE_DRAGGED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, listener);
        if (node instanceof Parent) {
            Parent parent = (Parent) node;
            ObservableList<Node> children = parent.getChildrenUnmodifiable();
            for (Node child : children) {
                addListenerDeeply(child, listener);
            }
        }
    }

    static class ResizeListener implements EventHandler<MouseEvent> {
        private Stage stage;
        private Cursor cursorEvent = Cursor.DEFAULT;
        private boolean resizing = true;
        private int border = 4;
        private double startX = 0;
        private double startY = 0;
        private double screenOffsetX = 0;
        private double screenOffsetY = 0;

        // Max and min sizes for controlled stage
        private double minWidth;
        private double maxWidth;
        private double minHeight;
        private double maxHeight;

        public ResizeListener(Stage stage) {
            this.stage = stage;
        }

        public void setMinWidth(double minWidth) {
            this.minWidth = minWidth;
        }

        public void setMaxWidth(double maxWidth) {
            this.maxWidth = maxWidth;
        }

        public void setMinHeight(double minHeight) {
            this.minHeight = minHeight;
        }

        public void setMaxHeight(double maxHeight) {
            this.maxHeight = maxHeight;
        }

        @Override
        public void handle(MouseEvent mouseEvent) {
            EventType<? extends MouseEvent> mouseEventType = mouseEvent.getEventType();
            Scene scene = stage.getScene();

            double mouseEventX = mouseEvent.getSceneX(),
                    mouseEventY = mouseEvent.getSceneY(),
                    sceneWidth = scene.getWidth(),
                    sceneHeight = scene.getHeight();

            if (MouseEvent.MOUSE_MOVED.equals(mouseEventType)) {
                if (mouseEventX < border && mouseEventY < border) {
                    cursorEvent = Cursor.NW_RESIZE;
                } else if (mouseEventX < border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SW_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY < border) {
                    cursorEvent = Cursor.NE_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SE_RESIZE;
                } else if (mouseEventX < border) {
                    cursorEvent = Cursor.W_RESIZE;
                } else if (mouseEventX > sceneWidth - border) {
                    cursorEvent = Cursor.E_RESIZE;
                } else if (mouseEventY < border) {
                    cursorEvent = Cursor.N_RESIZE;
                } else if (mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.S_RESIZE;
                } else {
                    cursorEvent = Cursor.DEFAULT;
                }
                scene.setCursor(cursorEvent);
            } else if (MouseEvent.MOUSE_EXITED.equals(mouseEventType) || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)) {
                scene.setCursor(Cursor.DEFAULT);
            } else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) {
                startX = stage.getWidth() - mouseEventX;
                startY = stage.getHeight() - mouseEventY;
            } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType)) {
                if (!Cursor.DEFAULT.equals(cursorEvent)) {
                    resizing = true;
                    if (!Cursor.W_RESIZE.equals(cursorEvent) && !Cursor.E_RESIZE.equals(cursorEvent)) {
                        double minHeight = stage.getMinHeight() > (border * 2) ? stage.getMinHeight() : (border * 2);
                        if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.N_RESIZE.equals(cursorEvent)
                                || Cursor.NE_RESIZE.equals(cursorEvent)) {
                            if (stage.getHeight() > minHeight || mouseEventY < 0) {
                                setStageHeight(stage.getY() - mouseEvent.getScreenY() + stage.getHeight());
                                stage.setY(mouseEvent.getScreenY() );
                            }
                        } else {
                            if (stage.getHeight() > minHeight || mouseEventY + startY - stage.getHeight() > 0) {
                                setStageHeight(mouseEventY + startY);
                            }
                        }
                    }

                    if (!Cursor.N_RESIZE.equals(cursorEvent) && !Cursor.S_RESIZE.equals(cursorEvent)) {
                        double minWidth = stage.getMinWidth() > (border * 2) ? stage.getMinWidth() : (border * 2);
                        if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.W_RESIZE.equals(cursorEvent)
                                || Cursor.SW_RESIZE.equals(cursorEvent)) {
                            if (stage.getWidth() > minWidth || mouseEventX < 0) {
                                setStageWidth(stage.getX() - mouseEvent.getScreenX() + stage.getWidth());
                                stage.setX(mouseEvent.getScreenX());
                            }
                        } else {
                            if (stage.getWidth() > minWidth || mouseEventX + startX - stage.getWidth() > 0) {
                                setStageWidth(mouseEventX + startX);
                            }
                        }
                    }
                    resizing = false;
                }
            }

            if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType) && Cursor.DEFAULT.equals(cursorEvent)) {
                resizing = false;
                screenOffsetX = stage.getX() - mouseEvent.getScreenX();
                screenOffsetY = stage.getY() - mouseEvent.getScreenY();
            }

            if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType) && Cursor.DEFAULT.equals(cursorEvent) && resizing == false) {
                stage.setX(mouseEvent.getScreenX() + screenOffsetX);
                stage.setY(mouseEvent.getScreenY() + screenOffsetY);
            }

        }

        private void setStageWidth(double width) {
            width = Math.min(width, maxWidth);
            width = Math.max(width, minWidth);
            stage.setWidth(width);
        }

        private void setStageHeight(double height) {
            height = Math.min(height, maxHeight);
            height = Math.max(height, minHeight);
            stage.setHeight(height);
        }

    }
}

Edit:

I've updated this code to not drag any scrollbars, as nobody wants that functionality and I needed to remove it myself. It should be easy to disallow additional controls from being dragged if you come across the need, by comparing my two code submissions on here.

Note that if the disallowed control is on the edge of the scene, you need to wrap it in something draggable to access resizing on those edges.

public class ResizeHelper {
    static boolean isScrollbar = false;

    public static void addResizeListener(Stage stage) {
        addResizeListener(stage, 1, 1, Double.MAX_VALUE, Double.MAX_VALUE);
    }

    public static void addResizeListener(Stage stage, double minWidth, double minHeight, double maxWidth, double maxHeight) {
        ResizeListener resizeListener = new ResizeListener(stage);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, resizeListener);

        resizeListener.setMinWidth(minWidth);
        resizeListener.setMinHeight(minHeight);
        resizeListener.setMaxWidth(maxWidth);
        resizeListener.setMaxHeight(maxHeight);


        ObservableList<Node> children = stage.getScene().getRoot().getChildrenUnmodifiable();
        for (Node child : children) {
            if (child instanceof ScrollBar) {
                isScrollbar = true;
            } else if (!(child instanceof ScrollBar)) {
                isScrollbar = false;
                addListenerDeeply(child, resizeListener);
            }
        }
    }

    private static void addListenerDeeply(Node node, EventHandler<MouseEvent> listener) {
        node.addEventHandler(MouseEvent.MOUSE_MOVED, listener);
        node.addEventHandler(MouseEvent.MOUSE_PRESSED, listener);
        node.addEventHandler(MouseEvent.MOUSE_DRAGGED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, listener);
        if (node instanceof Parent) {
            Parent parent = (Parent) node;
            ObservableList<Node> children = parent.getChildrenUnmodifiable();
            for (Node child : children) {
                if (child instanceof ScrollBar) {
                    isScrollbar = true;
                } else if (!(child instanceof ScrollBar)) {
                    isScrollbar = false;
                    addListenerDeeply(child, listener);
                }
            }
        }
    }

    static class ResizeListener implements EventHandler<MouseEvent> {
        private Stage stage;
        private Cursor cursorEvent = Cursor.DEFAULT;
        private boolean resizing = true;
        private int border = 4;
        private double startX = 0;
        private double startY = 0;
        private double screenOffsetX = 0;
        private double screenOffsetY = 0;

        // Max and min sizes for controlled stage
        private double minWidth;
        private double maxWidth;
        private double minHeight;
        private double maxHeight;

        public ResizeListener(Stage stage) {
            this.stage = stage;
        }

        public void setMinWidth(double minWidth) {
            this.minWidth = minWidth;
        }

        public void setMaxWidth(double maxWidth) {
            this.maxWidth = maxWidth;
        }

        public void setMinHeight(double minHeight) {
            this.minHeight = minHeight;
        }

        public void setMaxHeight(double maxHeight) {
            this.maxHeight = maxHeight;
        }

        @Override
        public void handle(MouseEvent mouseEvent) {
            EventType<? extends MouseEvent> mouseEventType = mouseEvent.getEventType();
            Scene scene = stage.getScene();

            double mouseEventX = mouseEvent.getSceneX(),
                    mouseEventY = mouseEvent.getSceneY(),
                    sceneWidth = scene.getWidth(),
                    sceneHeight = scene.getHeight();

            if (MouseEvent.MOUSE_MOVED.equals(mouseEventType) && stage.isMaximized() == false ) {
                if (mouseEventX < border && mouseEventY < border) {
                    cursorEvent = Cursor.NW_RESIZE;
                } else if (mouseEventX < border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SW_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY < border) {
                    cursorEvent = Cursor.NE_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SE_RESIZE;
                } else if (mouseEventX < border) {
                    cursorEvent = Cursor.W_RESIZE;
                } else if (mouseEventX > sceneWidth - border) {
                    cursorEvent = Cursor.E_RESIZE;
                } else if (mouseEventY < border) {
                    cursorEvent = Cursor.N_RESIZE;
                } else if (mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.S_RESIZE;
                } else {
                    cursorEvent = Cursor.DEFAULT;
                }
                scene.setCursor(cursorEvent);
            } else if (MouseEvent.MOUSE_EXITED.equals(mouseEventType) || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)) {
                scene.setCursor(Cursor.DEFAULT);
            } else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) {
                startX = stage.getWidth() - mouseEventX;
                startY = stage.getHeight() - mouseEventY;
            } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType)) {
                if (!Cursor.DEFAULT.equals(cursorEvent)) {
                    resizing = true;
                    if (!Cursor.W_RESIZE.equals(cursorEvent) && !Cursor.E_RESIZE.equals(cursorEvent)) {
                        double minHeight = stage.getMinHeight() > (border * 2) ? stage.getMinHeight() : (border * 2);
                        if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.N_RESIZE.equals(cursorEvent)
                                || Cursor.NE_RESIZE.equals(cursorEvent)) {
                            if (stage.getHeight() > minHeight || mouseEventY < 0) {
                                setStageHeight(stage.getY() - mouseEvent.getScreenY() + stage.getHeight());
                                stage.setY(mouseEvent.getScreenY() );
                            }
                        } else {
                            if (stage.getHeight() > minHeight || mouseEventY + startY - stage.getHeight() > 0) {
                                setStageHeight(mouseEventY + startY);
                            }
                        }
                    }

                    if (!Cursor.N_RESIZE.equals(cursorEvent) && !Cursor.S_RESIZE.equals(cursorEvent)) {
                        double minWidth = stage.getMinWidth() > (border * 2) ? stage.getMinWidth() : (border * 2);
                        if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.W_RESIZE.equals(cursorEvent)
                                || Cursor.SW_RESIZE.equals(cursorEvent)) {
                            if (stage.getWidth() > minWidth || mouseEventX < 0) {
                                setStageWidth(stage.getX() - mouseEvent.getScreenX() + stage.getWidth());
                                stage.setX(mouseEvent.getScreenX());
                            }
                        } else {
                            if (stage.getWidth() > minWidth || mouseEventX + startX - stage.getWidth() > 0) {
                                setStageWidth(mouseEventX + startX);
                            }
                        }
                    }
                    resizing = false;
                }
            }

            if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType) && Cursor.DEFAULT.equals(cursorEvent) ) {
                resizing = false;
                screenOffsetX = stage.getX() - mouseEvent.getScreenX();
                screenOffsetY = stage.getY() - mouseEvent.getScreenY();

            }

            if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType) && Cursor.DEFAULT.equals(cursorEvent) && resizing == false) {
                stage.setX(mouseEvent.getScreenX() + screenOffsetX);
                stage.setY(mouseEvent.getScreenY() + screenOffsetY);

            }

        }

        private void setStageWidth(double width) {
            width = Math.min(width, maxWidth);
            width = Math.max(width, minWidth);
            stage.setWidth(width);
        }

        private void setStageHeight(double height) {
            height = Math.min(height, maxHeight);
            height = Math.max(height, minHeight);
            stage.setHeight(height);
        }

    }

}
Arrears answered 19/12, 2019 at 15:29 Comment(1)
Bad approach, it allows moving the stage by dragging any node inside the stage (including Pane nodes)Ankledeep
H
6

Here is an updated version of ResizeHelper posted by @Alexander.Berg, which supports min and max stage sizes.

package sem.helper;

import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

/**
 * Util class to handle window resizing when a stage style set to StageStyle.UNDECORATED.
 * Created on 8/15/17.
 *
 * @author Evgenii Kanivets
 */
public class ResizeHelper {

    public static void addResizeListener(Stage stage) {
        addResizeListener(stage, 0, 0, Double.MAX_VALUE, Double.MAX_VALUE);
    }

    public static void addResizeListener(Stage stage, double minWidth, double minHeight, double maxWidth, double maxHeight) {
        ResizeListener resizeListener = new ResizeListener(stage);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, resizeListener);

        resizeListener.setMinWidth(minWidth);
        resizeListener.setMinHeight(minHeight);
        resizeListener.setMaxWidth(maxWidth);
        resizeListener.setMaxHeight(maxHeight);

        ObservableList<Node> children = stage.getScene().getRoot().getChildrenUnmodifiable();
        for (Node child : children) {
            addListenerDeeply(child, resizeListener);
        }
    }

    private static void addListenerDeeply(Node node, EventHandler<MouseEvent> listener) {
        node.addEventHandler(MouseEvent.MOUSE_MOVED, listener);
        node.addEventHandler(MouseEvent.MOUSE_PRESSED, listener);
        node.addEventHandler(MouseEvent.MOUSE_DRAGGED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, listener);
        if (node instanceof Parent) {
            Parent parent = (Parent) node;
            ObservableList<Node> children = parent.getChildrenUnmodifiable();
            for (Node child : children) {
                addListenerDeeply(child, listener);
            }
        }
    }

    static class ResizeListener implements EventHandler<MouseEvent> {
        private Stage stage;
        private Cursor cursorEvent = Cursor.DEFAULT;
        private int border = 4;
        private double startX = 0;
        private double startY = 0;

        // Max and min sizes for controlled stage
        private double minWidth;
        private double maxWidth;
        private double minHeight;
        private double maxHeight;

        public ResizeListener(Stage stage) {
            this.stage = stage;
        }

        public void setMinWidth(double minWidth) {
            this.minWidth = minWidth;
        }

        public void setMaxWidth(double maxWidth) {
            this.maxWidth = maxWidth;
        }

        public void setMinHeight(double minHeight) {
            this.minHeight = minHeight;
        }

        public void setMaxHeight(double maxHeight) {
            this.maxHeight = maxHeight;
        }

        @Override
        public void handle(MouseEvent mouseEvent) {
            EventType<? extends MouseEvent> mouseEventType = mouseEvent.getEventType();
            Scene scene = stage.getScene();

            double mouseEventX = mouseEvent.getSceneX(),
                    mouseEventY = mouseEvent.getSceneY(),
                    sceneWidth = scene.getWidth(),
                    sceneHeight = scene.getHeight();

            if (MouseEvent.MOUSE_MOVED.equals(mouseEventType)) {
                if (mouseEventX < border && mouseEventY < border) {
                    cursorEvent = Cursor.NW_RESIZE;
                } else if (mouseEventX < border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SW_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY < border) {
                    cursorEvent = Cursor.NE_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SE_RESIZE;
                } else if (mouseEventX < border) {
                    cursorEvent = Cursor.W_RESIZE;
                } else if (mouseEventX > sceneWidth - border) {
                    cursorEvent = Cursor.E_RESIZE;
                } else if (mouseEventY < border) {
                    cursorEvent = Cursor.N_RESIZE;
                } else if (mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.S_RESIZE;
                } else {
                    cursorEvent = Cursor.DEFAULT;
                }
                scene.setCursor(cursorEvent);
            } else if (MouseEvent.MOUSE_EXITED.equals(mouseEventType) || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)) {
                scene.setCursor(Cursor.DEFAULT);
            } else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) {
                startX = stage.getWidth() - mouseEventX;
                startY = stage.getHeight() - mouseEventY;
            } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType)) {
                if (!Cursor.DEFAULT.equals(cursorEvent)) {
                    if (!Cursor.W_RESIZE.equals(cursorEvent) && !Cursor.E_RESIZE.equals(cursorEvent)) {
                        double minHeight = stage.getMinHeight() > (border * 2) ? stage.getMinHeight() : (border * 2);
                        if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.N_RESIZE.equals(cursorEvent)
                                || Cursor.NE_RESIZE.equals(cursorEvent)) {
                            if (stage.getHeight() > minHeight || mouseEventY < 0) {
                                setStageHeight(stage.getY() - mouseEvent.getScreenY() + stage.getHeight());
                                stage.setY(mouseEvent.getScreenY());
                            }
                        } else {
                            if (stage.getHeight() > minHeight || mouseEventY + startY - stage.getHeight() > 0) {
                                setStageHeight(mouseEventY + startY);
                            }
                        }
                    }

                    if (!Cursor.N_RESIZE.equals(cursorEvent) && !Cursor.S_RESIZE.equals(cursorEvent)) {
                        double minWidth = stage.getMinWidth() > (border * 2) ? stage.getMinWidth() : (border * 2);
                        if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.W_RESIZE.equals(cursorEvent)
                                || Cursor.SW_RESIZE.equals(cursorEvent)) {
                            if (stage.getWidth() > minWidth || mouseEventX < 0) {
                                setStageWidth(stage.getX() - mouseEvent.getScreenX() + stage.getWidth());
                                stage.setX(mouseEvent.getScreenX());
                            }
                        } else {
                            if (stage.getWidth() > minWidth || mouseEventX + startX - stage.getWidth() > 0) {
                                setStageWidth(mouseEventX + startX);
                            }
                        }
                    }
                }

            }
        }

        private void setStageWidth(double width) {
            width = Math.min(width, maxWidth);
            width = Math.max(width, minWidth);
            stage.setWidth(width);
        }

        private void setStageHeight(double height) {
            height = Math.min(height, maxHeight);
            height = Math.max(height, minHeight);
            stage.setHeight(height);
        }

    }
}
Haemin answered 15/8, 2017 at 8:28 Comment(2)
Since it uses setWidth and setHeight in the original implementation, you can just call setMinWidth and setMaxWidth in the Stage to implement this.Longrange
This is the best answer I've found so far. @Longrange implementation has too many bugs. I've added the ability to drag the stage to this code, while not allowing the stage to drag while resizing. Also set default min H and W to 1 instead of 0. Someone still can improve on my answer, or yours, if they do not want stage dragging. The SW, W, NW, N, and NE edges do not resize smoothly, although it's a minor issue. They should resize smoothly like the S, SE, and E edges do.Arrears
L
5

Here is a revision @Alexander.Berg 's post. This correctly handles the minWidth and maxWidth properties of the Scene. His version does not do this: it completely ignores maxWidth and butchers minWidth. There is also a piece of code at the bottom that allows dragging the window.

import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

public class ResizeHelper {

    public static void addResizeListener(Stage stage) {
        ResizeListener resizeListener = new ResizeListener(stage);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, resizeListener);
        ObservableList<Node> children = stage.getScene().getRoot().getChildrenUnmodifiable();
        for (Node child : children) {
            addListenerDeeply(child, resizeListener);
        }
    }

    private static void addListenerDeeply(Node node, EventHandler<MouseEvent> listener) {
        node.addEventHandler(MouseEvent.MOUSE_MOVED, listener);
        node.addEventHandler(MouseEvent.MOUSE_PRESSED, listener);
        node.addEventHandler(MouseEvent.MOUSE_DRAGGED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, listener);
        if (node instanceof Parent) {
            Parent parent = (Parent) node;
            ObservableList<Node> children = parent.getChildrenUnmodifiable();
            for (Node child : children) {
                addListenerDeeply(child, listener);
            }
        }
    }

    private static class ResizeListener implements EventHandler<MouseEvent> {
        private Stage stage;
        private Cursor cursorEvent = Cursor.DEFAULT;
        private int border = 4;
        private double startX = 0;
        private double startY = 0;
        private double startScreenX = 0;
        private double startScreenY = 0;

        public ResizeListener(Stage stage) {
            this.stage = stage;
        }

        @Override
        public void handle(MouseEvent mouseEvent) {
            EventType<? extends MouseEvent> mouseEventType = mouseEvent.getEventType();
            Scene scene = stage.getScene();

            double mouseEventX = mouseEvent.getSceneX();
            double mouseEventY = mouseEvent.getSceneY();
            double sceneWidth = scene.getWidth();
            double sceneHeight = scene.getHeight();

            if (MouseEvent.MOUSE_MOVED.equals(mouseEventType)) {
                if (mouseEventX < border && mouseEventY < border) {
                    cursorEvent = Cursor.NW_RESIZE;
                } else if (mouseEventX < border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SW_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY < border) {
                    cursorEvent = Cursor.NE_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SE_RESIZE;
                } else if (mouseEventX < border) {
                    cursorEvent = Cursor.W_RESIZE;
                } else if (mouseEventX > sceneWidth - border) {
                    cursorEvent = Cursor.E_RESIZE;
                } else if (mouseEventY < border) {
                    cursorEvent = Cursor.N_RESIZE;
                } else if (mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.S_RESIZE;
                } else {
                    cursorEvent = Cursor.DEFAULT;
                }
                scene.setCursor(cursorEvent);
            } else if (MouseEvent.MOUSE_EXITED.equals(mouseEventType) || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)) {
                scene.setCursor(Cursor.DEFAULT);
            } else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) {
                startX = stage.getWidth() - mouseEventX;
                startY = stage.getHeight() - mouseEventY;
            } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType)) {
                if (!Cursor.DEFAULT.equals(cursorEvent)) {
                    if (!Cursor.W_RESIZE.equals(cursorEvent) && !Cursor.E_RESIZE.equals(cursorEvent)) {
                        double minHeight = stage.getMinHeight() > (border * 2) ? stage.getMinHeight() : (border * 2);
                        double maxHeight = stage.getMaxHeight();
                        if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.N_RESIZE.equals(cursorEvent) || Cursor.NE_RESIZE.equals(cursorEvent)) {
                            double newHeight = stage.getHeight() - (mouseEvent.getScreenY() - stage.getY());
                            if (newHeight >= minHeight && newHeight <= maxHeight) {
                                stage.setHeight(newHeight);
                                stage.setY(mouseEvent.getScreenY());
                            } else {
                                newHeight = Math.min(Math.max(newHeight, minHeight), maxHeight);
                                // y1 + h1 = y2 + h2
                                // y1 = y2 + h2 - h1
                                stage.setY(stage.getY() + stage.getHeight() - newHeight);
                                stage.setHeight(newHeight);
                            }
                        } else {
                            stage.setHeight(Math.min(Math.max(mouseEventY + startY, minHeight), maxHeight));
                        }
                    }

                    if (!Cursor.N_RESIZE.equals(cursorEvent) && !Cursor.S_RESIZE.equals(cursorEvent)) {
                        double minWidth = stage.getMinWidth() > (border * 2) ? stage.getMinWidth() : (border * 2);
                        double maxWidth = stage.getMaxWidth();
                        if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.W_RESIZE.equals(cursorEvent) || Cursor.SW_RESIZE.equals(cursorEvent)) {
                            double newWidth = stage.getWidth() - (mouseEvent.getScreenX() - stage.getX());
                            if (newWidth >= minWidth && newWidth <= maxWidth) {
                                stage.setWidth(newWidth);
                                stage.setX(mouseEvent.getScreenX());
                            } else {
                                newWidth = Math.min(Math.max(newWidth, minWidth), maxWidth);
                                // x1 + w1 = x2 + w2
                                // x1 = x2 + w2 - w1
                                stage.setX(stage.getX() + stage.getWidth() - newWidth);
                                stage.setWidth(newWidth);
                            }
                        } else {
                            stage.setWidth(Math.min(Math.max(mouseEventX + startX, minWidth), maxWidth));
                        }
                    }
                }
            }

            if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) {
                startScreenX = mouseEvent.getScreenX();
                startScreenY = mouseEvent.getScreenY();
            } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType)) {
                if (Cursor.DEFAULT.equals(cursorEvent)) {
                    stage.setX(stage.getX() + mouseEvent.getScreenX() - startScreenX);
                    startScreenX = mouseEvent.getScreenX();
                    stage.setY(stage.getY() + mouseEvent.getScreenY() - startScreenY);
                    startScreenY = mouseEvent.getScreenY();
                }
            }
        }
    }
}
Longrange answered 1/8, 2018 at 9:41 Comment(2)
There are a lot of answers to this question, but this one appears to be the most elegantly executed, especially the draggable bit at the end.Laic
I don't quite understand why, but with this code, I am able to move the stage by repeatedly resizing it from either the north or west directions. The south and east directions work as expected. The difference between the directions that work funny and fine is the stage.setY() and the stage.setX()Dicky
I
3

I've looked into some threads where they discuss about it before I tried myself to "solve" it, but in the end there was no Problem.

I created a button in the bottom-right corner, gave the Button an OnDraggedMethod and simply used the mouseevent and set the Height/Width on MousePosition - stage X/Y position.

    double newX = event.getScreenX() - stage.getX() + 13;
    double newY = event.getScreenY() - stage.getY() + 10;
    if (newX % 5 == 0 || newY % 5 == 0) {
        if (newX > 550) {
            stage.setWidth(newX);
        } else {
            stage.setWidth(550);
        }

        if (newY > 200) {
            stage.setHeight(newY);
        } else {
            stage.setHeight(200);
        }
    }

Also I set a minimal Width and Height. And because i think that the laggy resize-animation is annoying i surrounded the statement with the if and just resize the stage every 5th pixel.

Looks much better!

PS: The +13 and +10 is just for the MousePosition. Otherweise the Mouse is on the Corner not on the Button^^.

Iambic answered 17/2, 2015 at 9:8 Comment(0)
C
3

Kotlin Version

Inspired by the many answers here, I wrote my own implementation that has all the necessary features that I could think of:

Features

  • Draggability

    • Window is dragable
    • Customizable interaction areas (top, right, bottom, left)
    • Custom cursor when hovering over a drag area
  • Fullscreen

    • Window can be set into fullscreen
    • Window restores back to its previous size and position
    • Customizable interaction areas (top, right, bottom, left)
  • Resizeable

    • Window can be resized by the window border in any direction (n, ne, e, se, s, se, w, nw)
    • Customizable interaction areas (n, e, w, s)
    • Double clicking on a window border will resize the window and take up all the available space in that direction
    • Custom cursor when hovering over a resize area

Usage

Quick setup:

StageInteractor(this)
    .makeDraggable()
    .makeFullscreenable()
    .makeResizable()

Custom interaction areas:

StageInteractor(this)

    // custom width / height for drag areas (top, right, bottom, left)
    .makeDraggable(50, 20, 50, 20) 

    // custom width / height for double-clickable areas (top, right, bottom, left)
    .makeFullscreenable(50, 20, 50, 20)

    // custom width / height for resize areas (top, right, bottom, left)
    .makeResizable(20, 20, 20, 20)

Code

import javafx.scene.Cursor
import javafx.scene.Scene
import javafx.scene.input.MouseButton
import javafx.scene.input.MouseEvent
import javafx.stage.Screen
import javafx.stage.Stage

class StageInteractor {
    private val stage: Stage
    private val scene: Scene

    private var isDraggable = false
    private var isDragging = false
    private var allowDragging = true
    private var dragMarginTop = 0.0
    private var dragMarginRight = 0.0
    private var dragMarginBottom = 0.0
    private var dragMarginLeft = 0.0

    private var isFullscreenable = false
    private var isFullscreen = false
    private var allowFullscreen = true
    private var fullscreenMarginTop = 0.0
    private var fullscreenMarginRight = 0.0
    private var fullscreenMarginBottom = 0.0
    private var fullscreenMarginLeft = 0.0
    private var stageWidthBeforeFullscreen = 0.0
    private var stageHeightBeforeFullscreen = 0.0
    private var stageXBeforeFullscreen = 0.0
    private var stageYBeforeFullscreen = 0.0

    private var isResizeable = false
    private var isResizing = false
    private var allowResizing = true
    private var resizeDirection: ResizeDirection? = null
    private var resizeMarginTop = 0.0
    private var resizeMarginRight = 0.0
    private var resizeMarginBottom = 0.0
    private var resizeMarginLeft = 0.0


    constructor(stage: Stage) {
        this.stage = stage
        this.scene = stage.scene
    }

    fun makeDraggable(
        marginTop: Double = 50.0,
        marginRight: Double = 0.0,
        marginBottom: Double = 0.0,
        marginLeft: Double = 0.0
    ): StageInteractor {
        dragMarginTop = marginTop
        dragMarginRight = marginRight
        dragMarginBottom = marginBottom
        dragMarginLeft = marginLeft

        if (!isDraggable) {
            isDraggable = true

            var dragStartOffsetX = 0.0
            var dragStartOffsetY = 0.0

            scene.addEventHandler(MouseEvent.MOUSE_MOVED) {
                val isWithinBounds = detectDraggingBounds(it)

                if (isDraggable && allowDragging && isWithinBounds) {
                    scene.cursor = Cursor.OPEN_HAND
                } else {
                    if (scene.cursor == Cursor.OPEN_HAND) {
                        scene.cursor = Cursor.DEFAULT
                    }
                }
            }

            scene.addEventHandler(MouseEvent.MOUSE_PRESSED) {
                dragStartOffsetX = stage.x - it.screenX
                dragStartOffsetY = stage.y - it.screenY
            }

            scene.addEventHandler(MouseEvent.MOUSE_DRAGGED) {
                val isWithinBounds = detectDraggingBounds(it)

                if (isDraggable && allowDragging && isWithinBounds) {
                    isDragging = true
                    scene.cursor = Cursor.CLOSED_HAND
                }

                if (isDragging) {
                    stage.x = it.screenX + dragStartOffsetX
                    stage.y = it.screenY + dragStartOffsetY
                }
            }

            scene.addEventHandler(MouseEvent.MOUSE_RELEASED) {
                if (isDragging) {
                    isDragging = false
                    scene.cursor = Cursor.DEFAULT
                }
            }
        }

        return this
    }

    private fun detectDraggingBounds(event: MouseEvent): Boolean {
        return event.sceneY <= dragMarginTop
                || scene.height - event.sceneY <= dragMarginBottom
                || event.sceneX <= dragMarginLeft
                || scene.width - event.sceneX <= dragMarginRight
    }

    fun makeFullscreenable(
        marginTop: Double = 50.0,
        marginRight: Double = 0.0,
        marginBottom: Double = 0.0,
        marginLeft: Double = 0.0
    ): StageInteractor {
        fullscreenMarginTop = marginTop
        fullscreenMarginRight = marginRight
        fullscreenMarginBottom = marginBottom
        fullscreenMarginLeft = marginLeft

        if (!isFullscreenable) {
            isFullscreenable = true

            scene.addEventHandler(MouseEvent.MOUSE_PRESSED) {
                val isDoubleClick = it.button == MouseButton.PRIMARY && it.clickCount >= 2

                if (isFullscreenable && allowFullscreen && isDoubleClick && detectFullscreenBounds(it)) {
                    if (isFullscreen) {
                        isFullscreen = false
                        allowDragging = true
                        allowResizing = true

                        stage.x = stageXBeforeFullscreen
                        stage.y = stageYBeforeFullscreen
                        stage.width = stageWidthBeforeFullscreen
                        stage.height = stageHeightBeforeFullscreen
                    } else {
                        isFullscreen = true
                        allowDragging = false
                        allowResizing = false
                        stageWidthBeforeFullscreen = stage.width
                        stageHeightBeforeFullscreen = stage.height
                        stageXBeforeFullscreen = stage.x
                        stageYBeforeFullscreen = stage.y

                        val screenBounds = Screen.getPrimary().visualBounds
                        val newWidth = if (stage.maxWidth < screenBounds.width) {
                            stage.maxWidth
                        } else {
                            screenBounds.width
                        }
                        val newHeight = if (stage.maxHeight < screenBounds.height) {
                            stage.maxHeight
                        } else {
                            screenBounds.height
                        }

                        stage.width = newWidth
                        stage.height = newHeight
                        stage.x = screenBounds.minX
                        stage.y = screenBounds.minY
                    }
                }
            }

        }

        return this
    }

    private fun detectFullscreenBounds(event: MouseEvent): Boolean {
        val isWithinBounds = event.sceneY <= fullscreenMarginTop
                || scene.height - event.sceneY <= fullscreenMarginBottom
                || event.sceneX <= fullscreenMarginLeft
                || scene.width - event.sceneX <= fullscreenMarginRight

        val resizeDirection = detectResizeDirection(event)

        return isWithinBounds && resizeDirection == null
    }

    fun makeResizable(
        marginTop: Double = 10.0,
        marginRight: Double = 10.0,
        marginBottom: Double = 10.0,
        marginLeft: Double = 10.0
    ): StageInteractor {
        resizeMarginTop = marginTop
        resizeMarginRight = marginRight
        resizeMarginBottom = marginBottom
        resizeMarginLeft = marginLeft

        if (!isResizeable) {
            isResizeable = true

            scene.addEventHandler(MouseEvent.MOUSE_MOVED) {
                if (isResizeable && allowResizing && !isResizing) {
                    when (detectResizeDirection(it)) {
                        ResizeDirection.NORTH_WEST -> scene.cursor = Cursor.NW_RESIZE
                        ResizeDirection.NORTH_EAST -> scene.cursor = Cursor.NE_RESIZE
                        ResizeDirection.SOUTH_WEST -> scene.cursor = Cursor.SW_RESIZE
                        ResizeDirection.SOUTH_EAST -> scene.cursor = Cursor.SE_RESIZE
                        ResizeDirection.NORTH -> scene.cursor = Cursor.N_RESIZE
                        ResizeDirection.SOUTH -> scene.cursor = Cursor.S_RESIZE
                        ResizeDirection.WEST -> scene.cursor = Cursor.W_RESIZE
                        ResizeDirection.EAST -> scene.cursor = Cursor.E_RESIZE
                        else -> {
                            val cursors = listOf(
                                Cursor.NW_RESIZE,
                                Cursor.NE_RESIZE,
                                Cursor.SW_RESIZE,
                                Cursor.SE_RESIZE,
                                Cursor.N_RESIZE,
                                Cursor.S_RESIZE,
                                Cursor.W_RESIZE,
                                Cursor.E_RESIZE
                            )

                            if (cursors.contains(scene.cursor)) {
                                scene.cursor = Cursor.DEFAULT
                            }
                        }
                    }
                }
            }

            var resizeStartFromSceneX = 0.0
            var resizeStartFromSceneY = 0.0
            var resizeStartFromScreenX = 0.0
            var resizeStartFromScreenY = 0.0
            var resizeStartStageWidth = 0.0
            var resizeStartStageHeight = 0.0

            scene.addEventHandler(MouseEvent.MOUSE_PRESSED) {
                if (isResizeable && allowResizing && !isResizing) {
                    resizeDirection = detectResizeDirection(it)

                    if (resizeDirection != null) {
                        if (it.button == MouseButton.PRIMARY && it.clickCount >= 2) {
                            val screenBounds = Screen.getPrimary().visualBounds

                            if (resizeDirection == ResizeDirection.NORTH || resizeDirection == ResizeDirection.NORTH_WEST || resizeDirection == ResizeDirection.NORTH_EAST) {
                                stage.height = ensureStageHeightIsWithinLimits(
                                    stage.height + stage.y - screenBounds.minY
                                )
                                stage.y = 0.0
                            }

                            if (resizeDirection == ResizeDirection.SOUTH || resizeDirection == ResizeDirection.SOUTH_WEST || resizeDirection == ResizeDirection.SOUTH_EAST) {
                                stage.height = ensureStageHeightIsWithinLimits(
                                    screenBounds.height - stage.y + screenBounds.minY
                                )

                                if (stage.height == screenBounds.height) {
                                    stage.y = 0.0
                                }
                            }

                            if (resizeDirection == ResizeDirection.WEST || resizeDirection == ResizeDirection.NORTH_WEST || resizeDirection == ResizeDirection.SOUTH_WEST) {
                                stage.width = ensureStageWidthIsWithinLimits(
                                    stage.width + stage.x
                                )
                                stage.x = 0.0
                            }

                            if (resizeDirection == ResizeDirection.EAST || resizeDirection == ResizeDirection.NORTH_EAST || resizeDirection == ResizeDirection.SOUTH_EAST) {
                                stage.width = ensureStageWidthIsWithinLimits(
                                    screenBounds.width - stage.x
                                )

                                if (stage.width == screenBounds.width) {
                                    stage.x = 0.0
                                }
                            }
                        } else {
                            isResizing = true
                            isDraggable = false
                            isFullscreenable = false

                            resizeStartFromScreenX = it.screenX
                            resizeStartFromScreenY = it.screenY
                            resizeStartFromSceneX = it.sceneX
                            resizeStartFromSceneY = it.sceneY
                            resizeStartStageWidth = stage.width
                            resizeStartStageHeight = stage.height
                        }
                    }
                }
            }

            scene.addEventHandler(MouseEvent.MOUSE_DRAGGED) {
                if (isResizing) {
                    if (resizeDirection == ResizeDirection.NORTH || resizeDirection == ResizeDirection.NORTH_WEST || resizeDirection == ResizeDirection.NORTH_EAST) {
                        val newHeight = ensureStageHeightIsWithinLimits(
                            resizeStartStageHeight + (resizeStartFromScreenY - it.screenY)
                        )
                        val newY = when (newHeight) {
                            stage.maxHeight, stage.minHeight -> stage.y
                            else -> it.screenY - resizeStartFromSceneY
                        }

                        stage.height = newHeight
                        stage.y = newY
                    }

                    if (resizeDirection == ResizeDirection.SOUTH || resizeDirection == ResizeDirection.SOUTH_WEST || resizeDirection == ResizeDirection.SOUTH_EAST) {
                        val newHeight = ensureStageHeightIsWithinLimits(
                            resizeStartStageHeight + (it.screenY - resizeStartFromScreenY)
                        )

                        stage.height = newHeight
                    }

                    if (resizeDirection == ResizeDirection.WEST || resizeDirection == ResizeDirection.NORTH_WEST || resizeDirection == ResizeDirection.SOUTH_WEST) {
                        val newWidth = ensureStageWidthIsWithinLimits(
                            resizeStartStageWidth + (resizeStartFromScreenX - it.screenX)
                        )
                        val newX = when (newWidth) {
                            stage.maxWidth, stage.minWidth -> stage.x
                            else -> it.screenX - resizeStartFromSceneX
                        }

                        stage.width = newWidth
                        stage.x = newX
                    }

                    if (resizeDirection == ResizeDirection.EAST || resizeDirection == ResizeDirection.NORTH_EAST || resizeDirection == ResizeDirection.SOUTH_EAST) {
                        val newWidth = ensureStageWidthIsWithinLimits(
                            resizeStartStageWidth + (it.screenX - resizeStartFromScreenX)
                        )

                        stage.width = newWidth
                    }
                }
            }

            scene.addEventHandler(MouseEvent.MOUSE_RELEASED) {
                if (isResizing) {
                    isResizing = false
                    isDraggable = true
                    isFullscreenable = true
                }
            }
        }

        return this
    }

    private fun detectResizeDirection(event: MouseEvent): ResizeDirection? {
        val isNorthResize = event.sceneY <= resizeMarginTop
        val isSouthResize = scene.height - event.sceneY <= resizeMarginBottom
        val isWestResize = event.sceneX <= resizeMarginLeft
        val isEastResize = scene.width - event.sceneX <= resizeMarginRight
        val isNorthWestResize = isNorthResize && isWestResize
        val isNorthEastResize = isNorthResize && isEastResize
        val isSouthWestResize = isSouthResize && isWestResize
        val isSouthEastResize = isSouthResize && isEastResize

        return when {
            isNorthWestResize -> ResizeDirection.NORTH_WEST
            isNorthEastResize -> ResizeDirection.NORTH_EAST
            isSouthWestResize -> ResizeDirection.SOUTH_WEST
            isSouthEastResize -> ResizeDirection.SOUTH_EAST
            isNorthResize -> ResizeDirection.NORTH
            isSouthResize -> ResizeDirection.SOUTH
            isWestResize -> ResizeDirection.WEST
            isEastResize -> ResizeDirection.EAST
            else -> null
        }
    }

    private fun ensureStageWidthIsWithinLimits(width: Double): Double {
        val screenBounds = Screen.getPrimary().visualBounds

        return when {
            width > stage.maxWidth -> stage.maxWidth
            width < stage.minWidth -> stage.minWidth
            width > screenBounds.width -> screenBounds.width
            else -> width
        }
    }

    private fun ensureStageHeightIsWithinLimits(height: Double): Double {
        val screenBounds = Screen.getPrimary().visualBounds

        return when {
            height > stage.maxHeight -> stage.maxHeight
            height < stage.minHeight -> stage.minHeight
            height > screenBounds.height -> screenBounds.height
            else -> height
        }
    }

    enum class ResizeDirection {
        NORTH, NORTH_EAST, NORTH_WEST,
        SOUTH, SOUTH_EAST, SOUTH_WEST,
        EAST, WEST;
    }
}
Cognizant answered 10/8, 2018 at 11:34 Comment(0)
C
2

What can I say... I'm a slow learner. Took me a bit to really go through all the code and understand it. Its not hard... just a lot going on. I decided to make some changes to use absolute mouse position as opposed to relative mouse position, I also changed the class overall from using static members and whatnot. Here's what I came up with and works quite well for myself. For those searching for similar solutions, enjoy my copious comments... Using this version of this class is as simple as:

ResizeListener listener = new ResizeListener(stage);

import draco_logger.LeafLogger;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.stage.Screen;
import javafx.stage.Stage;

//originally created by Alexander Berg
//https://mcmap.net/q/487480/-allow-user-to-resize-an-undecorated-stage
//
//modified by Joseph Adomatis
//extracted ResizeListener class and made public, added arg for CWinMaxButton for detecting Maximized Window, using my own logger
//changed MouseDragged routines and private variables to make use of screen absolute values instead of relative values
//MouseDragged also updated to respect Min/Max sizes
public class ResizeListener implements EventHandler<MouseEvent> {
    public ResizeListener(Stage stage) {
        LeafLogger log = new LeafLogger("ResizeListener", "Constructor(Stage)");
        this.stage = stage;
        this._max = null;
        isPressed = false;
        cursorEvent = Cursor.DEFAULT;
        border = 3;
        stageStartH = 0;
        stageStartW = 0;
        stageStartX = 0;
        stageStartY = 0;
        this.addResizeListener();
        log.Wither();
    }
    public void AddMaxButton(CWinMaxButton max){ this._max = max; }
    private void addResizeListener() {
        LeafLogger log = new LeafLogger("ResizeListener", "addResizeListener");
        this.stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, this);
        this.stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, this);
        this.stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, this);
        this.stage.getScene().addEventHandler(MouseEvent.MOUSE_ENTERED, this);
        this.stage.getScene().addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, this);
        this.stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED, this);
        this.stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, this);
        ObservableList<Node> children = this.stage.getScene().getRoot().getChildrenUnmodifiable();
        for (Node child : children) {
            addListenerDeeply(child);
        }
        log.Wither();
    }
    private void addListenerDeeply(Node node) {
        LeafLogger log = new LeafLogger("ResizeListener", "addListenerDeeply");
        node.addEventHandler(MouseEvent.MOUSE_MOVED, this);
        node.addEventHandler(MouseEvent.MOUSE_PRESSED, this);
        node.addEventHandler(MouseEvent.MOUSE_DRAGGED, this);
        node.addEventHandler(MouseEvent.MOUSE_ENTERED, this);
        node.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, this);
        node.addEventHandler(MouseEvent.MOUSE_EXITED, this);
        node.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, this);
        if (node instanceof Parent) {
            Parent parent = (Parent) node;
            ObservableList<Node> children = parent.getChildrenUnmodifiable();
            for (Node child : children) {
                addListenerDeeply(child);
            }
        }
        log.Wither();
    }
    @Override
    public void handle(MouseEvent mouseEvent) {
        LeafLogger log = new LeafLogger("ResizeListener","handle");
        // Check if we registered a maximize button
        if(this._max != null){
            // Check with the maximize button to see if window is currently maximized
            if(this._max.GetMaximized()){
                // We do not resize Maximized windows
                log.Wither();
                return;
            }
        }
        EventType<? extends MouseEvent> mouseEventType = mouseEvent.getEventType();
        Scene scene = stage.getScene();

        // set minHeight vars in such a way as to ensure that there is always a border over which we can continue to resize again
        // if stage MinHeight were 0 and you resized to 0, the draggable zone is gone and you cannot resize anymore
        // so regardless of app preference, we artificially set min sizes to leave all borders
        double minHeight = stage.getMinHeight() > (border*2) ? stage.getMinHeight() : (border*2);
        double minWidth = stage.getMinWidth() > (border*2) ? stage.getMinWidth() : (border*2);

        double maxHeight = stage.getMaxHeight();
        double maxWidth = stage.getMaxWidth();

        // capture the position of the mouse cursor relative to the stage anchor point at the time of the event
        double mouseEventX = mouseEvent.getSceneX();
        double mouseEventY = mouseEvent.getSceneY();

        // capture the current scene Height and Width
        double sceneHeight = scene.getHeight();
        double sceneWidth = scene.getWidth();

        // capture the screen max visual Height and Width
        double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
        double screenWidth = Screen.getPrimary().getVisualBounds().getWidth();

        // if MOUSE_MOVED and its new position is over one of the stage borders, we want to update the cursor to be one of the resize variety
        if (MouseEvent.MOUSE_MOVED.equals(mouseEventType)) {
            if (mouseEventX < border && mouseEventY < border) {
                cursorEvent = Cursor.NW_RESIZE;
            } else if (mouseEventX < border && mouseEventY > sceneHeight - border) {
                cursorEvent = Cursor.SW_RESIZE;
            } else if (mouseEventX > sceneWidth - border && mouseEventY < border) {
                cursorEvent = Cursor.NE_RESIZE;
            } else if (mouseEventX > sceneWidth - border && mouseEventY > sceneHeight - border) {
                cursorEvent = Cursor.SE_RESIZE;
            } else if (mouseEventX < border) {
                cursorEvent = Cursor.W_RESIZE;
            } else if (mouseEventX > sceneWidth - border) {
                cursorEvent = Cursor.E_RESIZE;
            } else if (mouseEventY < border) {
                cursorEvent = Cursor.N_RESIZE;
            } else if (mouseEventY > sceneHeight - border) {
                cursorEvent = Cursor.S_RESIZE;
            } else {
                cursorEvent = Cursor.DEFAULT;
            }
            scene.setCursor(cursorEvent);
        // if MOUSE_EXITED the stage screen area and we'd pressed but did not release the mouse button, then we want to maintain our current cursor
        // otherwise, since the mouse is outside our stage, we return it to the default cursor
        } else if(MouseEvent.MOUSE_EXITED.equals(mouseEventType) || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)){
            if(!isPressed){
                scene.setCursor(Cursor.DEFAULT);
            }
        // similarly, if MOUSE ENTERED the stage screen area and we'd pressed but did not release the mouse button, then we want to maintain the current cursor
        // otherwise, since the mouse is coming back to us, we dont want to keep whatever other cursor may have been set by other windows so we return to default
        } else if(MouseEvent.MOUSE_ENTERED.equals(mouseEventType) || MouseEvent.MOUSE_ENTERED_TARGET.equals(mouseEventType)){
            if(!isPressed){
                scene.setCursor(Cursor.DEFAULT);
            }
        // if MOUSE_PRESSED we might need to keep track that we pressed it and are initiating a potential drag/resize event
        } else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) {
            // right now we dont care if mouse was pressed, but we might
            boolean iCare = false;
            // check the cursor type, if it is a resize cursor then mouse is over a border and we DO care that we pressed the mouse
            if(Cursor.N_RESIZE.equals(cursorEvent) || Cursor.S_RESIZE.equals(cursorEvent) || Cursor.E_RESIZE.equals(cursorEvent) || Cursor.W_RESIZE.equals(cursorEvent)){
                iCare = true;
            } else if(Cursor.NE_RESIZE.equals(cursorEvent) || Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.SE_RESIZE.equals(cursorEvent) || Cursor.SW_RESIZE.equals(cursorEvent)){
                iCare = true;
            }
            // if we care that we pressed the mouse, we need to capture the initial data that will be used by our drag event handler to actually resize the window
            if(iCare){
                stageStartH = stage.getHeight();
                stageStartW = stage.getWidth();
                stageStartX = stage.getX();
                stageStartY = stage.getY();
                mouseStartX = mouseEvent.getScreenX();
                mouseStartY = mouseEvent.getScreenY();
                isPressed = true;
            }
        // if MOUSE_RELEASED, we don't care what the mouse does anymore so release our flag
        } else if(MouseEvent.MOUSE_RELEASED.equals(mouseEventType)){
            isPressed = false;
        // if MOUSE_DRAGGED, this handler might have something to do
        } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType)) {
            // if the cursor is still default, then this handler doesnt care about the drag event so ignore everything else
            // this handler only cares if the cursor is of the resize variety
            if(Cursor.DEFAULT.equals(cursorEvent)){
                return;
            }
            // Check if there is a vertical component to the window resize
            // The only time there isn't a vertical component is if the mouse is strictly on the west or east side of the stage
            if (!Cursor.W_RESIZE.equals(cursorEvent) && !Cursor.E_RESIZE.equals(cursorEvent)) {
                // There is a vertical component.
                // If we are resizing the north side however, we will be resetting both the Y coordinate of the stage anchor as well as the stage height
                if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.N_RESIZE.equals(cursorEvent) || Cursor.NE_RESIZE.equals(cursorEvent)) {
                    double mouseDifY = mouseStartY - stageStartY;
                    // we are moving the north side
                    // figure out where the south side of the stage is
                    double finalY = stageStartY + stage.getHeight();
                    // we are free to move the north side until it reaches the point where the distance to the south side is greater than maxHeight
                    // OR, we run into the top of the screen
                    double minStageY = (finalY - maxHeight) > 0 ? (finalY - maxHeight): 0;
                    double minMouseY = minStageY + mouseDifY;
                    // we are free to move the north side until it reaches the point where the distance to the south side is less than minHeight
                    double maxStageY = finalY - minHeight;
                    double maxMouseY = maxStageY + mouseDifY;
                    // capture the absolute position of the mouse at the time of the event
                    double curMouseY = mouseEvent.getScreenY();
                    if(curMouseY < minMouseY){
                        stage.setY(minStageY);
                        // Our mouse passed the value at which we would breach max height
                        // We dont want the curMouseY to update any more until the mouse is back over the border.
                        // Otherwise, the window border would resize relative to mouse movement, not relative to absolute mouse position
                        curMouseY = minMouseY;
                    } else if(curMouseY > maxMouseY){
                        stage.setY(maxStageY);
                        // Our mouse passed the value at which we would breach min height
                        // We dont want the curMouseY to update any more until the mouse is back over the border.
                        // Otherwise, the window border would resize relative to mouse movement, not relative to absolute mouse position
                        curMouseY = maxMouseY;
                    } else {
                        stage.setY(curMouseY - mouseDifY);
                    }
                    double newY = stage.getY();
                    double newHeight = finalY - newY;
                    stage.setHeight(newHeight);
                    // Our stage and mouse start variables were set via the mouse pressed event handle
                    // If we did above procedure in the mouse released event handle, it would work, but there would be no display update till mouse released.
                    // By using mouse dragged event handle, we get display update each event cycle... but we have to constantly update our start variables for the next cycle
                    // While dragging mouse, you aren't releasing and re-pressing it to update the variables....
                    stageStartY = stage.getY();
                    stageStartH = stage.getHeight();
                    mouseStartY = curMouseY;
                } else {
                    // Else, we are resizing the south side, and the Y coordinate remains fixed. We only change the stage height
                    // figure out where the current south side actually is
                    double curFinalY = stageStartY + stageStartH;
                    double mouseDifY = mouseStartY - curFinalY;
                    // we are free to move the north side until it reaches the point where the distance to the south side is greater than maxHeight
                    // OR, we run into the bottom of the screen
                    double maxFinalY = (stageStartY + maxHeight) < screenHeight ? (stageStartY + maxHeight) : screenHeight;
                    double maxMouseY = maxFinalY + mouseDifY;
                    // we are free to move the south side until the point where the distance from anchor to south side is less than minHeight
                    double minFinalY = stageStartY + minHeight;
                    double minMouseY = minFinalY + mouseDifY;
                    // capture the absolute position of the mouse at the time of the event
                    double curMouseY = mouseEvent.getScreenY();
                    if (curMouseY < minMouseY) {
                        stage.setHeight(minHeight);
                        // Our mouse passed the value at which we would breach min height
                        // We don't want the curMouseY to update any more until the mouse is back over the border.
                        // Otherwise, the window border would resize relative to mouse movement, not relative to absolute mouse position
                        curMouseY = minMouseY;
                    } else if(curMouseY > maxMouseY){
                        double newFinalY = maxMouseY - mouseDifY;
                        double newHeight = newFinalY - stageStartY;
                        stage.setHeight(newHeight);
                        // Our mouse passed the value at which we would breach max height
                        // We don't want the curMouseY to update any more until the mouse is back over the border.
                        // Otherwise, the window border would resize relative to mouse movement, not relative to absolute mouse position
                        curMouseY = maxMouseY;
                    } else {
                        double newFinalY = curMouseY - mouseDifY;
                        double newHeight = newFinalY - stageStartY;
                        stage.setHeight(newHeight);
                    }
                    // Our stage and mouse start variables were set via the mouse pressed event handle
                    // If we did above procedure in the mouse released event handle, it would work, but there would be no display update till mouse released.
                    // By using mouse dragged event handle, we get display update each event cycle... but we have to constantly update our start variables for the next cycle
                    // While dragging mouse, you aren't releasing and re-pressing it to update the variables....
                    stageStartY = stage.getY();
                    stageStartH = stage.getHeight();
                    mouseStartY = curMouseY;
                }
            }
            // Check if there is a horizontal component to the window resize
            // The only time there isn't a horizontal component is if the mouse is strictly on the north or south side of the stage.
            if (!Cursor.N_RESIZE.equals(cursorEvent) && !Cursor.S_RESIZE.equals(cursorEvent)) {
                // There is a horizontal component.
                // If we are resizing the west side however, we will be resetting both the X coordinate of the stage anchor as well as the stage width.
                if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.W_RESIZE.equals(cursorEvent) || Cursor.SW_RESIZE.equals(cursorEvent)) {
                    // we are moving the west side
                    // figure out where the east side of the stage is
                    double mouseDifX = mouseStartX - stageStartX;
                    double finalX = stageStartX + stageStartW;
                    // we are free to move the west side until it reaches the point where the distance to the east side is greater than maxWidth
                    // OR, we run into the left of the screen
                    double minStageX = (finalX - maxHeight) > 0 ? (finalX - maxHeight): 0;
                    double minMouseX = minStageX + mouseDifX;
                    // we are free to move the west side until it reaches the point where the distance to the east side is less than minWidth
                    double maxStageX = finalX - minWidth;
                    double maxMouseX = maxStageX + mouseDifX;
                    // capture the absolute position of the mouse at the time of the event
                    double curMouseX = mouseEvent.getScreenX();
                    if(curMouseX < minMouseX){
                        stage.setX(minStageX);
                        // Our mouse passed the value at which we would breach max width
                        // We don't want the curMouseX to update any more until the mouse is back over the border.
                        // Otherwise, the window border would resize relative to mouse movement, not relative to absolute mouse position
                        curMouseX = minMouseX;
                    } else if(curMouseX > maxMouseX){
                        stage.setX(maxStageX);
                        curMouseX = maxMouseX;
                        // Our mouse passed the value at which we would breach min width
                        // We don't want the curMouseX to update any more until the mouse is back over the border.
                        // Otherwise, the window border would resize relative to mouse movement, not relative to absolute mouse position
                    } else {
                        stage.setX(curMouseX - mouseDifX);
                    }
                    double newX = stage.getX();
                    double newWidth = finalX - newX;
                    stage.setWidth(newWidth);
                    // Our stage and mouse start variables were set via the mouse pressed event handle
                    // If we did above procedure in the mouse released event handle, it would work, but there would be no display update till mouse released.
                    // By using mouse dragged event handle, we get display update each event cycle... but we have to constantly update our start variables for the next cycle
                    // While dragging mouse, you aren't releasing and re-pressing it to update the variables....
                    stageStartX = stage.getX();
                    stageStartW = stage.getWidth();
                    mouseStartX = curMouseX;
                } else {
                    // Else, we are resizing the east side, and the X coordinate remains fixed. We only change the stage width.
                    // figure out where the current east side actually is
                    double curFinalX = stageStartX + stageStartW;
                    double mouseDifX = mouseStartX - curFinalX;
                    // we are free to move the east side until the point where the distance from anchor to east side is less than minWidth
                    double minFinalX = stageStartX + minWidth;
                    double minMouseX = minFinalX + mouseDifX;
                    // we are free to move the east side until it reaches the point where the distance to the west side is greater than maxWidth
                    // OR, we run into the right of the screen
                    double maxFinalX = (stageStartX + maxWidth) < screenWidth ? (stageStartX + maxWidth) : screenWidth;
                    double maxMouseX = maxFinalX + mouseDifX;
                    // capture the absolute position of the mouse at the time of the event
                    double curMouseX = mouseEvent.getScreenX();
                    if (curMouseX < minMouseX) {
                        stage.setWidth(minWidth);
                        curMouseX = minMouseX;
                        // Our mouse passed the value at which we would breach min width
                        // We don't want the curMouseX to update any more until the mouse is back over the border.
                        // Otherwise, the window border would resize relative to mouse movement, not relative to absolute mouse position
                    } else if(curMouseX > maxMouseX){
                            double newFinalX = maxMouseX - mouseDifX;
                            double newWidth = newFinalX - stageStartX;
                            stage.setWidth(newWidth);
                            // Our mouse passed the value at which we would breach max width
                            // We don't want the curMouseY to update any more until the mouse is back over the border.
                            // Otherwise, the window border would resize relative to mouse movement, not relative to absolute mouse position
                            curMouseX = maxMouseX;
                    } else {
                        double newFinalX = curMouseX - mouseDifX;
                        double newWidth = newFinalX - stageStartX;
                        stage.setWidth(newWidth);
                    }
                    // Our stage and mouse start variables were set via the mouse pressed event handle
                    // If we did above procedure in the mouse released event handle, it would work, but there would be no display update till mouse released.
                    // By using mouse dragged event handle, we get display update each event cycle... but we have to constantly update our start variables for the next cycle
                    // While dragging mouse, you aren't releasing and re-pressing it to update the variables....
                    stageStartX = stage.getX();
                    stageStartW = stage.getWidth();
                    mouseStartX = curMouseX;
                }
            }
        }
        log.Wither();
    }
// <editor-fold defaultstate="collapsed" desc="***** Private Variable Declarations *****">
private boolean isPressed;
private Cursor cursorEvent;
private CWinMaxButton _max;
private double mouseStartX;
private double mouseStartY;
private double stageStartH;
private double stageStartW;
private double stageStartX;
private double stageStartY;
private final int border;
private final Stage stage;
// </editor-fold>
}
Cytolysis answered 12/2, 2018 at 6:44 Comment(0)
T
2

Here is an updated version of ResizeHelper posted by @Alexander.Berg, which supports window drag:

import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

//created by Alexander Berg
public class ResizeHelper {
    private static double xOffset = 0;
    private static double yOffset = 0;

    public static void addResizeListener(Stage stage) {
        ResizeListener resizeListener = new ResizeListener(stage);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED, resizeListener);
        stage.getScene().addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, resizeListener);

        ObservableList<Node> children = stage.getScene().getRoot().getChildrenUnmodifiable();
        for (Node child : children) {
            addListenerDeeply(child, resizeListener);
        }
    }

    public static void addListenerDeeply(Node node, EventHandler<MouseEvent> listener) {
        node.addEventHandler(MouseEvent.MOUSE_MOVED, listener);
        node.addEventHandler(MouseEvent.MOUSE_PRESSED, listener);
        node.addEventHandler(MouseEvent.MOUSE_DRAGGED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED, listener);
        node.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, listener);
        if (node instanceof Parent) {
            Parent parent = (Parent) node;
            ObservableList<Node> children = parent.getChildrenUnmodifiable();
            for (Node child : children) {
                addListenerDeeply(child, listener);
            }
        }
    }

    static class ResizeListener implements EventHandler<MouseEvent> {
        private Stage stage;
        private Cursor cursorEvent = Cursor.DEFAULT;
        private int border = 4;
        private double startX = 0;
        private double startY = 0;

        public ResizeListener(Stage stage) {
            this.stage = stage;
        }

        @Override
        public void handle(MouseEvent mouseEvent) {
            EventType<? extends MouseEvent> mouseEventType = mouseEvent.getEventType();
            Scene scene = stage.getScene();

            double mouseEventX = mouseEvent.getSceneX(), mouseEventY = mouseEvent.getSceneY(), sceneWidth = scene.getWidth(), sceneHeight = scene.getHeight();

            if (MouseEvent.MOUSE_MOVED.equals(mouseEventType) == true) {
                if (mouseEventX < border && mouseEventY < border) {
                    cursorEvent = Cursor.NW_RESIZE;
                } else if (mouseEventX < border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SW_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY < border) {
                    cursorEvent = Cursor.NE_RESIZE;
                } else if (mouseEventX > sceneWidth - border && mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.SE_RESIZE;
                } else if (mouseEventX < border) {
                    cursorEvent = Cursor.W_RESIZE;
                } else if (mouseEventX > sceneWidth - border) {
                    cursorEvent = Cursor.E_RESIZE;
                } else if (mouseEventY < border) {
                    cursorEvent = Cursor.N_RESIZE;
                } else if (mouseEventY > sceneHeight - border) {
                    cursorEvent = Cursor.S_RESIZE;
                } else {
                    cursorEvent = Cursor.DEFAULT;
                }
                scene.setCursor(cursorEvent);
            } else if (MouseEvent.MOUSE_EXITED.equals(mouseEventType) || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)) {
                scene.setCursor(Cursor.DEFAULT);
            } else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType) == true) {
                startX = stage.getWidth() - mouseEventX;
                startY = stage.getHeight() - mouseEventY;
                xOffset = mouseEvent.getSceneX();
                yOffset = mouseEvent.getSceneY();

            } else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType) == true) {
                if (Cursor.DEFAULT.equals(cursorEvent) == false) {
                    if (Cursor.W_RESIZE.equals(cursorEvent) == false && Cursor.E_RESIZE.equals(cursorEvent) == false) {
                        double minHeight = stage.getMinHeight() > (border * 2) ? stage.getMinHeight() : (border * 2);
                        if (Cursor.NW_RESIZE.equals(cursorEvent) == true || Cursor.N_RESIZE.equals(cursorEvent) == true || Cursor.NE_RESIZE.equals(cursorEvent) == true) {
                            if (stage.getHeight() > minHeight || mouseEventY < 0) {
                                stage.setHeight(stage.getY() - mouseEvent.getScreenY() + stage.getHeight());
                                stage.setY(mouseEvent.getScreenY());
                            }
                        } else {
                            if (stage.getHeight() > minHeight || mouseEventY + startY - stage.getHeight() > 0) {
                                stage.setHeight(mouseEventY + startY);
                            }
                        }
                    }

                    if (Cursor.N_RESIZE.equals(cursorEvent) == false && Cursor.S_RESIZE.equals(cursorEvent) == false) {
                        double minWidth = stage.getMinWidth() > (border * 2) ? stage.getMinWidth() : (border * 2);
                        if (Cursor.NW_RESIZE.equals(cursorEvent) == true || Cursor.W_RESIZE.equals(cursorEvent) == true || Cursor.SW_RESIZE.equals(cursorEvent) == true) {
                            if (stage.getWidth() > minWidth || mouseEventX < 0) {
                                stage.setWidth(stage.getX() - mouseEvent.getScreenX() + stage.getWidth());
                                stage.setX(mouseEvent.getScreenX());
                            }
                        } else {
                            if (stage.getWidth() > minWidth || mouseEventX + startX - stage.getWidth() > 0) {
                                stage.setWidth(mouseEventX + startX);
                            }
                        }
                    }
                } else if (mouseEvent.getSceneY() < 70) {
                    stage.setX(mouseEvent.getScreenX() - xOffset);
                    stage.setY(mouseEvent.getScreenY() - yOffset);
                }
            }
        }
    }
}
Thorpe answered 6/5, 2018 at 11:5 Comment(0)
C
1

Thanks @Alexander.Berg for providing this utility class (1st version). It helped me to get the desired functionality for my Undecorated Stage.

However, I have few queries regarding some points:

  1. What is need to traverse through all the child nodes and set the handler to each and every parent node. Is is not sufficient to put the handler on Scene only?
  2. If we are anyway checking for the EventType in handler, what is need in setting the same handler to different event types. Can it be set on one super mouse event (MouseEvent.ANY) and can skip the rest.
  3. If point#1 is handled, then there is no need to consider MOUSE_EXITED and MOUSE_EXITED_TARGET to set the default cursor back.
  4. There is every possible chance to trigger the event on underlying node(s) in the drag space(border space) if the handler is added as event handler. The most common case can be if the custom stage close button is very near to corner which comes under the border space. Trying to resize at that space will eventually trigger the close button. Should it need to be implement by adding it as filter (and consume the event when neccessary) rather than handler?

I tried to modify your code to address all the above queries. Please find below the updated ResizeHelper. I am only trying to simplify the things a bit further. Once again thanks for providing the code and letting me to think with a starting point.

import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

/**
 * Helper class to set the resizing implementation for any given undecorated stage.
 */
public class ResizeHelper {

    /**
     * Handler to process the resizing of the the given stage.
     */
    static class ResizeHandler implements EventHandler<MouseEvent> {

        /** Space to consider around the stage border for resizing */
        private static final int BORDER = 10;

        /** Stage to which the handler is implemented */
        private final Stage stage;

        /** Current cursor reference to the scene */
        private Cursor cursor = Cursor.DEFAULT;

        /** X position of the drag start */
        private double startX = 0;

        /** Y position of the drag start */
        private double startY = 0;

        /**
         * Constructor.
         *
         * @param stageTmp Stage to which resizing to be set.
         */
        public ResizeHandler(final Stage stageTmp) {
            stage = stageTmp;
        }

        @Override
        public void handle(final MouseEvent event) {
            final EventType<? extends MouseEvent> eventType = event.getEventType();
            final Scene scene = stage.getScene();
            final double mouseEventX = event.getSceneX();
            final double mouseEventY = event.getSceneY();
            final double sceneWidth = scene.getWidth();
            final double sceneHeight = scene.getHeight();

            if (MouseEvent.MOUSE_MOVED.equals(eventType)) {
                setCursor(mouseEventX, mouseEventY, sceneWidth, sceneHeight);
                scene.setCursor(cursor);

            } else if (MouseEvent.MOUSE_PRESSED.equals(eventType)) {
                startX = stage.getWidth() - mouseEventX;
                startY = stage.getHeight() - mouseEventY;
                consumeEventIfNotDefaultCursor(event);

            } else if (MouseEvent.MOUSE_DRAGGED.equals(eventType) && !Cursor.DEFAULT.equals(cursor)) {
                consumeEventIfNotDefaultCursor(event);
                if (!Cursor.W_RESIZE.equals(cursor) && !Cursor.E_RESIZE.equals(cursor)) {
                    processVerticalDrag(event);
                }

                if (!Cursor.N_RESIZE.equals(cursor) && !Cursor.S_RESIZE.equals(cursor)) {
                    processHorizontalDrag(event);
                }
            }
        }

        /**
         * Consumes the mouse event if the cursor is not the DEFAULT cursor.
         *
         * @param event MouseEvent instance.
         */
        private void consumeEventIfNotDefaultCursor(final MouseEvent event) {
            if (!cursor.equals(Cursor.DEFAULT)) {
                event.consume();
            }
        }

        /**
         * Processes the horizontal drag movement and resizes the window width.
         *
         * @param event MouseEvent instance.
         */
        private void processHorizontalDrag(final MouseEvent event) {
            final double minWidth =
                    stage.getMinWidth() > BORDER * 2 ? stage.getMinWidth() : BORDER * 2;
            final double mouseEventX = event.getSceneX();
            if (Cursor.NW_RESIZE.equals(cursor)
                || Cursor.W_RESIZE.equals(cursor)
                || Cursor.SW_RESIZE.equals(cursor)) {
                if (stage.getWidth() > minWidth || mouseEventX < 0) {
                    stage.setWidth(stage.getX() - event.getScreenX() + stage.getWidth());
                    stage.setX(event.getScreenX());
                }
            } else if (stage.getWidth() > minWidth || mouseEventX + startX - stage.getWidth() > 0) {
                stage.setWidth(mouseEventX + startX);
            }
        }

        /**
         * Processes the vertical drag movement and resizes the window height.
         *
         * @param event MouseEvent instance.
         */
        private void processVerticalDrag(final MouseEvent event) {
            final double minHeight =
                    stage.getMinHeight() > BORDER * 2 ? stage.getMinHeight() : BORDER * 2;
            final double mouseEventY = event.getSceneY();
            if (Cursor.NW_RESIZE.equals(cursor)
                || Cursor.N_RESIZE.equals(cursor)
                || Cursor.NE_RESIZE.equals(cursor)) {
                if (stage.getHeight() > minHeight || mouseEventY < 0) {
                    stage.setHeight(stage.getY() - event.getScreenY() + stage.getHeight());
                    stage.setY(event.getScreenY());
                }
            } else if (stage.getHeight() > minHeight || mouseEventY + startY - stage.getHeight() > 0) {
                stage.setHeight(mouseEventY + startY);
            }
        }

        /**
         * Determines and sets the appropriate cursor based on the mouse position in relative to scene bounds.
         *
         * @param mouseEventX X position of mouse in the scene.
         * @param mouseEventY Y position of mouse in the scene.
         * @param sceneWidth Width of the scene.
         * @param sceneHeight Height of the scene.
         */
        private void setCursor(final double mouseEventX, final double mouseEventY, final double sceneWidth,
                final double sceneHeight) {
            final Cursor cursor1;
            if (mouseEventX < BORDER && mouseEventY < BORDER) {
                cursor1 = Cursor.NW_RESIZE;
            } else if (mouseEventX < BORDER && mouseEventY > sceneHeight - BORDER) {
                cursor1 = Cursor.SW_RESIZE;
            } else if (mouseEventX > sceneWidth - BORDER && mouseEventY < BORDER) {
                cursor1 = Cursor.NE_RESIZE;
            } else if (mouseEventX > sceneWidth - BORDER && mouseEventY > sceneHeight - BORDER) {
                cursor1 = Cursor.SE_RESIZE;
            } else if (mouseEventX < BORDER) {
                cursor1 = Cursor.W_RESIZE;
            } else if (mouseEventX > sceneWidth - BORDER) {
                cursor1 = Cursor.E_RESIZE;
            } else if (mouseEventY < BORDER) {
                cursor1 = Cursor.N_RESIZE;
            } else if (mouseEventY > sceneHeight - BORDER) {
                cursor1 = Cursor.S_RESIZE;
            } else {
                cursor1 = Cursor.DEFAULT;
            }
            cursor = cursor1;
        }
    }

    /**
     * Constructor.
     *
     */
    private ResizeHelper() {

    }

    /**
     * Adds the resize handler to the provided stage.
     *
     * @param stage Stage to which the resizing should be implemented.
     */
    public static void addResizeHandler(final Stage stage) {
        ResizeHandler resizeHandler = new ResizeHandler(stage);
        stage.setMinHeight(stage.getHeight());
        stage.setMinWidth(stage.getWidth());
        stage.setMaxHeight(stage.getMaxHeight());
        stage.setMaxWidth(stage.getMaxWidth());
        stage.getScene().addEventFilter(MouseEvent.ANY, resizeHandler);
    }
}
Croix answered 25/7, 2018 at 5:11 Comment(1)
This one, might be little optimized and avoided possible memory leaks, but it's not behaving well to me... I think there are unnecessary statements in method addResizeHandler, after i removed 'em it's working well i think.Reenter
C
1

I made the original solution also support padding in the root container of the stage. Otherwise the cursor wouldn't be set properly and the resizing also. The resizing also works better by taking into account the mouse position inside the stage when starting to drag, to avoid "jumps" in the repositioning of the Stage.

package apro2.canbustool.ui.util;

import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

//created by Alexander Berg 
public class ResizeHelper {

public static ResizeListener addResizeListener (Stage stage) {
    ResizeListener resizeListener = new ResizeListener(stage);
    Scene scene = stage.getScene();
    scene.addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener);
    scene.addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener);
    scene.addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener);
    scene.addEventHandler(MouseEvent.MOUSE_EXITED, resizeListener);
    scene.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, resizeListener);

    return resizeListener;
}


public static class ResizeListener implements EventHandler<MouseEvent> {
    private Stage stage;
    private Scene scene;
    private Cursor cursorEvent = Cursor.DEFAULT;
    private int border = 4;
    private double startX = 0;
    private double startY = 0;
    private double sceneOffsetX = 0;
    private double sceneOffsetY = 0;
    private double padTop = 0;
    private double padRight = 0;
    private double padBottom = 0;
    private double padLeft = 0;


    public ResizeListener (Stage stage) {
        this.stage = stage;
        this.scene = stage.getScene();
    }


    public void setPadding (Insets padding) {
        padTop = padding.getTop();
        padRight = padding.getRight();
        padBottom = padding.getBottom();
        padLeft = padding.getLeft();
    }


    @Override
    public void handle(MouseEvent mouseEvent) {
        EventType<? extends MouseEvent> mouseEventType = mouseEvent.getEventType();

        double mouseEventX = mouseEvent.getSceneX(),
                mouseEventY = mouseEvent.getSceneY(),
                viewWidth = stage.getWidth() - padLeft - padRight,
                viewHeight = stage.getHeight() - padTop - padBottom;

        if (MouseEvent.MOUSE_MOVED.equals(mouseEventType)) {
            if (mouseEventX < border + padLeft && mouseEventY < border + padTop) {
                cursorEvent = Cursor.NW_RESIZE;
            }
            else if (mouseEventX < border + padLeft && mouseEventY > viewHeight - border + padTop) {
                cursorEvent = Cursor.SW_RESIZE;
            }
            else if (mouseEventX > viewWidth - border + padLeft && mouseEventY < border + padTop) {
                cursorEvent = Cursor.NE_RESIZE;
            }
            else if (mouseEventX > viewWidth - border + padLeft && mouseEventY > viewHeight - border + padTop) {
                cursorEvent = Cursor.SE_RESIZE;
            }
            else if (mouseEventX < border + padLeft) {
                cursorEvent = Cursor.W_RESIZE;
            }
            else if (mouseEventX > viewWidth - border + padLeft) {
                cursorEvent = Cursor.E_RESIZE;
            }
            else if (mouseEventY < border + padTop) {
                cursorEvent = Cursor.N_RESIZE;
            }
            else if (mouseEventY > viewHeight - border + padTop) {
                cursorEvent = Cursor.S_RESIZE;
            }
            else {
                cursorEvent = Cursor.DEFAULT;
            }

            scene.setCursor(cursorEvent);
        }
        else if (MouseEvent.MOUSE_EXITED.equals(mouseEventType) || MouseEvent.MOUSE_EXITED_TARGET.equals(mouseEventType)){
            scene.setCursor(Cursor.DEFAULT);
        }
        else if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) {
            startX = viewWidth - mouseEventX;
            startY = viewHeight - mouseEventY;
            sceneOffsetX = mouseEvent.getSceneX();
            sceneOffsetY = mouseEvent.getSceneY();
        }
        else if (MouseEvent.MOUSE_DRAGGED.equals(mouseEventType) && !Cursor.DEFAULT.equals(cursorEvent)) {
            if (!Cursor.W_RESIZE.equals(cursorEvent) && !Cursor.E_RESIZE.equals(cursorEvent)) {
                double minHeight = stage.getMinHeight() > (border*2) ? stage.getMinHeight() : (border*2);

                if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.N_RESIZE.equals(cursorEvent) || Cursor.NE_RESIZE.equals(cursorEvent)) {
                    if (stage.getHeight() > minHeight || mouseEventY < 0) {
                        double height = stage.getY() - mouseEvent.getScreenY() + stage.getHeight() + sceneOffsetY;
                        double y = mouseEvent.getScreenY() - sceneOffsetY;

                        stage.setHeight(height);
                        stage.setY(y);
                    }
                } else {
                    if (stage.getHeight() > minHeight || mouseEventY + startY - stage.getHeight() > 0) {
                        stage.setHeight(mouseEventY + startY + padBottom + padTop);
                    }
                }
            }

            if (!Cursor.N_RESIZE.equals(cursorEvent) && !Cursor.S_RESIZE.equals(cursorEvent)) {
                double minWidth = stage.getMinWidth() > (border*2) ? stage.getMinWidth() : (border*2);
                if (Cursor.NW_RESIZE.equals(cursorEvent) || Cursor.W_RESIZE.equals(cursorEvent) || Cursor.SW_RESIZE.equals(cursorEvent)) {
                    if (stage.getWidth() > minWidth || mouseEventX < 0) {
                        double width = stage.getX() - mouseEvent.getScreenX() + stage.getWidth() + sceneOffsetX;
                        double x = mouseEvent.getScreenX() - sceneOffsetX;

                        stage.setWidth(width);
                        stage.setX(x);
                    }
                } else {
                    if (stage.getWidth() > minWidth || mouseEventX + startX - stage.getWidth() > 0) {
                        stage.setWidth(mouseEventX + startX + padLeft + padRight);
                    }
                }
            }
        }
    }
}

}

Charteris answered 11/12, 2019 at 9:52 Comment(0)
C
1

Supports:

  1. Stage Resizing, with limitation at min and max bounds
  2. Stage Repositioning, with limitation at screen border
  3. Double Click, one side streching
  4. The cursor is set automatically by the javafx system, therefore less prone to graphical cursor bugs
  5. indentation support which can be used for CSS drop shadow
  6. clean, short, fast, easy to read, adaptable sourcecode with no complex nested queries therefore a smart clipping method
  7. no need to register the Handler deeply, nodes inherit the cursor from root and no uneccessary usage of MouseEvent.MOUSE_MOVED

Build concept:

Use Alignment.TOP|RIGHT|BOTTOM|LEFT and Alignment.TOP_LEFT|BOTTOM_LEFT for corners to place the Border Resize Panes inside the root StackPane

Gluon GUI Designer FXML_Structure:

/** Copyright © 2021 Izon Company, Free To Share: class ResizeHelper.java */

import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.stage.Screen;
import javafx.stage.Stage;

/**
 * handles Stage resizing for StageStyle.UNDECORATED and deals with a
 * indentation which can be used to render a CSS drop shadow effect around the
 * scene with transparent background.
 * 
 * @author Henryk Zschuppan,
 * @date MARCH 01,21
 */

public class ResizeHandler implements EventHandler<MouseEvent> {

    public static ResizeHandler install(Stage stage, double titlebarHeight, double pullEdgeDepth, double indentation) {
        ResizeHandler handler = new ResizeHandler(stage, titlebarHeight, pullEdgeDepth, indentation);
        stage.getScene().addEventHandler(MouseEvent.ANY, handler);
        return handler;
    }

    public static Rectangle2D SCREEN_BOUNDS = Screen.getPrimary().getVisualBounds();

    /** select the boundary clipping orientation in relation to the stage */
    private static enum CHECK {
        LOW,
        HIGH,
        NONE;
    }

    /** Stage to which the handler is implemented */
    final private Stage stage;
    /** Area from top to consider for stage reposition */
    double titlebarHeight;
    /** Space to consider around the stage border for resizing */
    final private int depth;
    /** padding space to render in the CSS effect drop shadow */
    final private double pad;
    /** stage size limits */
    final private double minWidth, minHeight, maxWidth, maxHeight;
    /** start point of mouse position on screen */
    private Point2D startDrag = null;
    /** frame rectangle of the stage on drag start */
    private Rectangle2D startRectangle;
    /** the relative mouse orientation to the stage */
    private CHECK checkX = CHECK.NONE, checkY = CHECK.NONE;

    private boolean inRepositioningArea = false;

    private ResizeHandler(Stage stage, double titlebarHeight, double pullEdgeDepth, double indentation) {
        this.stage = stage;
        this.titlebarHeight = titlebarHeight;
        pad = indentation;
        depth = (int) (indentation + pullEdgeDepth);

        minWidth = stage.getMinWidth();
        minHeight = stage.getMinHeight();
        maxWidth = stage.getMaxWidth();
        maxHeight = stage.getMaxHeight();
    }

    @Override
    public void handle(MouseEvent mouseEvent) {
        if (!mouseEvent.getButton().equals(MouseButton.PRIMARY))
            return;

        EventType<? extends MouseEvent> mouseEventType = mouseEvent.getEventType();
        final double mX = mouseEvent.getScreenX();
        final double mY = mouseEvent.getScreenY();
        /* local coordinates inside stage */
        final double lX = mouseEvent.getSceneX();
        final double lY = mouseEvent.getSceneY();
        final double sW = stage.getWidth();
        final double sH = stage.getHeight();

        if (MouseEvent.MOUSE_PRESSED.equals(mouseEventType)) {
            if (lX < depth && lY < depth) {
                setXYCheck(CHECK.LOW, CHECK.LOW);
            } else if (lX < depth && lY > sH - depth) {
                setXYCheck(CHECK.LOW, CHECK.HIGH);
            } else if (lX > sW - depth && lY < depth) {
                setXYCheck(CHECK.HIGH, CHECK.LOW);
            } else if (lX > sW - depth && lY > sH - depth) {
                setXYCheck(CHECK.HIGH, CHECK.HIGH);
            } else if (lX < depth) {
                setXYCheck(CHECK.LOW, CHECK.NONE);
            } else if (lX > sW - depth) {
                setXYCheck(CHECK.HIGH, CHECK.NONE);
            } else if (lY < depth) {
                setXYCheck(CHECK.NONE, CHECK.LOW);
            } else if (lY > sH - depth) {
                setXYCheck(CHECK.NONE, CHECK.HIGH);
            } else {
                setXYCheck(CHECK.NONE, CHECK.NONE);
            }

            /* check mouse is not inside the resize border space */
            if (lX < pad || lY < pad || lX > sW - pad || lY > sH - pad) {
                setXYCheck(CHECK.NONE, CHECK.NONE);
            }

            inRepositioningArea = lY >= depth && lY < this.titlebarHeight + pad;

            startDrag = new Point2D(mX, mY);
            startRectangle = new Rectangle2D(stage.getX(), stage.getY(), sW, sH);

        } else if (!isNone() && MouseEvent.MOUSE_DRAGGED.equals(mouseEventType)) {
            /* stage resizing */
            double dX = mX - startDrag.getX();
            double dY = mY - startDrag.getY();
            double min, max;
            /* don't overwrite start values */
            double x = startRectangle.getMinX(), y = startRectangle.getMinY(), x2 = startRectangle.getMaxX(), y2 = startRectangle.getMaxY();

            switch (checkX) {
                case LOW :// LEFT
                    min = Math.max(x - maxWidth, (0 - pad));
                    max = x2 - minWidth;
                    x = clip(x + dX, min, max);
                    break;
                case HIGH : // RIGHT
                    min = x + minWidth;
                    max = Math.min(x + maxWidth, SCREEN_BOUNDS.getWidth() + pad);
                    x2 = clip(x2 + dX, min, max);
                default :
                    break;
            }

            switch (checkY) {
                case LOW : // TOP
                    min = Math.max(y2 - maxHeight, (0 - pad));
                    max = y2 - minHeight;
                    y = clip(y + dY, min, max);
                    break;
                case HIGH :// BOTTOM
                    min = y + minHeight;
                    max = Math.min(y + maxHeight, SCREEN_BOUNDS.getHeight() + pad);
                    y2 = clip(y2 + dY, min, max);
                default :
                    break;
            }

            updateStagePosition(x, y, x2, y2);

        } else if (isNone() && MouseEvent.MOUSE_DRAGGED.equals(mouseEventType) && inRepositioningArea) {
            /* stage repositioning */
            double dX = mX - startDrag.getX();
            double dY = mY - startDrag.getY();

            this.stage.setX(startRectangle.getMinX() + dX);
            this.stage.setY(startRectangle.getMinY() + dY);
            stagePositionInsideScreen();

        } else if (!isNone() && MouseEvent.MOUSE_RELEASED.equals(mouseEventType) && mouseEvent.getClickCount() == 2) {
            /* The stage side is expanded or minimized by double-clicking */
            double min, max;
            /* don't overwrite start values */
            double x = startRectangle.getMinX(), y = startRectangle.getMinY(), x2 = startRectangle.getMaxX(), y2 = startRectangle.getMaxY();

            switch (checkX) {
                case LOW :// LEFT
                    if (x > (0 - pad)) {
                        min = Math.max(x - maxWidth, (0 - pad));
                        max = x2 - minWidth;
                        x = clip((0 - pad), min, max);
                    } else {
                        x = x2 - minWidth;
                    }
                    break;
                case HIGH : // RIGHT
                    if (x2 < SCREEN_BOUNDS.getWidth() + pad) {
                        min = x + minWidth;
                        max = Math.min(x + maxWidth, SCREEN_BOUNDS.getWidth() + pad);
                        x2 = clip(SCREEN_BOUNDS.getWidth() + pad, min, max);
                    } else {
                        x2 = x + minWidth;
                    }
                default :
                    break;
            }

            switch (checkY) {
                case LOW : // TOP
                    if (y > (0 - pad)) {
                        min = Math.max(y2 - maxHeight, (0 - pad));
                        max = y2 - minHeight;
                        y = clip((0 - pad), min, max);
                    } else {
                        y = y2 - minHeight;
                    }
                    break;
                case HIGH :// BOTTOM
                    if (y2 < SCREEN_BOUNDS.getHeight() + pad) {
                        min = y + minHeight;
                        max = Math.min(y + maxHeight, SCREEN_BOUNDS.getHeight() + pad);
                        y2 = clip(SCREEN_BOUNDS.getHeight() + pad, min, max);
                    } else {
                        y2 = y + minHeight;
                    }
                default :
                    break;
            }

            updateStagePosition(x, y, x2, y2);
        }
    }

    private double clip(double checkValue, double minValue, double maxValue) {
        if (checkValue < minValue) {
            return minValue;
        }
        if (checkValue > maxValue) {
            return maxValue;
        }
        return checkValue; // unmodified
    }

    private void setXYCheck(CHECK X, CHECK Y) {
        checkX = X;
        checkY = Y;
    }

    /** @return true if checkX and checkY is set to CHECK.NONE */
    private boolean isNone() {
        return checkX.equals(CHECK.NONE) && checkY.equals(CHECK.NONE);
    }

    private void stagePositionInsideScreen() {
        int width = (int) this.stage.getWidth();
        int height = (int) this.stage.getHeight();

        if (stage.getX() + width - pad >= SCREEN_BOUNDS.getWidth()) {
            stage.setX(SCREEN_BOUNDS.getWidth() - width + pad);
        }
        if (stage.getX() + pad < 0.0D) {
            stage.setX(0.0D - pad);
        }
        if (stage.getY() + height - pad >= SCREEN_BOUNDS.getHeight()) {
            stage.setY(SCREEN_BOUNDS.getHeight() - height + pad);
        }
        if (stage.getY() + pad < 0.0D)
            stage.setY(0.0D - pad);
    }

    private void updateStagePosition(double x1, double y1, double x2, double y2) {
        stage.setX(x1);
        stage.setY(y1);
        stage.setWidth(x2 - x1);
        stage.setHeight(y2 - y1);
    }
} // CLASS END

Don't forget to adapt the min and max size values to the Stage:

            public void setApplicationContentLayout(AbstractApp app) {
                    Pane contentLayout= app.getRootLayout();
                    BorderPane contentBorderPane = (BorderPane) rootStackPane.getChildren().get(0);
            
                    try {
                        contentBorderPane.setCenter(contentLayout);
// 2 is border width
            contentBorderPane.setMinWidth(contentLayout.getMinWidth() + 2);
            contentBorderPane.setMaxWidth(contentLayout.getMaxWidth() + 2);
// add titlebar height
            contentBorderPane.setMinHeight(contentBorderPane.getMinHeight() + contentLayout.getMinHeight() + 2);
            contentBorderPane.setMaxHeight(contentBorderPane.getMinHeight() + contentLayout.getMaxHeight() + 2);
            
                        stage.setMinWidth(contentBorderPane.getMinWidth() + 4);
                        stage.setMinHeight(contentBorderPane.getMinHeight() + 4);
                        stage.setMaxWidth(contentBorderPane.getMaxWidth() + 4);
                        stage.setMaxHeight(contentBorderPane.getMaxHeight() + 4);
                    } catch (NullPointerException e) {
                        System.out.print("error report:\n");
                        if (contentLayout == null)
                            System.out.print("WindowFrame:setApplicationContent: null \n");
                    }
if (stage.isResizable()) {
            ResizeHelper.install(stage, 28, 7, 0);
        } else {
            System.out.println("ResizeHelper not set, stage not resizable.");
        }
                }

Add the CSS-Style to Border Pane

.window{
    -fx-effect: dropshadow(three-pass-box, rgb(0,0,0,0.95), 2, 0.6, 0, 1);
    }
Coaxial answered 28/2, 2021 at 0:27 Comment(0)
C
0

https://github.com/goxr3plus/FX-BorderlessScene

This library saved my sanity, using 3 lines of code i managed to give the proper behaviour to my scene.

Craquelure answered 28/8, 2020 at 13:8 Comment(1)
This lib has the same resizing issues. Yeah, it utilizes JNA, so supposed to behave better. Still as of Gnome3 it's absolutely unusable.Demivolt
R
-5

Undecorater seems to be the only solution.

Respondent answered 25/11, 2013 at 13:8 Comment(1)
Undecorator is not a solution, it's just another problem. Anybody who has tried it, knows that.Psychological

© 2022 - 2024 — McMap. All rights reserved.