Java tutorial
/* * DoomManager * Copyright (C) 2014 Chris K * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.wumbo.doommanager.client.controller.file.entry; import static com.google.common.base.Preconditions.checkNotNull; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import ca.wumbo.doommanager.client.controller.CoreController; import ca.wumbo.doommanager.client.util.EntryControllable; import ca.wumbo.doommanager.client.util.ExternalMouseRenderable; import ca.wumbo.doommanager.client.util.SelfInjectableController; import ca.wumbo.doommanager.file.entry.Entry; import ca.wumbo.doommanager.file.entry.types.PlaypalEntry; import ca.wumbo.doommanager.util.MathUtil; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.input.MouseButton; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; /** * The controller for the Doom file viewer window. */ public class PlaypalController extends SelfInjectableController implements EntryControllable, ExternalMouseRenderable { /** * How many pixels should pad the boxes on each side. */ private static final int PALETTE_BOX_PADDING = 2; /** * How many palette boxes will be on each axis. This ensures it will be * square. */ private static final int PALETTE_BOXES_PER_DIMENSION = 16; /** * Indicates there is no selected index. */ private static final int NO_SELECTED_INDEX = -1; /** * The number of padding pixels for the palette changer. */ private static final int PALETTE_INDEX_PADDING_PIXELS = 16; /** * Constant text for the red label. */ private static final String RED_TEXT = "Red: "; /** * Constant text for the green label. */ private static final String GREEN_TEXT = "Green: "; /** * Constant text for the blue label. */ private static final String BLUE_TEXT = "Blue: "; /** * The entry that makes up this object. This may be null if not set * externally. */ private PlaypalEntry playpalEntry; /** * The layer that we want to view. */ private int paletteLayerToView; /** * The selected index. */ private int selectedIndex; /** * The rendering data that is used for drawing and finding out where boxes * are located relative to the canvas top left corner. */ private PaletteRenderData renderData; @Autowired private CoreController coreController; @Value("${playpal.controller.fxmlpath}") private String fxmlPath; //========================================================================= @FXML private StackPane rootStackPane; @FXML private Canvas overlayCanvas; @FXML private BorderPane borderPane; @FXML private HBox topHBox; @FXML private Button saveButton; @FXML private Canvas paletteCanvas; @FXML private VBox rightVBox; @FXML private Label redSelectionLabel; @FXML private Label greenSelectionLabel; @FXML private Label blueSelectionLabel; @FXML private HBox bottomHBox; @FXML private Button paletteIndexLeftButton; @FXML private Button paletteIndexRightButton; @FXML private Label paletteIndexLabel; @FXML private Label paletteIndexSlashLabel; @FXML private Label totalPaletteIndexes; //========================================================================= /** * The logger for this class. */ // private static final Logger log = LogManager.getLogger(PlaypalController.class); /** * Only to be instantiated by Spring. */ private PlaypalController() { renderData = new PaletteRenderData(); selectedIndex = NO_SELECTED_INDEX; } @FXML private void initialize() { // We want the canvas resizing with the stack pane. overlayCanvas.widthProperty().bind(rootStackPane.widthProperty()); overlayCanvas.heightProperty().bind(rootStackPane.heightProperty()); // Any time the dimensions change, we want to draw the canvas again. rootStackPane.widthProperty().addListener((event) -> repaintPlaypalCanvas()); rootStackPane.heightProperty().addListener((event) -> repaintPlaypalCanvas()); // The canvas in the middle should resize to it's container. borderPane.widthProperty().addListener((event) -> resizeAndRepaintCanvas()); borderPane.heightProperty().addListener((event) -> resizeAndRepaintCanvas()); // We want to get mouse events when the user clicks. paletteCanvas.setOnMouseClicked(event -> { // Left mouse click should select if we can. if (event.getButton() == MouseButton.PRIMARY) { handlePaletteCanvasLeftClick((int) event.getX(), (int) event.getY()); } }); // Default to the label values. redSelectionLabel.setText(RED_TEXT); greenSelectionLabel.setText(GREEN_TEXT); blueSelectionLabel.setText(BLUE_TEXT); } /** * Loads the FXML data and injects it into this object. Should be called by * Spring right after the constructor and dependencies are linked. To * reduce code duplication, this functionality was moved to a containing * class. * * @throws NullPointerException * If the FXML path is null. * * @throws RuntimeException * If the FXML file is missing or corrupt. */ @PostConstruct public void loadFXML() { super.loadFXML(fxmlPath); } /** * Called when the user wants to change the palette to the left. */ public void decreasePaletteIndex() { paletteLayerToView = Math.max(0, paletteLayerToView - 1); repaintPlaypalCanvas(); updateLabels(); } /** * Called when the user wants to change the palette to the right. */ public void increasePaletteIndex() { paletteLayerToView = Math.min(paletteLayerToView + 1, playpalEntry.getNumberOfPalettes() - 1); repaintPlaypalCanvas(); updateLabels(); } /** * Updates the labels to properly reflect the palette information. */ private void updateLabels() { totalPaletteIndexes.setText(Integer.toString(playpalEntry.getNumberOfPalettes())); paletteIndexLabel.setText(Integer.toString(paletteLayerToView + 1)); // Users will think '1' is the first index. } @Override public void repaintOverlayCanvas() { // TODO } /** * Paints the playpal canvas as needed. */ private void repaintPlaypalCanvas() { // At the end, we can also update the overlay canvas if needed. repaintOverlayCanvas(); // Don't paint if the entry hasn't been set, or its corrupt, or if there's no data. if (playpalEntry == null || playpalEntry.isCorrupt() || playpalEntry.getNumberOfPalettes() <= 0) return; // Before we draw, make sure we're in a valid range for our palette. paletteLayerToView = MathUtil.clamp(paletteLayerToView, 0, playpalEntry.getNumberOfPalettes() - 1); // Update the positions of where to draw the boxes. renderData.update((int) paletteCanvas.getWidth()); // Let the renderer with the data do the painting. renderData.paintPalette(); } /** * Resizes the canvas using the minimium expected widths of the borders, * and then calls for repainting. */ private void resizeAndRepaintCanvas() { // We have to resize such that the width = height, going with the smaller side. int w = (int) (borderPane.getWidth() - rightVBox.getMinWidth()); int h = (int) (borderPane.getHeight() - topHBox.getMinHeight() - bottomHBox.getMinHeight()); int dimension = Math.max(256, Math.min(w, h)); // The palette should be a square, clamp it from [0, inf). paletteCanvas.setWidth(dimension); paletteCanvas.setHeight(dimension); // Position the bottom buttons on the next pass. // We have to check for 'PALETTE_INDEX_PADDING_PIXELS' because it might not properly be refreshed. int nodeSizes = PALETTE_INDEX_PADDING_PIXELS; nodeSizes += (int) paletteIndexLeftButton.getWidth(); nodeSizes += (int) paletteIndexRightButton.getWidth(); nodeSizes += (int) paletteIndexLabel.getWidth(); nodeSizes += (int) paletteIndexSlashLabel.getWidth(); nodeSizes += (int) totalPaletteIndexes.getWidth(); if (nodeSizes != PALETTE_INDEX_PADDING_PIXELS) { HBox.setMargin(paletteIndexLeftButton, new Insets(8, 0, 0, Math.max(8, (dimension / 2) - (nodeSizes) / 2))); } else { HBox.setMargin(paletteIndexLeftButton, new Insets(8, 0, 0, (dimension / 2) - 56)); // Else, make an educated guess. } // After resizing, call for a repaint. repaintPlaypalCanvas(); } /** * Resets fields to their default value. */ private void unsetCanvasSelection() { selectedIndex = NO_SELECTED_INDEX; redSelectionLabel.setText(RED_TEXT); greenSelectionLabel.setText(GREEN_TEXT); blueSelectionLabel.setText(BLUE_TEXT); } /** * To be invoked when the user left clicks on the palette canvas. * * @param x * The X coordinate of the canvas. * * @param y * The Y coordinate of the canvas. */ private void handlePaletteCanvasLeftClick(int x, int y) { int paletteIndexClick = renderData.getSelectedIndex(x, y); // If we clicked off a box or clicked on the same box again... if (paletteIndexClick == NO_SELECTED_INDEX || paletteIndexClick == selectedIndex) { unsetCanvasSelection(); renderData.paintPalette(); // Re-render without the selected box. return; } // Since we clicked on a new palette index, load the data. selectedIndex = paletteIndexClick; Color color = playpalEntry.getPaletteColor(paletteLayerToView, selectedIndex); int r = (int) (color.getRed() * 255); int g = (int) (color.getGreen() * 255); int b = (int) (color.getBlue() * 255); redSelectionLabel.setText(RED_TEXT + r); greenSelectionLabel.setText(GREEN_TEXT + g); blueSelectionLabel.setText(BLUE_TEXT + b); // Render again with the selected box. renderData.paintPalette(); } public void save() { // TODO } @Override public Canvas getCanvas() { return overlayCanvas; } @Override public Pane getRootPane() { return rootStackPane; } @Override public void setEntry(Entry entry) { checkNotNull(entry, "Passed a null entry to a PlaypalController."); playpalEntry = (PlaypalEntry) entry; // Now that we have a valid entry, make sure the data is up to date. updateLabels(); } @Override public Entry getEntry() { return playpalEntry; } /** * Contains rendering information for the palette. */ private class PaletteRenderData { /** * How many pixels wide and tall a box is. This is the raw box and does * not include the buffering on the sides. */ private int boxSize; /** * The full box size plus padding. */ private int fullBoxSize; /** * The last canvas dimension processed. */ private int lastCanvasDimension; /** * Contains a [Y][X][0 = width offset, 1 = height offset] array. This is * used to get information for the width or height of a specific box. */ private int[] boxOffsets; /** * Creates a new empty palette rendering data object. */ public PaletteRenderData() { boxSize = 0; fullBoxSize = 0; lastCanvasDimension = 0; boxOffsets = new int[PALETTE_BOXES_PER_DIMENSION]; } /** * Performs a full update based on the canvas dimension so the location * of the palette box can be extracted quickly. * * @param canvasDimension * The dimension of the canvas */ public void update(int canvasDimension) { assert canvasDimension >= 0 : "Passed a bad canvas dimension (negative: " + canvasDimension + ")."; // If the canvas has no size, don't bother. if (canvasDimension == 0) return; // Let: b = total box width // d = inner palette dimension (where the actual color is rendered) // p = padding // w = canvas width // L = leftover pixels // We know that: // b = d + 2p // w = b * PALETTE_BOXES_PER_DIMENSION + L // // Therefore (taking integer division as floor() into account): // d = w / PALETTE_BOXES_PER_DIMENSION - 2p // L = w - PALETTE_BOXES_PER_DIMENSION * (d + 2p) // lastCanvasDimension = canvasDimension; boxSize = (canvasDimension / PALETTE_BOXES_PER_DIMENSION) - (2 * PALETTE_BOX_PADDING); fullBoxSize = boxSize + (2 * PALETTE_BOX_PADDING); int leftOverPixels = canvasDimension - (PALETTE_BOXES_PER_DIMENSION * (boxSize + (2 * PALETTE_BOX_PADDING))); // Set the position for the boxes. int offset = 0; for (int i = 0; i < PALETTE_BOXES_PER_DIMENSION; i++) { boxOffsets[i] = offset + PALETTE_BOX_PADDING; offset += (2 * PALETTE_BOX_PADDING) + boxSize; } // Now buffer by 1 pixel for the residual space. // This should take into account the ones before it, so increase linearly. int amount = 1; for (int i = PALETTE_BOXES_PER_DIMENSION - leftOverPixels; i < PALETTE_BOXES_PER_DIMENSION; i++) { boxOffsets[i] += amount; amount++; } } /** * Paints the processed box positions on the palette. */ public void paintPalette() { // Begin drawing with a clear background. GraphicsContext gc = paletteCanvas.getGraphicsContext2D(); gc.setFill(Color.BLACK); gc.fillRect(0, 0, paletteCanvas.getWidth(), paletteCanvas.getHeight()); // Draw the selected box (if any). if (selectedIndex != NO_SELECTED_INDEX) { int startX = boxOffsets[selectedIndex % PALETTE_BOXES_PER_DIMENSION]; int startY = boxOffsets[selectedIndex / PALETTE_BOXES_PER_DIMENSION]; // The selection box should be white. gc.setFill(Color.WHITE); gc.fillRect(startX - PALETTE_BOX_PADDING, startY - PALETTE_BOX_PADDING, fullBoxSize, fullBoxSize); } // Paint by rows (Y axis). for (int row = 0; row < PALETTE_BOXES_PER_DIMENSION; row++) { // Paint each column palette per row (X axis). for (int col = 0; col < PALETTE_BOXES_PER_DIMENSION; col++) { int startX = boxOffsets[col]; int startY = boxOffsets[row]; // Fill it based on whatever the color is at the palette. gc.setFill(playpalEntry.getPaletteColor(paletteLayerToView, (row * PALETTE_BOXES_PER_DIMENSION) + col)); gc.fillRect(startX, startY, boxSize, boxSize); } } } /** * Returns the index of the palette. If clicking on empty space or * outside of the palette, NO_SELECTED_INDEX is returned. * * @param x * The X location on the canvas. * * @param y * The Y location on the canvas. * * @return * The index, or NO_SELECTED_INDEX. */ public int getSelectedIndex(int x, int y) { // Must be in the bounds. if ((x < 0 || x > lastCanvasDimension) || (y < 0 || y > lastCanvasDimension)) return NO_SELECTED_INDEX; // Check if it's in any box. int boxX = -1; int boxY = -1; // Check for the X box. for (int i = 0; i < PALETTE_BOXES_PER_DIMENSION; i++) { // Determine if this is which box slot we fall into. if (x >= boxOffsets[i] + PALETTE_BOX_PADDING && x < boxOffsets[i] + fullBoxSize - PALETTE_BOX_PADDING) { boxX = i; break; } } // If we click on padding, don't bother continuing on since we won't ever click in a box. if (boxX == -1) return NO_SELECTED_INDEX; // Now check for the Y box. for (int i = 0; i < PALETTE_BOXES_PER_DIMENSION; i++) { // Determine if this is which box slot we fall into. if (y >= boxOffsets[i] + PALETTE_BOX_PADDING && y < boxOffsets[i] + fullBoxSize - PALETTE_BOX_PADDING) { boxY = i; break; } } // If we click on padding, don't bother continuing on. if (boxY == -1) return NO_SELECTED_INDEX; // Return the box we're in. return (boxY * PALETTE_BOXES_PER_DIMENSION) + boxX; } } }