package com.armadialogcreator.canvas; import com.armadialogcreator.util.UpdateGroupListener; import com.armadialogcreator.util.UpdateListenerGroup; import javafx.animation.AnimationTimer; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.geometry.Point2D; import javafx.geometry.VPos; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.paint.Color; import javafx.scene.paint.ImagePattern; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.function.Function; /** @author Kayler @since 05/11/2016. */ public abstract class UICanvas<N extends UINode> extends AnchorPane { /** javafx Canvas */ protected final Canvas canvas; /** GraphicsContext for the canvas */ protected final GraphicsContext gc; /** {@link CanvasContext} for the canvas */ protected CanvasContext canvasContext = new CanvasContext(); /** The timer that handles repainting */ protected final CanvasAnimationTimer timer; protected final Resolution resolution; protected UINode rootNode; /** Background image of the canvas */ protected ImagePattern backgroundImage = null; /** Background color of the canvas */ protected Color backgroundColor = CanvasViewColors.EDITOR_BG; /** Mouse button that is currently down */ protected final Point lastMousePosition = new Point(-1, -1);//last x and y positions of the mouse relative to the canvas protected Keys keys = new Keys(); /** All components added */ protected final ObservableList<CanvasComponent> components = FXCollections.observableArrayList(new ArrayList<>()); private volatile boolean needPaint = false; /** A synchronization lock for {@link #needPaint} to help prevent data races */ private final Object needPaintLock = new Object(); /** Set to true if {@link #requestPaint()} is not necessary and will always paint when {@link #timer} wants to */ protected boolean alwaysPaint = false; private final UpdateGroupListener renderUpdateGroupListener = (group, data) -> { requestPaint(); }; public UICanvas(@NotNull Resolution resolution, @NotNull UINode rootNode) { this.resolution = resolution; resolution.getUpdateGroup().addListener(new UpdateGroupListener<Resolution>() { @Override public void update(@NotNull UpdateListenerGroup<Resolution> group, @NotNull Resolution newResolution) { if (getCanvasHeight() != newResolution.getScreenHeight() || getCanvasWidth() != newResolution.getScreenWidth()) { canvas.setWidth(newResolution.getScreenWidth()); canvas.setHeight(newResolution.getScreenHeight()); } requestPaint(); } }); this.canvas = new Canvas(resolution.getScreenWidth(), resolution.getScreenHeight()); this.gc = this.canvas.getGraphicsContext2D(); this.getChildren().add(this.canvas); UICanvas.CanvasMouseEvent mouseEvent = new UICanvas.CanvasMouseEvent(this); this.setOnMousePressed(mouseEvent); this.setOnMouseReleased(mouseEvent); this.setOnMouseMoved(mouseEvent); this.setOnMouseDragged(mouseEvent); components.addListener(new ListChangeListener<CanvasComponent>() { @Override public void onChanged(Change<? extends CanvasComponent> c) { requestPaint(); } }); //do this last this.rootNode = rootNode; setUINodeListeners(true); timer = new CanvasAnimationTimer(); timer.start(); } @NotNull public UINode getRootNode() { return rootNode; } public int getCanvasWidth() { return (int) canvas.getWidth(); } public int getCanvasHeight() { return (int) this.canvas.getHeight(); } public void setRootNode(@NotNull UINode rootNode) { setUINodeListeners(false); this.rootNode = rootNode; setUINodeListeners(true); requestPaint(); } @SuppressWarnings("unchecked") private void setUINodeListeners(boolean add) { if (add) { this.rootNode.renderUpdateGroup().addListener(renderUpdateGroupListener); } else { this.rootNode.renderUpdateGroup().removeListener(renderUpdateGroupListener); } } /** Adds a component to the canvas and repaints the canvas */ public void addComponent(@NotNull CanvasComponent component) { this.components.add(component); } /** Removes the given component from the canvas render and user interaction. @param component component to remove @return true if the component was removed, false if nothing was removed */ public boolean removeComponent(@NotNull CanvasComponent component) { return this.components.remove(component); } /** Paint the canvas. Order of painting is: <ol> <li>background</li> <li>{@link #getRootNode()}</li> <li>components inserted via {@link #addComponent(CanvasComponent)}</li> </ol> */ protected void paint() { gc.setTextBaseline(VPos.TOP); //we actually need to run this with each call for some reason gc.save(); paintBackground(); paintRootNode(); paintComponents(); for (Function<GraphicsContext, Void> f : canvasContext.getPaintLast()) { f.apply(gc); } canvasContext.getPaintLast().clear(); gc.restore(); } /** Request a repaint. The paint operation won't happen until {@link #getTimer()} discovers the paint request. Therefore, multiple requests can be made and not have any performance impacts. <p> This method can be used across multiple threads. */ public void requestPaint() { synchronized (needPaintLock) { needPaint = true; } } /** Paints all nodes in {@link #getRootNode()} and will iterate each child's child as well. Each component will get an individual render space (GraphicsContext attributes will not bleed through each component). */ protected void paintRootNode() { paintNodes(rootNode); } private void paintNodes(@NotNull UINode node) { for (UINode child : node.deepIterateChildren()) { paintNode(rootNode); paintNodes(child); } } /** Paints all components. Each component will get an individual render space (GraphicsContext attributes will not bleed through each component). Before the paint, the components are sorted with {@link CanvasComponent#RENDER_PRIORITY_COMPARATOR} */ protected void paintComponents() { this.components.sort(CanvasComponent.RENDER_PRIORITY_COMPARATOR); for (CanvasComponent component : components) { paintComponent(component); } } protected void paintBackground() { gc.setFill(backgroundColor); gc.fillRect(0, 0, this.canvas.getWidth(), this.canvas.getHeight()); if (backgroundImage == null) { return; } gc.setFill(backgroundImage); gc.fillRect(0, 0, this.canvas.getWidth(), this.canvas.getHeight()); } protected void paintNode(@NotNull UINode node) { if (node.getComponent() == null) { return; } paintComponent(node.getComponent()); } protected void paintComponent(@NotNull CanvasComponent component) { if (component.isGhost()) { return; } gc.save(); component.paint(gc, canvasContext); gc.restore(); } /** Sets canvas background image and automatically repaints */ public void setCanvasBackgroundImage(@Nullable ImagePattern background) { this.backgroundImage = background; requestPaint(); } /** Sets canvas background color and repaints the canvas */ public void setCanvasBackgroundColor(@NotNull Color color) { this.backgroundColor = color; requestPaint(); } /** @return the background image, or null if not set */ @Nullable public ImagePattern getBackgroundImage() { return backgroundImage; } /** @return the background color */ @NotNull public Color getBackgroundColor() { return backgroundColor; } /** This is called when the mouse listener is invoked and a mouse press was the event. Default implementation does nothing. @param mousex x position of mouse relative to canvas @param mousey y position of mouse relative to canvas @param mb mouse button that was pressed */ protected void mousePressed(int mousex, int mousey, @NotNull MouseButton mb) { } /** This is called when the mouse listener is invoked and a mouse release was the event. Default implementation does nothing. @param mousex x position of mouse relative to canvas @param mousey y position of mouse relative to canvas @param mb mouse button that was released */ protected void mouseReleased(int mousex, int mousey, @NotNull MouseButton mb) { } /** This is called when the mouse is moved and/or dragged inside the canvas. Default implementation does nothing. @param mousex x position of mouse relative to canvas @param mousey y position of mouse relative to canvas */ protected void mouseMoved(int mousex, int mousey) { } /** This should be called when any mouse event occurs (press, release, drag, move, etc) @param shiftDown true if the shift key is down, false otherwise @param ctrlDown true if the ctrl key is down, false otherwise @param altDown true if alt key is down, false otherwise */ public void keyEvent(String key, boolean keyIsDown, boolean shiftDown, boolean ctrlDown, boolean altDown) { keys.update(key, keyIsDown, shiftDown, ctrlDown, altDown); requestPaint(); } /** This is called after mouseMove is called. This will ensure that no matter how mouse move exits, the last mouse position will be updated */ private void setLastMousePosition(int mousex, int mousey) { lastMousePosition.set(mousex, mousey); } @NotNull public CanvasAnimationTimer getTimer() { return timer; } @Override protected double computeMinWidth(double height) { return getCanvasWidth(); } @Override protected double computeMinHeight(double width) { return getCanvasHeight(); } @Override protected double computePrefWidth(double height) { return getCanvasWidth(); } @Override protected double computePrefHeight(double width) { return getCanvasHeight(); } @Override protected double computeMaxWidth(double height) { return super.computeMaxWidth(height); } @Override protected double computeMaxHeight(double width) { return super.computeMaxHeight(width); } @NotNull public Canvas getCanvas() { return canvas; } /** Clear any listeners attached to {@link #rootNode} */ public void clearListeners() { setUINodeListeners(false); } /** Created by Kayler on 05/13/2016. */ private static class CanvasMouseEvent implements EventHandler<MouseEvent> { private final UICanvas canvas; private boolean mouseDown = false; CanvasMouseEvent(UICanvas canvas) { this.canvas = canvas; } @Override public void handle(MouseEvent event) { MouseButton btn = event.getButton(); if (!(event.getTarget() instanceof Canvas)) { return; } Canvas c = (Canvas) event.getTarget(); Point2D p = c.sceneToLocal(event.getSceneX(), event.getSceneY()); int mousex = (int) p.getX(); int mousey = (int) p.getY(); if (event.getEventType() == MouseEvent.MOUSE_MOVED || event.getEventType() == MouseEvent.MOUSE_DRAGGED) { canvas.mouseMoved(mousex, mousey); canvas.setLastMousePosition(mousex, mousey); if (mouseDown) { this.canvas.requestPaint(); } } else { if (event.getEventType() == MouseEvent.MOUSE_PRESSED) { mouseDown = true; canvas.mousePressed(mousex, mousey, btn); } else if (event.getEventType() == MouseEvent.MOUSE_RELEASED) { canvas.mouseReleased(mousex, mousey, btn); mouseDown = false; canvas.requestPaint(); } } } } public class CanvasAnimationTimer extends AnimationTimer { private final List<Runnable> runnables = new ArrayList<>(); @Override public void handle(long now) { for (Runnable r : runnables) { r.run(); } if (!alwaysPaint) { synchronized (needPaintLock) { //synchronize to prevent data race if (needPaint) { needPaint = false; paint(); } } } else { paint(); } } /** @return a list of runnables to run on each timer update */ @NotNull public List<Runnable> getRunnables() { return runnables; } } }