Supports:
- Stage Resizing, with limitation at min and max bounds
- Stage Repositioning, with limitation at screen border
- Double Click, one side streching
- The cursor is set automatically by the javafx system, therefore less prone to graphical cursor bugs
- indentation support which can be used for CSS drop shadow
- clean, short, fast, easy to read, adaptable sourcecode with no complex nested queries therefore a smart clipping method
- 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);
}
java -jar Ensemble.jar
) as that is a more reliable execution model. – Chamonix