Created
May 22, 2016 16:34
-
-
Save Xemiru/6d87a748820ec2a398ba2a11cb736865 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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