blob: 771f0ea4c51085f3d5b2f095c92b47d8fb2a76d9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015 BestSolution.at and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation
*******************************************************************************/
package org.eclipse.fx.ui.controls.stage;
import org.eclipse.fx.core.NumberUtils;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.StyleConverter;
import javafx.css.StyleableDoubleProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
/**
* A window pane who has handles at the borders to resize it
*
* @since 2.0
*/
public abstract class ResizeableFramePane extends StackPane implements Frame {
private static final CssMetaData<ResizeableFramePane, Number> RESIZE_HANDLE_SIZE = new CssMetaData<ResizeableFramePane, Number>("-efx-resize-handle-size", StyleConverter.getSizeConverter(), Double.valueOf(5)) { //$NON-NLS-1$
@Override
public boolean isSettable(ResizeableFramePane n) {
return (n.resizeHandleSize == null) || !n.resizeHandleSize.isBound();
}
@Override
public StyleableDoubleProperty getStyleableProperty(ResizeableFramePane n) {
return (StyleableDoubleProperty) n.resizeHandleSize;
}
};
private Rectangle2D backupWindowBounds;
private double mouseDragDeltaX;
private double mouseDragDeltaY;
private Node resizeLeft;
private Node resizeRight;
private Node resizeBottom;
private Node resizeTop;
private Node resizeCornerRightBottom;
private Node resizeCornerRightTop;
private Node resizeCornerLeftBottom;
private Node resizeCornerLeftTop;
private BooleanProperty minimizable = new SimpleBooleanProperty(this, "minimizable", true); //$NON-NLS-1$
private BooleanProperty maximizable = new SimpleBooleanProperty(this, "maximizable", true); //$NON-NLS-1$
private BooleanProperty resizeable = new SimpleBooleanProperty(this, "resizeable", true); //$NON-NLS-1$
private ReadOnlyBooleanWrapper lightweight = new ReadOnlyBooleanWrapper(this, "lightweight", false); //$NON-NLS-1$
private ReadOnlyBooleanWrapper maximized = new ReadOnlyBooleanWrapper(this, "maximized", false); //$NON-NLS-1$
DoubleProperty resizeHandleSize = new SimpleStyleableDoubleProperty(RESIZE_HANDLE_SIZE, this, "resizeHandleSize", Double.valueOf(5)); //$NON-NLS-1$
enum Location {
TOP, LEFT, RIGHT, BOTTOM
}
class ResizeHandler implements EventHandler<MouseEvent> {
private double width;
private double height;
private double x;
private double y;
private Point2D dragAnchor;
private Location[] locations;
public ResizeHandler(Location... locations) {
this.locations = locations;
}
@Override
public void handle(MouseEvent event) {
EventType<? extends MouseEvent> type = event.getEventType();
if (type == MouseEvent.MOUSE_RELEASED) {
this.dragAnchor = null;
} else if (type == MouseEvent.MOUSE_PRESSED) {
this.x = getStage().getX();
this.y = getStage().getY();
this.width = getStage().getWidth();
this.height = getStage().getHeight();
this.dragAnchor = new Point2D(event.getScreenX(), event.getScreenY());
} else if (type == MouseEvent.MOUSE_DRAGGED && this.dragAnchor != null) {
if (!isLightweight()) {
double deltaX = event.getScreenX() - this.dragAnchor.getX();
double deltaY = event.getScreenY() - this.dragAnchor.getY();
// update(this.x, this.y, this.width, this.height, deltaX,
// deltaY);
for (Location l : this.locations) {
switch (l) {
case TOP:
{
double value = this.height + -1 * (int) deltaY;
double constraintedValue = NumberUtils.unsignedConstraintValue(
value,
getMinHeight(),
getMaxHeight());
if( constraintedValue != value ) {
deltaY -= constraintedValue - value;
}
getStage().setY(this.y + (int) deltaY);
getStage().setHeight(constraintedValue);
break;
}
case BOTTOM:
{
double value = this.height + deltaY;
value = NumberUtils.unsignedConstraintValue(
value,
getMinHeight(),
getMaxHeight());
getStage().setHeight(value);
break;
}
case LEFT:
{
double value = this.width + -1 * (int) deltaX;
double constraintedValue = NumberUtils.unsignedConstraintValue(
value,
getMinWidth(),
getMaxWidth());
if( constraintedValue != value ) {
deltaX -= constraintedValue - value;
}
getStage().setX(this.x + (int) deltaX);
getStage().setWidth(constraintedValue);
break;
}
case RIGHT:
{
double value = this.width + deltaX;
value = NumberUtils.unsignedConstraintValue(
value,
getMinWidth(),
getMaxWidth());
getStage().setWidth(value);
break;
}
default:
break;
}
}
}
}
}
}
private ObservableValue<Window> windowProperty;
/**
* Create a new pane which is used in a heavy-weight dialog
*/
public ResizeableFramePane() {
this(false);
}
/**
* A resizeable frame pane
*
* @param lighweight
* <code>true</code> if used in a light-weight scenario
*/
public ResizeableFramePane(boolean lighweight) {
this.lightweight.set(lighweight);
if( ! lighweight ) {
{
ChangeListener<? super Number> l = (o,ol,ne) -> {
if( ne.doubleValue() != -1 ) {
if( getScene() != null && getScene().getWindow() != null ) {
Window w = getScene().getWindow();
getScene().getWindow().setWidth(NumberUtils.unsignedConstraintValue(w.getWidth(), getMinWidth(), getMaxWidth()));
}
}
};
minWidthProperty().addListener(l);
maxWidthProperty().addListener(l);
}
{
ChangeListener<? super Number> l = (o,ol,ne) -> {
if( ne.doubleValue() != -1 ) {
if( getScene() != null && getScene().getWindow() != null ) {
Window w = getScene().getWindow();
getScene().getWindow().setHeight(NumberUtils.unsignedConstraintValue(w.getHeight(), getMinHeight(), getMaxHeight()));
}
}
};
minHeightProperty().addListener(l);
maxHeightProperty().addListener(l);
}
}
Node windowNode = createWindowArea();
getChildren().add(windowNode);
initResize();
if (!lighweight) {
this.windowProperty = org.eclipse.fx.ui.controls.Util.windowProperty(this);
this.windowProperty.addListener((o, oldV, newV) -> {
if (oldV != null) {
oldV.widthProperty().removeListener(this::handleStageChange);
oldV.heightProperty().removeListener(this::handleStageChange);
oldV.xProperty().removeListener(this::handleStageChange);
oldV.yProperty().removeListener(this::handleStageChange);
}
if (newV != null) {
newV.widthProperty().addListener(this::handleStageChange);
newV.heightProperty().addListener(this::handleStageChange);
newV.xProperty().addListener(this::handleStageChange);
newV.yProperty().addListener(this::handleStageChange);
}
});
}
}
private void handleStageChange(Observable o, Number oldVal, Number newVal) {
getStyleClass().remove("window-maximized"); //$NON-NLS-1$
this.maximized.set(false);
}
private void initResize() {
initResizeLeft();
initResizeRight();
initResizeTop();
initResizeBottom();
initResizeCornerRightBottom();
initResizeCornerRightTop();
initResizeCornerLeftBottom();
initResizeCornerLeftTop();
}
private void initResizeBottom() {
this.resizeBottom = createBottomResize();
this.resizeBottom.setManaged(false);
getChildren().add(this.resizeBottom);
ResizeHandler resizeHandler = new ResizeHandler(Location.BOTTOM);
this.resizeBottom.setOnMousePressed(resizeHandler);
this.resizeBottom.setOnMouseDragged(resizeHandler);
this.resizeBottom.setOnMouseReleased(resizeHandler);
}
private void initResizeTop() {
this.resizeTop = createTopResize();
this.resizeTop.setManaged(false);
getChildren().add(this.resizeTop);
ResizeHandler h = new ResizeHandler(Location.TOP);
this.resizeTop.setOnMousePressed(h);
this.resizeTop.setOnMouseDragged(h);
this.resizeTop.setOnMouseReleased(h);
}
private void initResizeLeft() {
this.resizeLeft = createLeftResize();
this.resizeLeft.setManaged(false);
getChildren().add(this.resizeLeft);
ResizeHandler h = new ResizeHandler(Location.LEFT);
this.resizeLeft.setOnMousePressed(h);
this.resizeLeft.setOnMouseDragged(h);
this.resizeLeft.setOnMouseReleased(h);
}
private void initResizeRight() {
this.resizeRight = createRightResize();
this.resizeRight.setManaged(false);
getChildren().add(this.resizeRight);
ResizeHandler h = new ResizeHandler(Location.RIGHT);
this.resizeRight.setOnMousePressed(h);
this.resizeRight.setOnMouseDragged(h);
this.resizeRight.setOnMouseReleased(h);
}
private void initResizeCornerRightBottom() {
this.resizeCornerRightBottom = createResizeCornerRightBottom();
this.resizeCornerRightBottom.setManaged(false);
getChildren().add(this.resizeCornerRightBottom);
ResizeHandler h = new ResizeHandler(Location.BOTTOM, Location.RIGHT);
this.resizeCornerRightBottom.setOnMousePressed(h);
this.resizeCornerRightBottom.setOnMouseDragged(h);
this.resizeCornerRightBottom.setOnMouseReleased(h);
}
private void initResizeCornerRightTop() {
this.resizeCornerRightTop = createResizeCornerRightTop();
this.resizeCornerRightTop.setManaged(false);
getChildren().add(this.resizeCornerRightTop);
ResizeHandler h = new ResizeHandler(Location.TOP, Location.RIGHT);
this.resizeCornerRightTop.setOnMousePressed(h);
this.resizeCornerRightTop.setOnMouseDragged(h);
this.resizeCornerRightTop.setOnMouseReleased(h);
}
private void initResizeCornerLeftBottom() {
this.resizeCornerLeftBottom = createResizeCornerLeftBottom();
this.resizeCornerLeftBottom.setManaged(false);
getChildren().add(this.resizeCornerLeftBottom);
ResizeHandler h = new ResizeHandler(Location.BOTTOM, Location.LEFT);
this.resizeCornerLeftBottom.setOnMousePressed(h);
this.resizeCornerLeftBottom.setOnMouseDragged(h);
this.resizeCornerLeftBottom.setOnMouseReleased(h);
}
private void initResizeCornerLeftTop() {
this.resizeCornerLeftTop = createResizeCornerLeftTop();
this.resizeCornerLeftTop.setManaged(false);
getChildren().add(this.resizeCornerLeftTop);
ResizeHandler h = new ResizeHandler(Location.TOP, Location.LEFT);
this.resizeCornerLeftTop.setOnMousePressed(h);
this.resizeCornerLeftTop.setOnMouseDragged(h);
this.resizeCornerLeftTop.setOnMouseReleased(h);
}
/**
* Register the title bar which is the area used to drag the window around
*
* @param node
* the node
*/
protected void registerTitleBar(Node node) {
node.setOnMousePressed(event -> {
this.mouseDragDeltaX = event.getSceneX();
this.mouseDragDeltaY = event.getSceneY();
});
node.setOnMouseDragged(event -> {
getStage().setX(event.getScreenX() - this.mouseDragDeltaX);
getStage().setY(event.getScreenY() - this.mouseDragDeltaY);
});
node.setOnMouseClicked(e -> {
if (e.getClickCount() > 1) {
if (this.maximized.get()) {
restore();
} else {
maximize();
}
}
});
}
/**
* @return the node acting as the root container in the window
*/
protected abstract Node createWindowArea();
/**
* @return the stage
*/
protected Stage getStage() {
return (Stage) getScene().getWindow();
}
private static Node createResizeCornerRightBottom() {
Pane resizeCorner = new Pane();
resizeCorner.getStyleClass().add("window-resize-corner-right-bottom"); //$NON-NLS-1$
return resizeCorner;
}
private static Node createResizeCornerRightTop() {
Pane resizeCorner = new Pane();
resizeCorner.getStyleClass().add("window-resize-corner-right-top"); //$NON-NLS-1$
return resizeCorner;
}
private static Node createResizeCornerLeftBottom() {
Pane resizeCorner = new Pane();
resizeCorner.getStyleClass().add("window-resize-corner-left-bottom"); //$NON-NLS-1$
return resizeCorner;
}
private static Node createResizeCornerLeftTop() {
Pane resizeCorner = new Pane();
resizeCorner.getStyleClass().add("window-resize-corner-left-top"); //$NON-NLS-1$
return resizeCorner;
}
private static Node createLeftResize() {
Pane resizeCorner = new Pane();
resizeCorner.getStyleClass().add("window-resize-left"); //$NON-NLS-1$
return resizeCorner;
}
private static Node createRightResize() {
Pane resizeCorner = new Pane();
resizeCorner.getStyleClass().add("window-resize-right"); //$NON-NLS-1$
return resizeCorner;
}
private static Node createTopResize() {
Pane resizeCorner = new Pane();
resizeCorner.getStyleClass().add("window-resize-top"); //$NON-NLS-1$
return resizeCorner;
}
private static Node createBottomResize() {
Pane resizeCorner = new Pane();
resizeCorner.getStyleClass().add("window-resize-bottom"); //$NON-NLS-1$
return resizeCorner;
}
/**
* Set the main content to be displayed by this window
*
* @param content
* the content node
*/
public final void setContent(Node content) {
contentProperty().set(content);
}
/**
* The main content to be displayed by this window
*
* @return the current content
*/
public final Node getContent() {
return contentProperty().get();
}
/**
* The main content to be displayed by this window
*
* @return the property to observe
*/
public final ObjectProperty<Node> contentProperty() {
return impl_contentProperty();
}
/**
* @return the real content property
*/
protected abstract ObjectProperty<Node> impl_contentProperty();
@Override
public final void setTitle(String title) {
titleProperty().set(title);
}
/**
* @return the current title
*/
public final String getTitle() {
return titleProperty().get();
}
/**
* @return the title property
*/
public final StringProperty titleProperty() {
return impl_titleProperty();
}
/**
* @return the real title property
*/
protected abstract StringProperty impl_titleProperty();
@Override
public final void setClientArea(Node node) {
clientAreaProperty().set(node);
}
/**
* @return the current client area node
*/
public final Node getClientArea() {
return clientAreaProperty().get();
}
/**
* @return the client area property
*/
public final ObjectProperty<Node> clientAreaProperty() {
return impl_clientAreaProperty();
}
/**
* @return the real client area property
*/
protected abstract ObjectProperty<Node> impl_clientAreaProperty();
/**
* Maximize the window
*/
protected void maximize() {
// We are bound to the stage
if (!isLightweight()) {
Stage stage = getStage();
final double stageY = stage.getY();
final Screen screen = Screen.getScreensForRectangle(stage.getX(), stageY, 1, 1).get(0);
this.backupWindowBounds = new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight());
final double newStageY = screen.getVisualBounds().getMinY();
stage.setX(screen.getVisualBounds().getMinX());
stage.setY(newStageY);
stage.setWidth(screen.getVisualBounds().getWidth());
stage.setHeight(screen.getVisualBounds().getHeight());
getStyleClass().add("window-maximized"); //$NON-NLS-1$
// stage.setMaximized(true);
this.maximized.set(true);
} else {
// We are embedded into a container do nothing
}
}
/**
* Restore the window to original size
*/
protected void restore() {
if (!isLightweight()) {
if (this.backupWindowBounds != null) {
Stage stage = getStage();
stage.setX(this.backupWindowBounds.getMinX());
stage.setY(this.backupWindowBounds.getMinY());
stage.setWidth(this.backupWindowBounds.getWidth());
stage.setHeight(this.backupWindowBounds.getHeight());
stage.setMaximized(false);
this.backupWindowBounds = null;
}
}
}
/**
* @return The current state of the maximized property
*/
protected ReadOnlyBooleanProperty maximizedProperty() {
return this.maximized.getReadOnlyProperty();
}
/**
* Close the window
*/
@Override
public void close() {
close(false);
}
/**
* Close window but don't send
* <ul>
* <li>{@link FrameEvent#CLOSING} if the frame is lightweight</li>
* <li>{@link WindowEvent#WINDOW_CLOSE_REQUEST} if the frame is heavyweight
* </li>
* </ul>
*
* @param uiInteraction
* if closed by user interaction and events need to be fired
*/
protected void close(boolean uiInteraction) {
if (isLightweight()) {
// We are bound to the stage
if (uiInteraction) {
FrameEvent evt = new FrameEvent(this, FrameEvent.CLOSING);
Event.fireEvent(this, evt);
if (!evt.isConsumed()) {
Event.fireEvent(this, new FrameEvent(this, FrameEvent.CLOSED));
}
} else {
Event.fireEvent(this, new FrameEvent(this, FrameEvent.CLOSED));
}
} else {
if (uiInteraction) {
WindowEvent event = new WindowEvent(getStage(), WindowEvent.WINDOW_CLOSE_REQUEST);
Event.fireEvent(getStage(), event);
if (!event.isConsumed()) {
getStage().close();
}
} else {
getStage().close();
}
}
}
/**
* Minimize the window
*/
protected void minimize() {
// We are bound to the stage
if (!isLightweight()) {
getStage().setIconified(true);
} else {
// We are embedded into a container do nothing
}
}
@Override
public final void setMaximizable(boolean maximizeable) {
this.maximizable.set(maximizeable);
}
/**
* Observable property defining of the dialog is maximizable
*
* @return the property
*/
public final BooleanProperty maximizableProperty() {
return this.maximizable;
}
/**
* @return check if maximizable
*/
public final boolean isMaximizable() {
return this.maximizable.get();
}
@Override
public final void setMinimizable(boolean minimizable) {
this.minimizable.set(minimizable);
}
/**
* Observable property defining of the dialog is minimizable
*
* @return the property
*/
public final BooleanProperty minimizableProperty() {
return this.minimizable;
}
/**
* @return check if the dialog is minizable
*/
public final boolean isMinimizable() {
return this.minimizable.get();
}
@Override
public final void setResizeable(boolean resizable) {
this.resizeable.set(resizable);
}
/**
* @return the resize handle size
*/
public double getResizeHandleSize() {
return this.resizeHandleSize.get();
}
/**
* Set a handle size
*
* @param size
* the size
*/
public void setResizeHandleSize(double size) {
this.resizeHandleSize.set(size);
}
/**
* @return property to observe the size
*/
public DoubleProperty resizeHandleSize() {
return this.resizeHandleSize;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (this.resizeLeft != null) {
this.resizeLeft.resizeRelocate(0, this.getResizeHandleSize(), this.getResizeHandleSize(), getHeight() - 2 * this.getResizeHandleSize());
this.resizeLeft.visibleProperty().bind(this.resizeable);
}
if (this.resizeRight != null) {
this.resizeRight.resizeRelocate(getWidth() - this.getResizeHandleSize(), this.getResizeHandleSize(), this.getResizeHandleSize(), getHeight() - 2 * this.getResizeHandleSize());
this.resizeRight.visibleProperty().bind(this.resizeable);
}
if (this.resizeTop != null) {
this.resizeTop.resizeRelocate(this.getResizeHandleSize(), 0, getWidth() - this.getResizeHandleSize(), this.getResizeHandleSize());
this.resizeTop.visibleProperty().bind(this.resizeable);
}
if (this.resizeBottom != null) {
this.resizeBottom.resizeRelocate(this.getResizeHandleSize(), getHeight() - this.getResizeHandleSize(), getWidth() - this.getResizeHandleSize(), this.getResizeHandleSize());
this.resizeBottom.visibleProperty().bind(this.resizeable);
}
if (this.resizeCornerRightBottom != null) {
this.resizeCornerRightBottom.resizeRelocate(getWidth() - this.getResizeHandleSize(), getHeight() - this.getResizeHandleSize(), this.getResizeHandleSize(), this.getResizeHandleSize());
this.resizeCornerRightBottom.visibleProperty().bind(this.resizeable);
}
if (this.resizeCornerRightTop != null) {
this.resizeCornerRightTop.resizeRelocate(getWidth() - this.getResizeHandleSize(), 0, this.getResizeHandleSize(), this.getResizeHandleSize());
this.resizeCornerRightTop.visibleProperty().bind(this.resizeable);
}
if (this.resizeCornerLeftBottom != null) {
this.resizeCornerLeftBottom.resizeRelocate(0, getHeight() - this.getResizeHandleSize(), this.getResizeHandleSize(), this.getResizeHandleSize());
this.resizeCornerLeftBottom.visibleProperty().bind(this.resizeable);
}
if (this.resizeCornerLeftTop != null) {
this.resizeCornerLeftTop.resizeRelocate(0, 0, this.getResizeHandleSize(), this.getResizeHandleSize());
this.resizeCornerLeftTop.visibleProperty().bind(this.resizeable);
}
}
/**
* @return is the frame lightweight
*/
public final ReadOnlyBooleanProperty lightweightProperty() {
return this.lightweight.getReadOnlyProperty();
}
/**
* @return check if the frame is lightweight
*/
public final boolean isLightweight() {
return this.lightweightProperty().get();
}
}