ca.wumbo.doommanager.client.controller.file.entry.PlaypalController.java Source code

Java tutorial

Introduction

Here is the source code for ca.wumbo.doommanager.client.controller.file.entry.PlaypalController.java

Source

/*
 * 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;
        }
    }
}