package org.erikaredmark.monkeyshines.editor;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.JTabbedPane;
import org.erikaredmark.monkeyshines.GameConstants;
import org.erikaredmark.monkeyshines.menu.MenuUtils;
import org.erikaredmark.monkeyshines.resource.WorldResource;
import org.erikaredmark.monkeyshines.tiles.CommonTile.StatelessTileType;
import org.erikaredmark.util.swing.layout.WrapLayout;
/**
*
* Represents the main palette for selecting tiles. The user clicks here, and then the palette is tied to
* a {@code LevelEditorMainCanvas} that will be updated to use a different 'brush'.
*
* @author Erika Redmark
*
*/
@SuppressWarnings("serial")
public class BrushPalette extends JPanel {
/**
* To save on object creation, each type of tile has one listener for each button. Each button has a different action
* command. The action command is the id of the tile. So the type of the tile goes to a type of listener, plus the id,
* gives the exact tile the user chose. They all derive the same id: Get the id from the action command, and then set
* the brush to some type.
*/
private class ChangeBrushListener implements ActionListener {
private final PaintbrushType brush;
private ChangeBrushListener(final PaintbrushType brush) {
this.brush = brush;
}
@Override public void actionPerformed(ActionEvent e) {
int id = Integer.parseInt(e.getActionCommand() );
mainCanvas.setTileBrushAndId(brush, id);
}
}
private final ChangeBrushListener SOLID_TILE_LISTENER = new ChangeBrushListener(PaintbrushType.SOLIDS);
private final ChangeBrushListener THRU_TILE_LISTENER = new ChangeBrushListener(PaintbrushType.THRUS);
private final ChangeBrushListener SCENE_TILE_LISTENER = new ChangeBrushListener(PaintbrushType.SCENES);
private final ChangeBrushListener HAZARD_TILE_LISTENER = new ChangeBrushListener(PaintbrushType.HAZARDS);
private final ChangeBrushListener CONVEYER_CLOCKWISE_LISTENER = new ChangeBrushListener(PaintbrushType.CONVEYERS_CLOCKWISE);
private final ChangeBrushListener CONVEYER_ANTI_CLOCKWISE_LISTENER = new ChangeBrushListener(PaintbrushType.CONVEYERS_ANTI_CLOCKWISE);
private final ChangeBrushListener COLLAPSIBLE_TILE_LISTENER = new ChangeBrushListener(PaintbrushType.COLLAPSIBLE);
private final ChangeBrushListener GOODIE_LISTENER = new ChangeBrushListener(PaintbrushType.GOODIES);
private final ChangeBrushListener SPRITE_LISTENER = new ChangeBrushListener(PaintbrushType.PLACE_SPRITES);
private final ChangeBrushListener ERASER_TILE_LISTENER = new ChangeBrushListener(PaintbrushType.ERASER_TILES);
private final ChangeBrushListener ERASER_SPRITE_LISTENER = new ChangeBrushListener(PaintbrushType.ERASER_SPRITES);
private final ChangeBrushListener ERASER_GOODIES_LISTENER = new ChangeBrushListener(PaintbrushType.ERASER_GOODIES);
private final ChangeBrushListener EDIT_SPRITE_LISTENER = new ChangeBrushListener(PaintbrushType.EDIT_SPRITES);
public BrushPalette(final LevelEditor mainCanvas, final WorldResource rsrc) {
this.mainCanvas = mainCanvas;
// Some standard graphics regardless of the world. All drawn and set in constructor to proper places...
// no need to save them as instance data in object.
final BufferedImage eraserMain;
final BufferedImage eraserTiles;
final BufferedImage eraserSprites;
final BufferedImage eraserGoodies;
final BufferedImage spriteMain;
final BufferedImage editSprite;
// Overlayed on Conveyer belts to make it obvious which direction it is pointing.
final BufferedImage leftArrow;
final BufferedImage rightArrow;
try {
rightArrow = ImageIO.read(BrushPalette.class.getResourceAsStream("/resources/graphics/editor/rightArrow.png") );
leftArrow = ImageIO.read(BrushPalette.class.getResourceAsStream("/resources/graphics/editor/leftArrow.png") );
eraserMain = ImageIO.read(BrushPalette.class.getResourceAsStream("/resources/graphics/editor/eraserMain.png") );
eraserTiles = ImageIO.read(BrushPalette.class.getResourceAsStream("/resources/graphics/editor/eraserTiles.png") );
eraserSprites = ImageIO.read(BrushPalette.class.getResourceAsStream("/resources/graphics/editor/eraserSprites.png") );
eraserGoodies = ImageIO.read(BrushPalette.class.getResourceAsStream("/resources/graphics/editor/eraserGoodies.png") );
editSprite = ImageIO.read(BrushPalette.class.getResourceAsStream("/resources/graphics/editor/editSprite.png") );
spriteMain = ImageIO.read(BrushPalette.class.getResourceAsStream("/resources/graphics/editor/spriteMain.png") );
} catch (IOException e) {
throw new RuntimeException("Corrupted .jar: missing right/left arrow pngs: " + e.getMessage(), e);
}
setLayout(new BorderLayout() );
final JTabbedPane brushTypes = new JTabbedPane(JTabbedPane.TOP);
add(brushTypes, BorderLayout.CENTER);
// Create tabs for all types of tiles. Add to a list so that background modification can be done
// globally
// SOLIDS + THRUS + SCENES + CONVEYERS + COLLAPSIBLES + SPRITES + HAZARDS + ERASER = 8.
final List<JPanel> palettePanels = new ArrayList<>(8);
// Sprites (creation of sprites. Removal is in ERASER tab)
{
JPanel spritesPanel = new JPanel();
spritesPanel.setLayout(new WrapLayout(FlowLayout.LEFT, GRID_MARGIN_X, GRID_MARGIN_Y) );
palettePanels.add(spritesPanel);
final int spriteCount = rsrc.getSpritesCount();
// First button is always the Edit Sprite button
spritesPanel.add(createTileButton(editSprite, 0, EDIT_SPRITE_LISTENER) );
for (int i = 0; i < spriteCount; ++i) {
BufferedImage spriteSheet = rsrc.getSpritesheetFor(i);
BufferedImage firstFrame = new BufferedImage(GameConstants.SPRITE_SIZE_X, GameConstants.SPRITE_SIZE_Y, spriteSheet.getType() );
Graphics2D g2d = firstFrame.createGraphics();
try {
g2d.drawImage(spriteSheet,
0, 0,
firstFrame.getWidth(), firstFrame.getHeight(),
0, 0,
firstFrame.getWidth(), firstFrame.getHeight(),
null);
} finally {
g2d.dispose();
}
spritesPanel.add(createTileButton(firstFrame, i, SPRITE_LISTENER) );
}
final JScrollPane typeScroller = new JScrollPane(spritesPanel);
brushTypes.addTab("", new ImageIcon(spriteMain), typeScroller);
}
{
JPanel solidsPanel = new JPanel();
palettePanels.add(solidsPanel);
JPanel thrusPanel = new JPanel();
palettePanels.add(thrusPanel);
JPanel scenesPanel = new JPanel();
palettePanels.add(scenesPanel);
// Fill tabs with buttons that correspond to their type, where their index in the list (their
// id) matches the graphic for the button, and the id that will be used when the button is clicked
// when setting the canvas brush type.
// SOLIDS, THRUS, and SCENES
initialiseBasicTilePanel(solidsPanel, brushTypes, StatelessTileType.SOLID, SOLID_TILE_LISTENER, rsrc);
initialiseBasicTilePanel(thrusPanel, brushTypes, StatelessTileType.THRU, THRU_TILE_LISTENER, rsrc);
initialiseBasicTilePanel(scenesPanel, brushTypes, StatelessTileType.SCENE, SCENE_TILE_LISTENER, rsrc);
}
// Hazards
{
JPanel hazardsPanel = new JPanel();
hazardsPanel.setLayout(new WrapLayout(FlowLayout.LEFT, GRID_MARGIN_X, GRID_MARGIN_Y) );
palettePanels.add(hazardsPanel);
BufferedImage[] tiles =
WorldResource.chop(GameConstants.TILE_SIZE_X,
GameConstants.TILE_SIZE_Y,
rsrc.getHazardSheet() );
// Only the first half are relevant
for (int i = 0; i < (tiles.length / 2); ++i) {
hazardsPanel.add(createTileButton(tiles[i], i, HAZARD_TILE_LISTENER) );
}
final JScrollPane typeScroller = new JScrollPane(hazardsPanel);
brushTypes.addTab("", new ImageIcon(tiles[0]), typeScroller);
}
// Conveyers
{
JPanel conveyersPanel = new JPanel();
conveyersPanel.setLayout(new WrapLayout(FlowLayout.LEFT, GRID_MARGIN_X, GRID_MARGIN_Y) );
palettePanels.add(conveyersPanel);
BufferedImage[] tiles =
WorldResource.chop(GameConstants.TILE_SIZE_X,
GameConstants.TILE_SIZE_Y,
rsrc.getConveyerSheet() );
for (int i = 0; i < tiles.length; i += 10) {
// TWO buttons per conveyer. There is the clockwise then anti-clockwise one
BufferedImage clockwise = tiles[i];
BufferedImage antiClockwise = tiles[i + 5];
Graphics2D gClockwise = clockwise.createGraphics();
Graphics2D gAntiClockwise = antiClockwise.createGraphics();
try {
gClockwise.drawImage(rightArrow, 0, 0, null);
gAntiClockwise.drawImage(leftArrow, 0, 0, null);
} finally {
gClockwise.dispose();
gAntiClockwise.dispose();
}
conveyersPanel.add(createTileButton(clockwise, i / 10, CONVEYER_CLOCKWISE_LISTENER) );
conveyersPanel.add(createTileButton(antiClockwise, i / 10, CONVEYER_ANTI_CLOCKWISE_LISTENER) );
}
final JScrollPane typeScroller = new JScrollPane(conveyersPanel);
brushTypes.addTab("", new ImageIcon(tiles[0]), typeScroller);
}
// Collapsibles
{
JPanel collapsiblesPanel = new JPanel();
collapsiblesPanel.setLayout(new WrapLayout(FlowLayout.LEFT, GRID_MARGIN_X, GRID_MARGIN_Y) );;
palettePanels.add(collapsiblesPanel);
BufferedImage[] tiles =
WorldResource.chop(GameConstants.TILE_SIZE_X,
GameConstants.TILE_SIZE_Y,
rsrc.getCollapsingSheet() );
for (int i = 0; i < tiles.length; i += 10) {
collapsiblesPanel.add(createTileButton(tiles[i], i / 10, COLLAPSIBLE_TILE_LISTENER) );
}
final JScrollPane typeScroller = new JScrollPane(collapsiblesPanel);
brushTypes.addTab("", new ImageIcon(tiles[0]), typeScroller);
}
// Goodies
{
// Similar techniques to hazards as the sprite sheets are similar (one row of everything, plus row of second animation frame)
JPanel goodiesPanel = new JPanel();
goodiesPanel.setLayout(new WrapLayout(FlowLayout.LEFT, GRID_MARGIN_X, GRID_MARGIN_Y) );
palettePanels.add(goodiesPanel);
BufferedImage[] tiles =
WorldResource.chop(GameConstants.TILE_SIZE_X,
GameConstants.TILE_SIZE_Y,
rsrc.getGoodieSheet() );
// Only the first half are relevant
for (int i = 0; i < (tiles.length / 2); ++i) {
goodiesPanel.add(createTileButton(tiles[i], i, GOODIE_LISTENER) );
}
final JScrollPane typeScroller = new JScrollPane(goodiesPanel);
brushTypes.addTab("", new ImageIcon(tiles[0]), typeScroller);
}
// Erasers
{
JPanel erasersPanel = new JPanel();
erasersPanel.setLayout(new WrapLayout(FlowLayout.LEFT, GRID_MARGIN_X, GRID_MARGIN_Y) );
palettePanels.add(erasersPanel);
erasersPanel.add(createTileButton(eraserTiles, 0, ERASER_TILE_LISTENER) );
erasersPanel.add(createTileButton(eraserGoodies, 0, ERASER_GOODIES_LISTENER) );
erasersPanel.add(createTileButton(eraserSprites, 0, ERASER_SPRITE_LISTENER) );
final JScrollPane typeScroller = new JScrollPane(erasersPanel);
brushTypes.addTab("", new ImageIcon(eraserMain), typeScroller);
}
// Allow background colour to invert to black... this is for transparent tiles that look horrible on white (such as
// cobwebs or any tiles predominantly white) to be visible easily.
JButton invert = new JButton("Invert Background");
invert.addActionListener(new InversionListener(palettePanels) );
add(invert, BorderLayout.SOUTH);
}
// Common code for initialising SOLIDS, THRUS, and SCENES Only
private void initialiseBasicTilePanel(JPanel panel, JTabbedPane brushTypes, StatelessTileType type, ActionListener listener, WorldResource rsrc) {
BufferedImage[] tiles =
WorldResource.chop(GameConstants.TILE_SIZE_X,
GameConstants.TILE_SIZE_Y,
rsrc.getStatelessTileTypeSheet(type) );
panel.setLayout(new WrapLayout(FlowLayout.LEFT, GRID_MARGIN_X, GRID_MARGIN_Y) );
final JScrollPane typeScroller = new JScrollPane(panel);
for (int i = 0; i < tiles.length; ++i) {
panel.add(createTileButton(tiles[i], i, listener) );
}
// Add the panel to the tabbed pane. The icon for the tab will be the first tile for the sheet.
brushTypes.addTab("", new ImageIcon(tiles[0]), typeScroller);
}
// Creates a button that graphically represents some drawable thing in the palette, setting margins and listener properly
// and forwarding the click action to the appropriate listener for the appropriate tile id.
private JButton createTileButton(BufferedImage img, int id, ActionListener listener) {
JButton tileButton = new JButton(new ImageIcon(img) );
MenuUtils.renderImageOnly(tileButton);
MenuUtils.removeMargins(tileButton);
tileButton.setActionCommand(String.valueOf(id) );
tileButton.addActionListener(listener);
return tileButton;
}
/**
*
* Switches the passed panels between their original colour and black when activated
*
* @author Erika Redmark
*
*/
private final class InversionListener implements ActionListener {
private InversionListener(final List<JPanel> panels) {
assert !(panels.isEmpty() ) : "Number of panels is predefined; cannot work with empty collection in this method";
inversionPanels = Collections.unmodifiableList(panels);
originalColor = panels.get(0).getBackground();
nextColor = Color.BLACK;
}
@Override public void actionPerformed(ActionEvent e) {
for (JPanel p : inversionPanels) {
p.setBackground(nextColor);
}
nextColor = nextColor.equals(Color.BLACK)
? originalColor
: Color.BLACK;
}
private final List<JPanel> inversionPanels;
private final Color originalColor;
// Stores the color to change to on next click.
private Color nextColor;
}
private final LevelEditor mainCanvas;
private static final int GRID_MARGIN_X = 4;
private static final int GRID_MARGIN_Y = 4;
}