// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.gui; import java.awt.Color; import java.awt.ComponentOrientation; import java.awt.Dimension; import java.awt.Graphics; import java.awt.GridLayout; import java.awt.Image; import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.ListIterator; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.border.Border; /** * Implements a 2D grid of tiles (image blocks of fixed size) featuring resizable rows and columns * and several visual options. */ public class TileGrid extends JComponent { private static final int DEFAULT_WIDTH = 64; private static final int DEFAULT_HEIGHT = 64; private static final int DEFAULT_ROWS = 1; private static final int DEFAULT_COLUMNS = 1; private static final Color DEFAULT_COLOR = Color.LIGHT_GRAY; private JPanel root; private GridLayout gridLayout; private int rowCount, colCount; private int tileWidth, tileHeight; private Color bgColor, gridColor; private List<Image> imageList; private List<TileLabel> tileList; private Border tileBorder; private boolean bShowGrid, bShowIcons; /** * Creates an empty 1x1 tile grid. */ public TileGrid() { super(); init(DEFAULT_ROWS, DEFAULT_COLUMNS, DEFAULT_WIDTH, DEFAULT_HEIGHT); } /** * Creates an empty tile grid of the specified dimensions. Each tile has a default size of 64x64 pixels. * @param rows Number of rows * @param columns Number of columns */ public TileGrid(int rows, int columns) { super(); init(rows, columns, DEFAULT_WIDTH, DEFAULT_HEIGHT); } /** * Creates an empty tile grid of the specified dimensions. * @param rows Number of rows * @param columns Number of columns * @param tileWidth Width of each tile in pixels. * @param tileHeight Height of each tile in pixels. */ public TileGrid(int rows, int columns, int tileWidth, int tileHeight) { super(); init(rows, columns, tileWidth, tileHeight); } /** * Gets the number of tile columns. * @return Number of tile columns */ public int getTileColumns() { return colCount; } /** * Sets the number of tile columns. * @param columns The new number of tiles for each row. */ public void setColumns(int columns) { setGridSize(new Dimension(columns, rowCount)); } /** * Gets the number of rows. * @return Number of rows */ public int getTileRows() { return rowCount; } /** * Sets the number of tile rows. * @param rows The new number of rows */ public void setRows(int rows) { setGridSize(new Dimension(colCount, rows)); } /** * Sets both rows and columns. * @param rows The new number of rows * @param columns The new number of columns */ public void setGridSize(int rows, int columns) { setGridSize(new Dimension(columns, rows)); } /** * Sets both rows and columns. * @param dim Rows and columns specified as Dimension structure. */ public void setGridSize(Dimension dim) { if (dim != null) { updateGridSize(dim.height, dim.width); validate(); } } /** * Gets the width of each tile in pixels. * @return Width of each tile in pixels */ public int getTileWidth() { return tileWidth; } /** * Sets the width of each tile in pixels. * @param width New width of each tile in pixels */ public void setTileWidth(int width) { if (width > 0 && width != tileWidth) { tileWidth = width; updateSize(); validate(); } } /** * Gets the height of each tile in pixels. * @return Height of each tile in pixels */ public int getTileHeight() { return tileHeight; } /** * Sets the height of each tile in pixels. * @param height The new height of each tile in pixels */ public void setTileHeight(int height) { if (height > 0 && height != tileHeight) { tileHeight = height; updateSize(); validate(); } } /** * Returns the number of tiles displayed in the component (NOT necessarily equal to the number * of images assigned to this component!). * @return The number of tiles displayed within the grid of the component. */ public int getTileCount() { return rowCount*colCount; } /** * Returns the background color for unused tiles (i.e. for tiles without graphics). * @return Color of unused tiles */ public Color getBackgroundColor() { return bgColor; } /** * Sets the background color of unused tiles. * @param color The new background color for unused tiles */ public void setTileColor(Color color) { updateColor(color); validate(); } /** * Returns the image object located at the specified index in the internal image list. * @param index The index of the image object within the internal image list * @return An image object * @throws IndexOutOfBoundsException If no image found at the specified index */ public Image getImage(int index) throws IndexOutOfBoundsException { if (index < 0 || index >= imageList.size()) throw new IndexOutOfBoundsException("Index out of bounds: " + index); return imageList.get(index); } /** * Returns a copy of the internal image list. * @return A copy of the internal image list */ public List<Image> getImageList() { return new ArrayList<Image>(imageList); } /** * Adds a new image to the internal image list. * @param image The image to add */ public void addImage(Image image) { if (image == null) throw new NullPointerException(); imageList.add(image); updateTileList(imageList.size() - 1, 1); validate(); } /** * Adds an array of images to the internal image list. * @param images The array of images to add */ public void addImage(Image[] images) { if (images == null) throw new NullPointerException(); addImage(Arrays.asList(images)); } /** * Adds a collection of images to the internal image list. * @param images The collection of images to add */ public void addImage(List<Image> images) { if (images == null) throw new NullPointerException(); if (images.size() > 0) { int startIndex = imageList.size(); imageList.addAll(images); updateTileList(startIndex, images.size()); validate(); } } /** * Adds an image into the internal image list at a specified index position. * @param index The index position to add the image * @param image The image to insert */ public void insertImage(int index, Image image) { if (image == null) throw new NullPointerException(); if (index < 0 || index >= imageList.size()) throw new IndexOutOfBoundsException("Index out of bounds: " + index); imageList.add(index, image); updateTileList(index, imageList.size() - index); validate(); } /** * Adds an array of images into the internal image list, starting at the specified index position. * @param index The start index to add the images into * @param images The array of images to insert */ public void insertImage(int index, Image[] images) { if (images == null) throw new NullPointerException(); if (index < 0 || index >= imageList.size()) throw new IndexOutOfBoundsException("Index out of bounds: " + index); insertImage(index, Arrays.asList(images)); } /** * Adds a collection of images into the internal image list, starting at the specified index position. * @param index The start index to add the images into * @param images The collection of images to insert */ public void insertImage(int index, List<Image> images) { if (images == null) throw new NullPointerException(); if (index < 0 || index >= imageList.size()) throw new IndexOutOfBoundsException("Index out of bounds: " + index); if (images.size() > 0) { imageList.addAll(index, images); updateTileList(index, imageList.size() - index); validate(); } } /** * Replaces an existing image in the internal image list with a new one. * @param index The index position of the image to replace * @param image The replacement image */ public void replaceImage(int index, Image image) { if (image == null) throw new NullPointerException(); if (index < 0 || index >= imageList.size()) throw new IndexOutOfBoundsException("Index out of bounds: " + index); imageList.set(index, image); updateTileList(index, 1); validate(); } /** * Replaces an array of existing images with new ones. * @param index The start index of the array of images to replace * @param images The array of replacement images */ public void replaceImage(int index, Image[] images) { if (images == null) throw new NullPointerException(); if (index < 0 || index >= imageList.size()) throw new IndexOutOfBoundsException("Index out of bounds: " + index); replaceImage(index, Arrays.asList(images)); } /** * Replaces a collection of existing images with new ones. * @param index The start index of the collection of images to replace * @param images The collection of replacement images */ public void replaceImage(int index, List<Image> images) { if (images == null) throw new NullPointerException(); if (index < 0 || index >= imageList.size()) throw new IndexOutOfBoundsException("Index out of bounds: " + index); if (images.size() > 0) { int count = 0; ListIterator<Image> iterSrc = images.listIterator(index); ListIterator<Image> iterDst = images.listIterator(index); // replacing elements while (iterSrc.hasNext() && iterDst.hasNext()) { Image image = iterSrc.next(); iterDst.next(); iterDst.set(image); count++; } // adding remaining elements (if any) if (!iterDst.hasNext()) { while (iterSrc.hasNext()) { Image image = iterSrc.next(); iterDst.add(image); count++; } } updateTileList(index, count); validate(); } } /** * Removes one image from the internal image list at the specified index position. * @param index The index of the image to remove */ public void removeImage(int index) { if (index < 0 || index >= imageList.size()) throw new IndexOutOfBoundsException("Index out of bounds: " + index); removeImage(index, 1); } /** * Removes a specific number of images from the internal image list. * @param index The start position of the images to remove * @param count The number of images to remove */ public void removeImage(int index, int count) { if (index < 0 || index >= imageList.size()) throw new IndexOutOfBoundsException("Index out of bounds: " + index); while (count > 0 && imageList.size() > index) { imageList.remove(index); count--; } updateTileList(index, 0); validate(); } /** * Removes all images from the internal image list. */ public void clearImages() { imageList.clear(); updateTileList(0, 0); validate(); } /** * Returns the index position of the specified image within the internal image list. * @param image The image to look for * @return The index position of the matching image or -1 otherwise */ public int findImage(Image image) { if (image != null) { int idx = 0; ListIterator<Image> iter = imageList.listIterator(); while (iter.hasNext()) { Image listImage = iter.next(); if (listImage.equals(image)) return idx; idx++; } } return -1; } /** * Returns the tile position of the specified image. * @param image The image to look for * @return A point structure, containing the row (height) and column (width) of the matching image * or null otherwise. */ public Point findImageLocation(Image image) { return indexToLocation(findImage(image)); } // get/set image from/to specified tile location /** * Returns the image found at the specified tile position. * @param row The zero-based row of the tile * @param column The zero-based column of the tile * @return The image object or null if no image available at the specified location */ public Image getImage(int row, int column) { int index = locationToIndex(new Point(row, column)); if (index >= 0 && index < imageList.size()) return imageList.get(index); else return null; } /** * Replace or set a new image at the specified grid location. * @param row The zero-based row of the tile * @param column The zero-based column of the tile * @param image The replacement image */ public void setImage(int row, int column, Image image) { int index = locationToIndex(new Point(row, column)); if (index >= 0 && index < imageList.size()) { replaceImage(index, image); } else throw new IndexOutOfBoundsException(); } /** * Returns the number of images in the internal image list. * @return The number of images in the internal image list */ public int getImageCount() { return imageList.size(); } /** * Returns the currently active grid color. * @return The grid color as Color object. */ public Color getGridColor() { return gridColor; } /** * Sets a new grid color. * @param gridColor The new grid color. */ public void setGridColor(Color gridColor) { if (gridColor != this.gridColor) { this.gridColor = gridColor; updateTileGrid(); validate(); } } /** * Returns whether the grid is currently shown. * @return The visibility state of the grid */ public boolean getShowGrid() { return bShowGrid; } /** * Sets the visibility state of the grid * @param showGrid True to show the grid, false to hide it. */ public void setShowGrid(boolean showGrid) { if (showGrid != bShowGrid) { bShowGrid = showGrid; updateTileGrid(); validate(); } } /** * Returns the current border around each tile. * @return The border around each tile. */ public Border getTileBorder() { return tileBorder; } /** * Sets a new border around each tile. * @param border The new border for each tile */ public void setTileBorder(Border border) { if (border != this.tileBorder) { this.tileBorder = border; invalidate(); updateTileBorders(); validate(); } } /** * Returns the visibility state of the tiles * @param showIcons * @return */ public boolean getShowIcons(boolean showIcons) { return bShowIcons; } /** * Sets the visibility state of the tiles. * @param showIcons True to show the tiles, false to hide them */ public void setShowIcons(boolean showIcons) { if (showIcons != bShowIcons) { bShowIcons = showIcons; updateTileIcons(bShowIcons); validate(); } } // -------------------------- PRIVATE METHODS -------------------------- private void init(int rows, int cols, int tw, int th) { this.rowCount = 0; this.colCount = 0; this.tileWidth = Math.max(tw, 1); this.tileHeight = Math.max(th, 1); this.bgColor = null; this.imageList = new ArrayList<Image>(1); this.tileList = new ArrayList<TileLabel>(); this.tileBorder = null; this.bShowGrid = false; this.bShowIcons = true; setLayout(null); gridLayout = new GridLayout(1, 1, 0, 0); root = new JPanel(gridLayout); add(root); root.setLocation(0, 0); root.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); updateColor(DEFAULT_COLOR); updateGridSize(Math.max(rows, DEFAULT_ROWS), Math.max(cols, DEFAULT_COLUMNS)); } // Call whenever the grid size (rows or columns) changes private boolean updateGridSize(int newRows, int newCols) { if (newRows < 1 || newCols < 1) throw new IllegalArgumentException("Invalid grid size: " + newRows + "x" + newCols); if (newRows != rowCount || newCols != colCount) { if (newRows != rowCount) gridLayout.setRows(newRows); if (newCols != colCount) gridLayout.setColumns(newCols); rowCount = newRows; colCount = newCols; // synchronizing tileList with new number of tiles syncTileList(rowCount*colCount); updateTileLayout(); updateSize(); return true; } else return false; } // tileList must always contain as many elements as there are tiles on the grid private void syncTileList(int newSize) { if (newSize > 0) { // synchronizing tileList with new number of tiles if (newSize > tileList.size() && imageList.size() > tileList.size()) { ListIterator<Image> iterImage = imageList.listIterator(tileList.size()); while (iterImage.hasNext() && tileList.size() < newSize) tileList.add(createLabel(iterImage.next())); } while (newSize < tileList.size()) tileList.remove(tileList.size() - 1); while (newSize > tileList.size()) tileList.add(createLabel(null)); } } // Calls whenever the size of the main component changes private void updateSize() { Dimension dim = new Dimension(colCount*tileWidth, rowCount*tileHeight); root.setMinimumSize(dim); root.setPreferredSize(dim); root.setMaximumSize(dim); root.setSize(dim); setMinimumSize(dim); setPreferredSize(dim); setMaximumSize(dim); setSize(dim); } // Call whenever (a portion of) the tile list has to be re-created (tileCount=0: all tiles, starting at startIndex) private void updateTileList(int startIndex, int tileCount) { startIndex = Math.min(Math.max(startIndex, 0), tileList.size()); if (tileCount <= 0) tileCount = tileList.size() - startIndex; ListIterator<Image> iterImage = imageList.listIterator(startIndex); ListIterator<TileLabel> iterTile = tileList.listIterator(startIndex); // updating icons on labels while (tileCount > 0 && iterImage.hasNext() && iterTile.hasNext()) { iterTile.next().setImage(iterImage.next()); tileCount--; } // removing icons from remaining labels while (tileCount > 0 && iterTile.hasNext()) { iterTile.next().setImage(null); tileCount--; } } // Calls whenever all tiles have to be re-added to the main component private void updateTileLayout() { // 1. remove all tiles from main component root.removeAll(); // 2. re-add tiles, depending on ordering ListIterator<TileLabel> iter = tileList.listIterator(); while (iter.hasNext()) root.add(iter.next()); } // Call whenever the frame around the tiles has to be drawn/undrawn private void updateTileGrid() { root.setEnabled(false); for (final TileLabel label: tileList) { label.setGridColor(gridColor); label.setShowGrid(bShowGrid); } root.setEnabled(true); } // Call whenever a border has to be added/removed private void updateTileBorders() { root.setEnabled(false); for (final TileLabel label: tileList) { label.setBorder(tileBorder); root.setEnabled(true); } } // Call whenever the visibility state of the icons changes private void updateTileIcons(boolean visible) { // for each tile: add/remove icon if (visible != bShowIcons) { bShowIcons = visible; if (bShowIcons) { ListIterator<Image> iterSrc = imageList.listIterator(); ListIterator<TileLabel> iterDst = tileList.listIterator(); while (iterDst.hasNext()) { Image image = iterSrc.hasNext() ? iterSrc.next() : null; TileLabel label = iterDst.next(); label.setImage(image); } } else { ListIterator<TileLabel> iter = tileList.listIterator(); while (iter.hasNext()) { TileLabel label = iter.next(); label.setImage(null); } } } } // Call whenever the background color of the tiles and main component changes private void updateColor(Color color) { if (bgColor != color) { bgColor = color; root.setBackground(bgColor); // needed??? for (final TileLabel label: tileList) label.setBackground(bgColor); } } // Create a new label (use this in place of new TileLabel()) private TileLabel createLabel(Image img) { TileLabel label = new TileLabel(); label.setBackground(bgColor); label.setBorder(tileBorder); if (img != null) label.setImage(img); return label; } // Translate tile index to grid location private Point indexToLocation(int index) { if (index >= 0) { Point pt = new Point(index % colCount, index / colCount); return (pt.y < rowCount) ? pt : null; } return null; } // Translate grid location to tile index private int locationToIndex(Point location) { if (location != null) { if (location.x >= 0 && location.x < colCount && location.y >= 0 && location.y < rowCount) { return location.y*colCount + location.x; } } return -1; } //-------------------------- INNER CLASSES -------------------------- // Adding grid support to RenderCanvas private class TileLabel extends RenderCanvas { private Color gridColor; private boolean showGrid; public TileLabel() { super(); init(); } public void setShowGrid(boolean bShow) { if (bShow != showGrid) { showGrid = bShow; repaint(); } } public void setGridColor(Color newColor) { if (newColor != null && !gridColor.equals(newColor)) { gridColor = newColor; if (showGrid) repaint(); } } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (showGrid) { g.setColor(gridColor); g.drawLine(getWidth() - 1, 0, getWidth() - 1, getHeight()); g.drawLine(0, getHeight() - 1, getWidth(), getHeight() - 1); } } private void init() { showGrid = false; gridColor = Color.GRAY; } } }