package market.guess.ui.desktop; import javafx.fxml.FXMLLoader; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Scene; import javafx.scene.control.ScrollPane; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; import javafx.stage.StageStyle; import market.guess.api.AccountContext; import market.guess.api.CatalogContext; import market.guess.api.GuessMarketContext; import market.guess.ui.desktop.controllers.AppController; public final class AppView { private static final double CONTENT_MIN_WIDTH = 1040; private static final double CONTENT_MIN_HEIGHT = 560; private static final double WINDOW_MIN_WIDTH = 400; private static final double WINDOW_MIN_HEIGHT = 300; private final Stage stage; private final CatalogContext catalogContext; private final GuessMarketContext marketContext; private final AccountContext accountContext; private final AppState state; private AppController controller; public AppView(Stage stage) { this(stage, new ServiceEngine()); } public AppView(Stage stage, ServiceEngine engine) { this(stage, engine.getCatalogContext(), engine.getMarketContext(), engine.getAccountContext()); } public AppView( Stage stage, CatalogContext catalogContext, GuessMarketContext marketContext, AccountContext accountContext) { this.stage = stage; this.catalogContext = catalogContext; this.marketContext = marketContext; this.accountContext = accountContext; this.state = new AppState(catalogContext, marketContext, accountContext); stage.initStyle(StageStyle.TRANSPARENT); initScene(); } public CatalogContext getCatalogContext() { return catalogContext; } public GuessMarketContext getMarketContext() { return marketContext; } public AccountContext getAccountContext() { return accountContext; } private void initScene() { try { var loader = new FXMLLoader(getClass().getResource("app_view.fxml")); StackPane root = loader.load(); this.controller = loader.getController(); this.controller.init(this); var clip = new Rectangle(); clip.setArcWidth(22); clip.setArcHeight(22); clip.widthProperty().bind(root.widthProperty()); clip.heightProperty().bind(root.heightProperty()); root.setClip(clip); addResizeHandles(root); var scene = new Scene(root, 1280, 800); scene.setFill(Color.TRANSPARENT); scene.getStylesheets().add(getClass().getResource("theme.css").toExternalForm()); stage.setScene(scene); stage.setTitle("Guess Market"); stage.setMinWidth(WINDOW_MIN_WIDTH); stage.setMinHeight(WINDOW_MIN_HEIGHT); // lookup() can't see ScrollPane content until its skin exists, so go through getContent(). var appScroll = (ScrollPane) root.lookup(".gm-app-scroll"); ((Region) appScroll.getContent()).setMinSize(CONTENT_MIN_WIDTH, CONTENT_MIN_HEIGHT); controller.refresh(); } catch (Exception e) { throw new RuntimeException("Failed to load app_view.fxml", e); } } public Stage getStage() { return stage; } public AppState getState() { return state; } public void show() { stage.show(); } private static final double RESIZE_MARGIN = 6; private static final double RESIZE_CORNER = 12; private void addResizeHandles(StackPane stack) { stack .getChildren() .addAll( resizeHandle( stack, Cursor.N_RESIZE, Pos.TOP_CENTER, -1, RESIZE_MARGIN, false, false, true, false), resizeHandle( stack, Cursor.S_RESIZE, Pos.BOTTOM_CENTER, -1, RESIZE_MARGIN, false, false, false, true), resizeHandle( stack, Cursor.W_RESIZE, Pos.CENTER_LEFT, RESIZE_MARGIN, -1, true, false, false, false), resizeHandle( stack, Cursor.E_RESIZE, Pos.CENTER_RIGHT, RESIZE_MARGIN, -1, false, true, false, false), resizeHandle( stack, Cursor.NW_RESIZE, Pos.TOP_LEFT, RESIZE_CORNER, RESIZE_CORNER, true, false, true, false), resizeHandle( stack, Cursor.NE_RESIZE, Pos.TOP_RIGHT, RESIZE_CORNER, RESIZE_CORNER, false, true, true, false), resizeHandle( stack, Cursor.SW_RESIZE, Pos.BOTTOM_LEFT, RESIZE_CORNER, RESIZE_CORNER, true, false, false, true), resizeHandle( stack, Cursor.SE_RESIZE, Pos.BOTTOM_RIGHT, RESIZE_CORNER, RESIZE_CORNER, false, true, false, true)); } private Region resizeHandle( StackPane stack, Cursor cursor, Pos pos, double w, double h, boolean left, boolean right, boolean top, boolean bottom) { var r = new Region(); r.setCursor(cursor); r.setMouseTransparent(false); if (w < 0) { r.prefWidthProperty().bind(stack.widthProperty()); } else { r.setPrefWidth(w); r.setMaxWidth(w); } if (h < 0) { r.prefHeightProperty().bind(stack.heightProperty()); } else { r.setPrefHeight(h); r.setMaxHeight(h); } StackPane.setAlignment(r, pos); double[] start = new double[6]; r.setOnMousePressed( e -> { if (stage.isMaximized()) return; start[0] = e.getScreenX(); start[1] = e.getScreenY(); start[2] = stage.getX(); start[3] = stage.getY(); start[4] = stage.getWidth(); start[5] = stage.getHeight(); e.consume(); }); r.setOnMouseDragged( e -> { if (stage.isMaximized()) return; double dx = e.getScreenX() - start[0]; double dy = e.getScreenY() - start[1]; if (right) stage.setWidth(Math.max(stage.getMinWidth(), start[4] + dx)); if (bottom) stage.setHeight(Math.max(stage.getMinHeight(), start[5] + dy)); if (left) { double newW = Math.max(stage.getMinWidth(), start[4] - dx); stage.setX(start[2] + (start[4] - newW)); stage.setWidth(newW); } if (top) { double newH = Math.max(stage.getMinHeight(), start[5] - dy); stage.setY(start[3] + (start[5] - newH)); stage.setHeight(newH); } e.consume(); }); return r; } }