Skip to content

Instantly share code, notes, and snippets.

@Xemiru
Created May 22, 2016 16:34
Show Gist options
  • Save Xemiru/6d87a748820ec2a398ba2a11cb736865 to your computer and use it in GitHub Desktop.
Save Xemiru/6d87a748820ec2a398ba2a11cb736865 to your computer and use it in GitHub Desktop.
package me.scarlet.undertailor.editor.util;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.List;
/**
* Emulation of a functional undecorated window that may optionally create its
* own window decorations.
*/
public class UndecoratedScene extends Scene {
/**
* Object holding data relevant to an owning {@link UndecoratedScene} acting
* as a window.
*/
static class WindowData {
private double[] clickAnchor;
public WindowData() {
this.clickAnchor = new double[2];
this.reset();
}
public void reset() {
this.clickAnchor[0] = -1;
this.clickAnchor[1] = -1;
}
public boolean isAnchorIdle() {
return this.clickAnchor[0] == -1 && this.clickAnchor[1] == -1;
}
public double getAnchorX() {
return this.clickAnchor[0];
}
public double getAnchorY() {
return this.clickAnchor[1];
}
public void setAnchor(double x, double y) {
this.clickAnchor[0] = x;
this.clickAnchor[1] = y;
}
}
/**
* Determines how long a window "edge" is.
*/
public static final double EDGE_DETECTION_PADDING = 6.0; // "edge"'s width in pixels
/**
* Denoting all the possible cursors used to tell whether or not we're
* trying to resize the window.
*
* <p>Includes Cursors:</p>
* <ul>
* <li>{@link Cursor#NW_RESIZE}</li>
* <li>{@link Cursor#N_RESIZE}</li>
* <li>{@link Cursor#NE_RESIZE}</li>
* <li>{@link Cursor#E_RESIZE}</li>
* <li>{@link Cursor#SE_RESIZE}</li>
* <li>{@link Cursor#S_RESIZE}</li>
* <li>{@link Cursor#SW_RESIZE}</li>
* <li>{@link Cursor#W_RESIZE}</li>
* </ul>
*/
static final Cursor[] EDGE_RESIZE_CURSORS = new Cursor[] {
Cursor.NW_RESIZE,
Cursor.N_RESIZE,
Cursor.NE_RESIZE,
Cursor.E_RESIZE,
Cursor.SE_RESIZE,
Cursor.S_RESIZE,
Cursor.SW_RESIZE,
Cursor.W_RESIZE
};
/**
* Returns whether or not the given {@link Cursor} is an accepted instance
* of a "resize cursor."
*
* @param cursor the Cursor instance to compare
* @return whether or not the provided Cursor instance matches any of the
* ones denoted by {@link UndecoratedScene#EDGE_RESIZE_CURSORS}
*
* @see Cursor
*/
static boolean isResizeCursor(Cursor cursor) {
for(int i = 0; i < UndecoratedScene.EDGE_RESIZE_CURSORS.length; i++) {
if(cursor == UndecoratedScene.EDGE_RESIZE_CURSORS[i]) {
return true;
}
}
return false;
}
/**
* Tests whether or not a given point within a rectangular space of the
* provided properties is within <code>edgeWidth</code> pixels away from any
* of its 4 edges.
*
* <pre>
* 4 bits.
*
* 3210
*
* 3 - top is active?
* 2 - bottom is active?
* 1 - left is active?
* 0 - right is active?
* </pre>
*
* @param x the x-coordinate of the point to check
* @param y the y-coordinate of the point to check
* @param width the width of the space to check with
* @param height the height of the space to check with
* @param edgeWidth the value determining the distance from the very edge of
* the space the point is required to be within in order to be
* considered as touching a given edge
* @param filter a list of values this method is allowed to return,
* producing 0 if the intended value is not contained within this
* list
*
* @return an int representative of a binary value denoting which edges the
* provided point resides in
*/
public static int testEdgePosition(double x, double y, double width, double height, double edgeWidth, int... filter) {
// generate binary to read
// 0000
// top bottom left right
int bytee = 0;
if(x < edgeWidth) {
// left
bytee = bytee | 1 << 1;
}
if(x > width - edgeWidth) {
// right
bytee = bytee | 1;
}
if(y < edgeWidth) {
// top
bytee = bytee | 1 << 3;
}
if(y > height - edgeWidth) {
// bottom
bytee = bytee | 1 << 2;
}
if(filter.length > 0) {
for(int i = 0; i < filter.length; i++) {
if(filter[i] == bytee) {
return bytee;
}
}
return 0;
}
return bytee;
}
/**
* Convenience method checking whether or not a given object of the type
* {@link Scene} is an {@link UndecoratedScene}.
*
* @param scene the object to test
*
* @return whether the provided object was an UndecoratedScene
*/
public static boolean checkScene(Scene scene) {
return scene != null && scene instanceof UndecoratedScene;
}
static final Group DUMMY_ROOT;
static {
DUMMY_ROOT = new Group();
}
private Stage stage;
private Region root;
private WindowData windowData;
private SimpleBooleanProperty resizableProperty;
private SimpleBooleanProperty maximizedProperty;
private SimpleDoubleProperty widthProperty, heightProperty;
private SimpleDoubleProperty minWidthProperty, minHeightProperty;
private SimpleDoubleProperty windowXProperty, windowYProperty;
private SimpleObjectProperty<Node> titleBarProprerty;
private EventHandler<MouseEvent> titleBarListener;
public UndecoratedScene(Stage stage, Region root) {
// throw in a dummy root so we don't trigger the real root's scene property listener
// dummy root cuz scene refuses to take null for root
super(DUMMY_ROOT);
this.root = root;
this.stage = stage;
this.windowData = new WindowData();
this.resizableProperty = new SimpleBooleanProperty(true);
this.maximizedProperty = new SimpleBooleanProperty(false);
this.windowXProperty = new SimpleDoubleProperty();
this.windowYProperty = new SimpleDoubleProperty();
this.minWidthProperty = new SimpleDoubleProperty(32);
this.minHeightProperty = new SimpleDoubleProperty(32);
this.widthProperty = new SimpleDoubleProperty();
this.heightProperty = new SimpleDoubleProperty();
this.titleBarProprerty = new SimpleObjectProperty<>();
stage.initStyle(StageStyle.UNDECORATED);
this.prepareListenerModules();
this.preparePropertyListener();
this.prepareAnchorDataListener();
this.prepareEdgeDetectListener();
this.prepareWindowResizeListener();
this.setRoot(root);
}
// modular properties
public SimpleObjectProperty<Node> titleBarProperty() {
return this.titleBarProprerty;
}
// window properties
public BooleanProperty resizableProperty() {
return this.resizableProperty;
}
public BooleanProperty maximizedProperty() {
return this.maximizedProperty;
}
public DoubleProperty windowXProperty() {
return this.windowXProperty;
}
public DoubleProperty windowYProperty() {
return this.windowYProperty;
}
public DoubleProperty minWidthProperty() {
return this.minWidthProperty;
}
public DoubleProperty minHeightProperty() {
return this.minHeightProperty;
}
public DoubleProperty windowWidthProperty() {
return this.widthProperty;
}
public DoubleProperty windowHeightProperty() {
return this.heightProperty;
}
// utility
void setRootWidth(double width) {
this.root.setMinWidth(width);
this.root.setPrefWidth(width);
this.root.setMaxWidth(width);
}
void setRootHeight(double height) {
this.root.setMinHeight(height);
this.root.setPrefHeight(height);
this.root.setMaxHeight(height);
}
// organizing initial setup
void prepareAnchorDataListener() {
this.setOnMousePressed(event -> {
if(event.getButton() == MouseButton.PRIMARY) {
this.windowData.setAnchor(event.getScreenX(), event.getScreenY());
}
});
this.setOnMouseReleased(event -> {
if(event.getButton() == MouseButton.PRIMARY) {
this.windowData.reset();
}
});
}
void prepareListenerModules() {
this.titleBarListener = event -> {
if(event.getEventType() == MouseEvent.MOUSE_DRAGGED) {
WindowData data = this.windowData;
if(event.getButton() == MouseButton.PRIMARY
&& !data.isAnchorIdle()) {
if(this.maximizedProperty.get()) {
this.maximizedProperty.set(false);
this.windowYProperty.set(event.getScreenY() - event.getSceneY());
this.windowXProperty.set(event.getScreenX() - this.widthProperty.doubleValue() * (event.getSceneX() / this.getWidth()));
} else {
double currentX = event.getScreenX();
double currentY = event.getScreenY();
double setX = this.windowXProperty.get() + (currentX - data.getAnchorX());
double setY = this.windowYProperty.get() + (currentY - data.getAnchorY());
this.windowXProperty.set(setX);
this.windowYProperty.set(setY);
data.setAnchor(currentX, currentY);
}
event.consume();
}
}
if(event.getEventType() == MouseEvent.MOUSE_RELEASED) {
if(this.windowYProperty.get() < 0) {
this.windowYProperty.set(0);
}
}
if(event.getEventType() == MouseEvent.MOUSE_CLICKED) {
if(event.getClickCount() >= 2) {
this.maximizedProperty.set(!this.maximizedProperty.get());
event.consume();
}
}
};
}
void prepareEdgeDetectListener() {
// edge detect for window resizing
this.addEventFilter(MouseEvent.MOUSE_MOVED, event -> { // add as filter, otherwise we can't tell the cursor to change if another node is in front of the root node
if(this.maximizedProperty.get()) {
return; // We're already at max size, scroob.
}
if(!this.resizableProperty.get()) {
return; // Not allowed.
}
int pos = UndecoratedScene.testEdgePosition(
event.getSceneX(),
event.getSceneY(),
this.widthProperty.get(),
this.heightProperty.get(),
UndecoratedScene.EDGE_DETECTION_PADDING);
switch(pos) {
case 10: // 1010 topleft
this.setCursor(Cursor.NW_RESIZE);
break;
case 8: // 1000 top
this.setCursor(Cursor.N_RESIZE);
break;
case 9: // 1001 topright
this.setCursor(Cursor.NE_RESIZE);
break;
case 1: // 0001 right
this.setCursor(Cursor.E_RESIZE);
break;
case 5: // 0101 bottomright
this.setCursor(Cursor.SE_RESIZE);
break;
case 4: // 0100 bottom
this.setCursor(Cursor.S_RESIZE);
break;
case 6: // 0110 bottomleft
this.setCursor(Cursor.SW_RESIZE);
break;
case 2: // 0010 left
this.setCursor(Cursor.W_RESIZE);
break;
default:
this.setCursor(Cursor.DEFAULT);
break;
}
// don't consume; we're just changing cursor and not doing anything
// no need to not let underlying children see where the mouse is
});
}
void prepareWindowResizeListener() {
this.addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> {
if(event.isPrimaryButtonDown()) {
Cursor cursor = this.getCursor();
if(UndecoratedScene.isResizeCursor(cursor)) {
// window is anchored at 0,0
// define the opposite anchor on screen coordinate plane
double oppAnchorX = this.windowXProperty.get() + this.widthProperty.get();
double oppAnchorY = this.windowYProperty.get() + this.heightProperty.get();
// should we move the window along with the resize? (north/west resizes only)
boolean moveWindowX = false;
boolean moveWindowY = false;
// default new values
double newWidth = -1;
double newHeight = -1;
// 0,0 is anchored on top left of window
// north and west have to be resized and move the window
// south and east can just have size mod
if(cursor == Cursor.NW_RESIZE // north mod
|| cursor == Cursor.N_RESIZE
|| cursor == Cursor.NE_RESIZE) {
newHeight = oppAnchorY - event.getScreenY();
moveWindowY = true;
}
if(cursor == Cursor.SW_RESIZE // south mod
|| cursor == Cursor.S_RESIZE
|| cursor == Cursor.SE_RESIZE) {
newHeight = event.getScreenY() - this.windowYProperty.get();
}
if(cursor == Cursor.NW_RESIZE // west mod
|| cursor == Cursor.W_RESIZE
|| cursor == Cursor.SW_RESIZE) {
newWidth = oppAnchorX - event.getScreenX();
moveWindowX = true;
}
if(cursor == Cursor.NE_RESIZE // east mod
|| cursor == Cursor.E_RESIZE
|| cursor == Cursor.SE_RESIZE) {
newWidth = event.getScreenX() - this.windowXProperty.get();
}
// apply
// setting goes first; it defaults to the minimum if it can't
if(newWidth > -1) {
this.widthProperty.set(newWidth);
if(moveWindowX) {
this.windowXProperty.set(oppAnchorX - this.widthProperty.get());
}
}
if(newHeight > -1) {
this.heightProperty.set(newHeight);
if(moveWindowY) {
this.windowYProperty.set(oppAnchorY - this.heightProperty.get());
}
}
event.consume();
}
}
});
}
void preparePropertyListener() {
// node listeners
this.titleBarProprerty.addListener((value, old, neww) -> {
if(neww.getScene() == null || neww.getScene() != this) {
this.titleBarProprerty.setValue(old);
throw new IllegalArgumentException("Provided title bar node must be contained in scene");
}
if(old != null) old.removeEventHandler(MouseEvent.ANY, this.titleBarListener);
neww.addEventHandler(MouseEvent.ANY, this.titleBarListener);
});
// value listeners
this.widthProperty.addListener((value, old, neww) -> {
if(!this.maximizedProperty.get()) {
if(neww.doubleValue() < this.minWidthProperty.doubleValue()) {
this.widthProperty.set(this.minWidthProperty.doubleValue());
} else {
stage.setWidth(neww.doubleValue());
this.setRootWidth(neww.doubleValue());
}
}
});
stage.widthProperty().addListener((value, old, neww) -> { if(!this.maximizedProperty.get()) this.widthProperty.set(neww.doubleValue()); }); // i don't know why this doesn't cause some weird infinite loop shit, but it doesn't lol
this.heightProperty.addListener((value, old, neww) -> {
if(!this.maximizedProperty.get()) {
if(neww.doubleValue() < this.minHeightProperty.doubleValue()) {
this.heightProperty.set(this.minHeightProperty.doubleValue());
} else {
stage.setHeight(neww.doubleValue());
this.setRootHeight(neww.doubleValue());
}
}
});
stage.heightProperty().addListener((value, old, neww) -> { if(!this.maximizedProperty.get()) this.heightProperty.set(neww.doubleValue()); });
this.minWidthProperty.addListener((value, old, neww) -> {
if(this.widthProperty.doubleValue() < neww.doubleValue()) {
this.widthProperty.set(neww.doubleValue());
}
});
stage.minWidthProperty().addListener((value, old, neww) -> { if(!this.maximizedProperty.get()) this.minWidthProperty.set(neww.doubleValue()); });
this.minHeightProperty.addListener((value, old, neww) -> {
if(this.heightProperty.doubleValue() < neww.doubleValue()) {
this.heightProperty.set(neww.doubleValue());
}
});
stage.minHeightProperty().addListener((value, old, neww) -> { if(!this.maximizedProperty.get()) this.minHeightProperty.set(neww.doubleValue()); });
this.windowXProperty.addListener((value, old, neww) -> {
if(!this.maximizedProperty.get()) {
stage.setX(neww.doubleValue());
}
});
stage.xProperty().addListener((value, old, neww) -> { if(!this.maximizedProperty.get()) this.windowXProperty.set(neww.doubleValue()); });
this.windowYProperty.addListener((value, old, neww) -> {
if(!this.maximizedProperty.get()) {
stage.setY(neww.doubleValue());
}
});
stage.yProperty().addListener((value, old, neww) -> { if(!this.maximizedProperty.get()) this.windowYProperty.set(neww.doubleValue()); });
this.maximizedProperty.addListener((value, old, neww) -> {
if(neww) { // maximize the window
if(UndecoratedScene.isResizeCursor(this.getCursor())) {
this.setCursor(Cursor.DEFAULT); // help out a stuck cursor
}
List<Screen> screens = Screen.getScreensForRectangle(this.windowXProperty.doubleValue(), this.windowYProperty.doubleValue(), this.widthProperty.doubleValue(), this.heightProperty.doubleValue());
Screen screen = screens.size() >= 1 ? screens.get(0) : Screen.getPrimary();
Rectangle2D bounds = screen.getVisualBounds();
stage.setX(bounds.getMinX());
stage.setY(bounds.getMinY());
stage.setWidth(bounds.getWidth());
this.setRootWidth(bounds.getWidth());
stage.setHeight(bounds.getHeight());
this.setRootHeight(bounds.getHeight());
} else { // restore to state prior to maximizing
stage.setX(this.windowXProperty.get());
stage.setY(this.windowYProperty.get());
stage.setWidth(this.widthProperty.get());
this.setRootWidth(this.widthProperty.get());
stage.setHeight(this.heightProperty.get());
this.setRootHeight(this.heightProperty.get());
}
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment