package com.faforever.client.fx;
import com.faforever.client.theme.ThemeService;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.fxml.FXML;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import static com.faforever.client.fx.WindowController.WindowButtonType.CLOSE;
import static com.faforever.client.fx.WindowController.WindowButtonType.MAXIMIZE_RESTORE;
import static com.faforever.client.fx.WindowController.WindowButtonType.MINIMIZE;
public class WindowController {
public enum WindowButtonType {
MINIMIZE,
MAXIMIZE_RESTORE,
CLOSE
}
private enum ResizeDirection {
NORTH,
EAST,
SOUTH,
WEST
}
public static final double RESIZE_BORDER_WIDTH = 7d;
public static final String PROPERTY_WINDOW_DECORATOR = "windowDecorator";
private static final PseudoClass MAXIMIZED_PSEUDO_STATE = PseudoClass.getPseudoClass("maximized");
@FXML
AnchorPane contentPane;
@FXML
Button minimizeButton;
@FXML
Button maximizeButton;
@FXML
Button restoreButton;
@FXML
Button closeButton;
@FXML
AnchorPane windowRoot;
@FXML
Pane windowButtons;
@Resource
ThemeService themeService;
private Stage stage;
private boolean resizable;
private Point2D dragOffset;
private EnumSet<ResizeDirection> resizeDirections;
private boolean isResizing;
@FXML
void onMinimizeButtonClicked() {
stage.setIconified(true);
}
@FXML
void onCloseButtonClicked() {
stage.close();
}
@FXML
void onRestoreButtonClicked() {
restore();
}
private void restore() {
windowRoot.pseudoClassStateChanged(MAXIMIZED_PSEUDO_STATE, false);
stage.setMaximized(false);
AnchorPane.setTopAnchor(contentPane, RESIZE_BORDER_WIDTH);
AnchorPane.setRightAnchor(contentPane, RESIZE_BORDER_WIDTH);
AnchorPane.setBottomAnchor(contentPane, RESIZE_BORDER_WIDTH);
AnchorPane.setLeftAnchor(contentPane, RESIZE_BORDER_WIDTH);
AnchorPane.setRightAnchor(windowButtons, RESIZE_BORDER_WIDTH);
}
@FXML
void onMaximizeButtonClicked() {
maximize();
}
public void maximize() {
windowRoot.pseudoClassStateChanged(MAXIMIZED_PSEUDO_STATE, true);
Rectangle2D visualBounds = getVisualBounds(stage);
stage.setMaximized(true);
stage.setWidth(visualBounds.getWidth());
stage.setHeight(visualBounds.getHeight());
stage.setX(visualBounds.getMinX());
stage.setY(visualBounds.getMinY());
AnchorPane.setTopAnchor(contentPane, 0d);
AnchorPane.setRightAnchor(contentPane, 0d);
AnchorPane.setBottomAnchor(contentPane, 0d);
AnchorPane.setLeftAnchor(contentPane, 0d);
AnchorPane.setRightAnchor(windowButtons, 0d);
}
public static Rectangle2D getVisualBounds(Stage stage) {
double x1 = stage.getX() + (stage.getWidth() / 2);
double y1 = stage.getY() + (stage.getHeight() / 2);
Rectangle2D windowCenter = new Rectangle2D(x1, y1, 1, 1);
ObservableList<Screen> screensForRectangle = Screen.getScreensForRectangle(windowCenter);
return screensForRectangle.get(0).getVisualBounds();
}
@FXML
void initialize() {
minimizeButton.managedProperty().bind(minimizeButton.visibleProperty());
maximizeButton.managedProperty().bind(maximizeButton.visibleProperty());
restoreButton.managedProperty().bind(restoreButton.visibleProperty());
closeButton.managedProperty().bind(closeButton.visibleProperty());
}
public void configure(Stage stage, Region content, boolean resizable, WindowButtonType... buttons) {
if (this.stage != null) {
throw new IllegalStateException("Already configured");
}
this.stage = stage;
this.resizable = resizable;
Scene scene = new Scene(windowRoot);
stage.setScene(scene);
themeService.registerScene(scene);
// Configure these only once per stage
if (!stage.getProperties().containsKey(PROPERTY_WINDOW_DECORATOR)) {
stage.getProperties().put(PROPERTY_WINDOW_DECORATOR, this);
stage.iconifiedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue && stage.isMaximized()) {
maximize();
}
});
}
maximizeButton.managedProperty().bind(maximizeButton.visibleProperty());
restoreButton.managedProperty().bind(restoreButton.visibleProperty());
List<WindowButtonType> buttonList = Arrays.asList(buttons);
if (!resizable) {
maximizeButton.setVisible(false);
restoreButton.setVisible(false);
} else if (buttonList.contains(MAXIMIZE_RESTORE)) {
maximizeButton.visibleProperty().bind(stage.maximizedProperty().not());
restoreButton.visibleProperty().bind(stage.maximizedProperty());
} else {
maximizeButton.setVisible(false);
restoreButton.setVisible(false);
}
minimizeButton.setVisible(buttonList.contains(MINIMIZE));
closeButton.setVisible(buttonList.contains(CLOSE));
resizeDirections = EnumSet.noneOf(ResizeDirection.class);
if (stage.isMaximized()) {
maximize();
} else {
restore();
}
setContent(content);
}
public void setContent(Region content) {
contentPane.getChildren().setAll(content);
AnchorPane.setTopAnchor(content, 0d);
AnchorPane.setRightAnchor(content, 0d);
AnchorPane.setBottomAnchor(content, 0d);
AnchorPane.setLeftAnchor(content, 0d);
if (content.getMinWidth() > 0) {
stage.minWidthProperty().bind(content.minWidthProperty());
}
if (content.getMinHeight() > 0) {
stage.minHeightProperty().bind(content.minHeightProperty());
}
windowRoot.requestLayout();
}
public Parent getWindowRoot() {
return windowRoot;
}
@FXML
void onMouseMoved(MouseEvent event) {
if (!resizable || stage.isMaximized()) {
return;
}
resizeDirections.clear();
StringBuilder cursorName = new StringBuilder(9);
boolean isSouth = event.getY() > stage.getHeight() - RESIZE_BORDER_WIDTH;
boolean isNorth = !isSouth && event.getY() < RESIZE_BORDER_WIDTH;
boolean isEast = event.getX() > stage.getWidth() - RESIZE_BORDER_WIDTH;
boolean isWest = !isEast && event.getX() < RESIZE_BORDER_WIDTH;
if (isSouth) {
resizeDirections.add(ResizeDirection.SOUTH);
cursorName.append('S');
}
if (isNorth) {
resizeDirections.add(ResizeDirection.NORTH);
cursorName.append('N');
}
if (isEast) {
resizeDirections.add(ResizeDirection.EAST);
cursorName.append('E');
}
if (isWest) {
resizeDirections.add(ResizeDirection.WEST);
cursorName.append('W');
}
if (cursorName.length() == 0) {
windowRoot.setCursor(Cursor.DEFAULT);
return;
}
windowRoot.setCursor(Cursor.cursor(cursorName.append("_RESIZE").toString()));
}
@FXML
void onMouseDragged(MouseEvent event) {
if (dragOffset == null) {
// Somehow the drag event occurred without an initial press event
onMousePressed(event);
}
if (isResizing) {
onWindowResize(event);
} else {
onWindowMove(event);
}
}
@FXML
void onMousePressed(MouseEvent event) {
if (isOnResizeBorder(event)) {
isResizing = true;
}
dragOffset = new Point2D(event.getScreenX() - stage.getX(), event.getScreenY() - stage.getY());
event.consume();
}
private void onWindowResize(MouseEvent event) {
final double oldX = stage.getX();
final double oldY = stage.getY();
double newHeight = stage.getHeight();
double newWidth = stage.getWidth();
if (resizeDirections.contains(ResizeDirection.NORTH)) {
double newY = event.getScreenY() - dragOffset.getY();
stage.setY(newY);
newHeight += oldY - newY;
} else if (resizeDirections.contains(ResizeDirection.SOUTH)) {
newHeight = event.getScreenY() - stage.getY();
}
if (resizeDirections.contains(ResizeDirection.WEST)) {
double newX = event.getScreenX() - dragOffset.getX();
stage.setX(newX);
newWidth += oldX - newX;
} else if (resizeDirections.contains(ResizeDirection.EAST)) {
newWidth = event.getScreenX() - stage.getX();
}
if (newHeight < stage.getMinHeight()) {
newHeight = stage.getMinHeight();
}
if (newWidth < stage.getMinWidth()) {
newWidth = stage.getMinWidth();
}
stage.setHeight(newHeight);
stage.setWidth(newWidth);
event.consume();
}
private void onWindowMove(MouseEvent event) {
if (event.getTarget() == windowRoot) {
return;
}
if (stage.isMaximized()) {
return;
}
double newY = event.getScreenY() - dragOffset.getY();
double newX = event.getScreenX() - dragOffset.getX();
stage.setY(newY);
stage.setX(newX);
event.consume();
}
private boolean isOnResizeBorder(MouseEvent event) {
return event.getY() > stage.getHeight() - RESIZE_BORDER_WIDTH
|| event.getY() < RESIZE_BORDER_WIDTH
|| event.getX() > stage.getWidth() - RESIZE_BORDER_WIDTH
|| event.getX() < RESIZE_BORDER_WIDTH;
}
@FXML
void onMouseClicked(MouseEvent event) {
if (event.getTarget() instanceof Pane
&& event.getButton() == MouseButton.PRIMARY
&& event.getClickCount() == 2 && resizable) {
if (stage.isMaximized()) {
restore();
} else {
maximize();
}
}
}
@FXML
void onMouseReleased() {
isResizing = false;
dragOffset = null;
}
public void onMouseExited() {
windowRoot.setCursor(Cursor.DEFAULT);
}
public static void maximize(Stage stage) {
((WindowController) stage.getProperties().get(PROPERTY_WINDOW_DECORATOR)).maximize();
}
}