--- /dev/null
+<!-- Ant build file -->
+<project name="tile_draw">
+ <property name="src" location="src" />
+ <property name="out" location="out" />
+
+ <target name="setup">
+ <mkdir dir="${out}/classes" />
+ </target>
+
+ <target name="clean">
+ <delete dir="${out}" />
+ </target>
+
+ <target name="compile" depends="setup">
+ <javac srcdir="${src}" destdir="${out}/classes" debug="true" />
+ </target>
+
+ <target name="jar" depends="compile">
+ <jar jarfile="${out}/build.jar" basedir="${out}/classes">
+ <manifest>
+ <attribute name="Main-Class" value="net.jcornell.tile_draw.Main" />
+ </manifest>
+ </jar>
+ </target>
+</project>
--- /dev/null
+package net.jcornell.tile_draw;
+
+import java.awt.Dialog.ModalityType;
+import java.awt.Dimension;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import net.jcornell.tile_draw.util.Util;
+
+
+public class DrawController {
+ protected final Map<TileConfigModel, Integer> remaining;
+ protected TileConfigModel currentTile;
+ protected Random randSrc;
+ public final DrawView view;
+
+ public DrawController(List<TileConfigModel> models) {
+ remaining = models.stream()
+ .collect(Collectors.toMap(
+ Function.identity(),
+ (TileConfigModel m) -> m.multiplicity)
+ )
+ ;
+ currentTile = null;
+ randSrc = new Random();
+ view = new DrawView(this);
+ }
+
+ public void copyTile() throws Util.ImageLoadException {
+ Util.copyImage(Util.extractImage(currentTile.imageFile));
+ }
+
+ public void drawTile() {
+ int tileCount = remaining.values().stream().mapToInt(x -> x).sum();
+ int cursor = randSrc.nextInt(tileCount);
+ currentTile = null;
+ for (Map.Entry<TileConfigModel, Integer> e : remaining.entrySet()) {
+ cursor -= e.getValue();
+ if (cursor <= 0) {
+ currentTile = e.getKey();
+ break;
+ }
+ }
+ int oldCount = remaining.put(currentTile, remaining.get(currentTile) - 1);
+ if (oldCount == 1) {
+ remaining.remove(currentTile);
+ }
+ view.tileLabel.setIcon(currentTile.imageView);
+ view.setTileCount(tileCount - 1);
+ }
+}
+
+
+class DrawView {
+ protected final DrawController controller;
+ public final JDialog dialog;
+ public final JLabel tileLabel;
+ protected final JLabel tileCountLabel;
+ protected final JButton nextButton;
+
+ public DrawView(DrawController controller) {
+ this.controller = controller;
+ tileLabel = new JLabel();
+ tileCountLabel = new JLabel();
+ nextButton = new JButton("Next");
+ dialog = createDialog();
+ }
+
+ public JDialog createDialog() {
+ final String COPYING_TEXT = "Copying…";
+ final String COPIED_TEXT = "Copied";
+ final String FAILED_TEXT = "Failed";
+
+ JDialog dialog = new JDialog();
+ JLabel copyStatusBox = new JLabel("");
+
+ JButton copyButton = new JButton("Copy");
+ copyButton.addActionListener(ev -> {
+ copyStatusBox.setText(COPYING_TEXT);
+ dialog.pack();
+ try {
+ controller.copyTile();
+ } catch (Util.ImageLoadException ex) {
+ copyStatusBox.setText(FAILED_TEXT);
+ dialog.pack();
+ ex.showDialog(dialog);
+ return;
+ }
+ copyStatusBox.setText(COPIED_TEXT);
+ dialog.pack();
+ });
+
+ Dimension statusBoxSize = new Dimension();
+ for (String s : new String[] {COPYING_TEXT, COPIED_TEXT, FAILED_TEXT}) {
+ Dimension curr = new JLabel(s).getPreferredSize();
+ statusBoxSize.height = curr.height;
+ statusBoxSize.width = Math.max(statusBoxSize.width, curr.width);
+ }
+ copyStatusBox.setPreferredSize(statusBoxSize);
+
+ JPanel tilePanel = new JPanel();
+ tilePanel.setBorder(BorderFactory.createTitledBorder("Current tile"));
+
+ tilePanel.add(tileLabel);
+ tilePanel.add(copyButton);
+ tilePanel.add(copyStatusBox);
+
+ nextButton.addActionListener(ev -> {
+ controller.drawTile();
+ });
+
+ JPanel drawPanel = new JPanel();
+ drawPanel.add(tileCountLabel);
+ drawPanel.add(nextButton);
+
+ JPanel contentPanel = new JPanel();
+ contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.PAGE_AXIS));
+ contentPanel.add(tilePanel);
+ contentPanel.add(Box.createRigidArea(new Dimension(0, 5)));
+ contentPanel.add(drawPanel);
+ dialog.setContentPane(contentPanel);
+
+ dialog.setTitle("Draw Tiles");
+ dialog.setModalityType(ModalityType.APPLICATION_MODAL);
+ return dialog;
+ }
+
+ public void setTileCount(int value) {
+ nextButton.setEnabled(value > 0);
+ tileCountLabel.setText(String.format("%d more tiles", value));
+ }
+}
--- /dev/null
+package net.jcornell.tile_draw;
+
+import java.awt.Window;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.jcornell.tile_draw.util.Util;
+
+
+public class Main {
+ public static void main(String[] args) throws Exception {
+ SaveData data = null;
+ try {
+ data = Util.loadData();
+ } catch (IOException | ClassNotFoundException | Util.ImageLoadException e) {
+ e.printStackTrace();
+ }
+
+ List<TileConfigModel> tileModels;
+ File chooserDir;
+ if (data == null) {
+ tileModels = new ArrayList<>();
+ chooserDir = null;
+ } else {
+ tileModels = data.tileModels;
+ chooserDir = data.fileChooserDir;
+ }
+
+ TileSelectController tsController = new TileSelectController(tileModels, chooserDir);
+ Window window = tsController.view.window;
+ window.setLocationRelativeTo(null);
+ window.setVisible(true);
+ }
+}
--- /dev/null
+package net.jcornell.tile_draw;
+
+import java.io.Serializable;
+import java.io.File;
+import java.util.List;
+
+
+public class SaveData implements Serializable {
+ private static final long serialVersionUID = 0;
+
+ public List<TileConfigModel> tileModels;
+ public File fileChooserDir;
+
+ public SaveData() {}
+
+ public SaveData(List<TileConfigModel> tileModels, File fileChooserDir) {
+ this.tileModels = tileModels;
+ this.fileChooserDir = fileChooserDir;
+ }
+}
--- /dev/null
+package net.jcornell.tile_draw;
+
+import java.io.File;
+import java.io.Serializable;
+import javax.swing.ImageIcon;
+
+import net.jcornell.tile_draw.util.Util;
+
+
+public class TileConfigModel implements Serializable {
+ private static final long serialVersionUID = 0;
+
+ public File imageFile;
+ public int multiplicity;
+ public boolean selected;
+ protected transient ImageIcon imageView;
+
+ public TileConfigModel(File imageFile, int multiplicity) throws Util.ImageLoadException {
+ this.imageFile = imageFile;
+ this.multiplicity = multiplicity;
+ selected = false;
+ loadImage();
+ }
+
+ public void loadImage() throws Util.ImageLoadException {
+ imageView = new ImageIcon(
+ Util.extractImage(imageFile).getScaledInstance(-1, TileSelectController.THUMBNAIL_HEIGHT, 0)
+ );
+ }
+}
--- /dev/null
+package net.jcornell.tile_draw;
+
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Image;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JTable;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.table.AbstractTableModel;
+
+import net.jcornell.tile_draw.util.Util;
+import net.jcornell.tile_draw.util.JSpinnerTableCellEditor;
+
+
+public class TileSelectController extends AbstractTableModel {
+ public static final int DEFAULT_MULTIPLICITY = 1;
+ public static final int THUMBNAIL_HEIGHT = 50;
+ protected static final Class<?>[] COL_CLASSES = new Class<?>[] {Boolean.class, ImageIcon.class, Integer.class};
+ protected static final String[] COL_NAMES = new String[] {"", "Tile", "Multiplicity"};
+
+ public final List<TileConfigModel> tileModels;
+ public final TileSelectView view;
+ public File fileChooserDir;
+
+ public TileSelectController(List<TileConfigModel> tileModels, File fileChooserDir) {
+ this.tileModels = tileModels;
+ view = new TileSelectView(this);
+ this.fileChooserDir = fileChooserDir;
+ view.setCanBegin(!tileModels.isEmpty());
+ }
+
+ public void addTiles(List<TileConfigModel> toAdd) {
+ int oldSize = tileModels.size();
+ tileModels.addAll(toAdd);
+ super.fireTableRowsInserted(oldSize, tileModels.size());
+ view.setCanBegin(true);
+ saveData();
+ }
+
+ public void dropTiles(List<TileConfigModel> toDrop) {
+ for (TileConfigModel m : toDrop) {
+ int row = tileModels.indexOf(m);
+ tileModels.remove(m);
+ super.fireTableRowsDeleted(row, row);
+ }
+ view.setCanBegin(!tileModels.isEmpty());
+ saveData();
+ }
+
+ public List<TileConfigModel> getSelectedTiles() {
+ return tileModels.stream()
+ .filter(m -> m.selected)
+ .collect(Collectors.toList())
+ ;
+ }
+
+ public void beginDraw() {
+ DrawController c = new DrawController(tileModels);
+ c.drawTile();
+ Dialog dialog = c.view.dialog;
+ dialog.pack();
+ dialog.setResizable(false);
+ dialog.setLocationRelativeTo(view.window);
+ dialog.setVisible(true);
+ }
+
+ public void saveData() {
+ SaveData data = new SaveData(tileModels, fileChooserDir);
+ try {
+ Util.saveData(data);
+ } catch (IOException e) {}
+ }
+
+ @Override
+ public int getRowCount() {
+ return tileModels.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return COL_CLASSES.length;
+ }
+
+ @Override
+ public String getColumnName(int col) {
+ return COL_NAMES[col];
+ }
+
+ @Override
+ public Object getValueAt(int row, int col) {
+ TileConfigModel m = tileModels.get(row);
+ return new Object[] {m.selected, m.imageView, m.multiplicity}[col];
+ }
+
+ @Override
+ public void setValueAt(Object value, int row, int col) {
+ TileConfigModel m = tileModels.get(row);
+ if (col == 0) {
+ m.selected = (boolean) value;
+ } else if (col == 2) {
+ m.multiplicity = (int) value;
+ saveData();
+ } else {
+ throw new AssertionError();
+ }
+ }
+
+ @Override
+ public Class<?> getColumnClass(int col) {
+ return COL_CLASSES[col];
+ }
+
+ @Override
+ public boolean isCellEditable(int row, int col) {
+ return new boolean[] {true, false, true}[col];
+ }
+}
+
+
+class TileSelectView {
+ protected final TileSelectController controller;
+ protected final JTable grid;
+ protected final JButton beginButton;
+ public final JFrame window;
+
+ public TileSelectView(TileSelectController controller) {
+ this.controller = controller;
+ grid = createGrid();
+ beginButton = new JButton("Begin");
+ window = createWindow();
+ }
+
+ protected JTable createGrid() {
+ JTable grid = new JTable(controller);
+ grid.setRowHeight(TileSelectController.THUMBNAIL_HEIGHT);
+ JSpinner spinner = new JSpinner(
+ new SpinnerNumberModel(0, 0, Integer.MAX_VALUE, 1)
+ );
+ grid.getColumnModel().getColumn(2).setCellEditor(new JSpinnerTableCellEditor(spinner));
+ return grid;
+ }
+
+ private static TileConfigModel tryTileBuild(File imageFile) {
+ try {
+ return new TileConfigModel(imageFile, TileSelectController.DEFAULT_MULTIPLICITY);
+ } catch (Util.ImageLoadException e) {
+ return null;
+ }
+ }
+
+ protected JFrame createWindow() {
+ JFrame frame = new JFrame("Select Tiles");
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setLayout(new GridBagLayout());
+
+ JScrollPane tilesScrollPane = new JScrollPane(grid);
+ tilesScrollPane.setPreferredSize(new Dimension(400, 150));
+
+ GridBagConstraints tilesPaneC = new GridBagConstraints();
+ tilesPaneC.gridx = 0;
+ tilesPaneC.gridy = GridBagConstraints.RELATIVE;
+ tilesPaneC.fill = GridBagConstraints.BOTH;
+ tilesPaneC.weightx = 1;
+ tilesPaneC.weighty = 1;
+ frame.add(tilesScrollPane, tilesPaneC);
+
+ JButton deleteBtn = new JButton("Delete selected");
+ deleteBtn.addActionListener(e -> {
+ controller.dropTiles(controller.getSelectedTiles());
+ });
+
+ JButton addBtn = new JButton("Add…");
+ addBtn.addActionListener(e -> {
+ JFileChooser chooser = new JFileChooser(controller.fileChooserDir);
+ chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
+ chooser.setMultiSelectionEnabled(true);
+ int result = chooser.showOpenDialog(addBtn);
+ controller.fileChooserDir = chooser.getCurrentDirectory();
+ controller.saveData();
+ if (result == JFileChooser.APPROVE_OPTION) {
+ File[] files = chooser.getSelectedFiles();
+ List<TileConfigModel> newModels = Arrays.stream(files)
+ .map(TileSelectView::tryTileBuild)
+ .collect(Collectors.toList())
+ ;
+ if (newModels.contains(null)) {
+ JOptionPane.showMessageDialog(
+ frame, "Failed to load one or more files as images", "Image load error", JOptionPane.WARNING_MESSAGE
+ );
+ }
+ List<TileConfigModel> okay = newModels.stream()
+ .filter(m -> m != null)
+ .collect(Collectors.toList())
+ ;
+ controller.addTiles(okay);
+ }
+ });
+
+ beginButton.addActionListener(e -> {
+ controller.beginDraw();
+ });
+
+ JPanel buttonRow = new JPanel();
+ buttonRow.setLayout(new BoxLayout(buttonRow, BoxLayout.LINE_AXIS));
+ buttonRow.add(deleteBtn);
+ buttonRow.add(Box.createRigidArea(new Dimension(5, 0)));
+ buttonRow.add(addBtn);
+ buttonRow.add(Box.createHorizontalGlue());
+ buttonRow.add(beginButton);
+ buttonRow.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+
+ GridBagConstraints btnBarC = new GridBagConstraints();
+ btnBarC.gridx = 0;
+ btnBarC.gridy = GridBagConstraints.RELATIVE;
+ btnBarC.fill = GridBagConstraints.HORIZONTAL;
+ frame.add(buttonRow, btnBarC);
+
+ frame.pack();
+ return frame;
+ }
+
+ public void setCanBegin(boolean canBegin) {
+ beginButton.setEnabled(canBegin);
+ }
+}
--- /dev/null
+package net.jcornell.tile_draw.util;
+
+import java.awt.Component;
+import javax.swing.AbstractCellEditor;
+import javax.swing.JSpinner;
+import javax.swing.JTable;
+import javax.swing.table.TableCellEditor;
+
+
+public class JSpinnerTableCellEditor extends AbstractCellEditor implements TableCellEditor {
+ protected final JSpinner spinner;
+
+ public JSpinnerTableCellEditor(JSpinner spinner) {
+ this.spinner = spinner;
+ }
+
+ @Override
+ public Object getCellEditorValue() {
+ return spinner.getValue();
+ }
+
+ @Override
+ public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int col) {
+ spinner.setValue(value);
+ return spinner;
+ }
+}
--- /dev/null
+package net.jcornell.tile_draw.util;
+
+import java.awt.Component;
+import java.awt.Image;
+import java.awt.Toolkit;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import javax.imageio.ImageIO;
+import javax.swing.JOptionPane;
+
+import net.jcornell.tile_draw.SaveData;
+import net.jcornell.tile_draw.TileConfigModel;
+
+
+public final class Util {
+ private Util() {}
+
+ public static class ImageLoadException extends Exception {
+ public static enum Kind {IO, FORMAT};
+
+ public final Kind kind;
+
+ public ImageLoadException(Kind kind) {
+ this.kind = kind;
+ }
+
+ public void showDialog(Component parent) {
+ if (kind == Kind.IO) {
+ JOptionPane.showMessageDialog(
+ parent, "Error reading the image", "Error", JOptionPane.ERROR_MESSAGE
+ );
+ } else if (kind == Kind.FORMAT) {
+ JOptionPane.showMessageDialog(
+ parent, "Unrecognized image format", "Error", JOptionPane.ERROR_MESSAGE
+ );
+ } else {
+ throw new AssertionError();
+ }
+ }
+ }
+
+ public static Image extractImage(File file) throws ImageLoadException {
+ Image i;
+ try {
+ i = ImageIO.read(file);
+ } catch (IOException _e) {
+ throw new ImageLoadException(ImageLoadException.Kind.IO);
+ }
+
+ if (i == null) {
+ throw new ImageLoadException(ImageLoadException.Kind.FORMAT);
+ } else {
+ return i;
+ }
+ }
+
+ /** Copies an image to the clipboard. */
+ public static void copyImage(Image image) {
+ Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
+ Transferable t = new Transferable() {
+ @Override public DataFlavor[] getTransferDataFlavors() {
+ return new DataFlavor[] {DataFlavor.imageFlavor};
+ }
+
+ @Override public boolean isDataFlavorSupported(DataFlavor flavor) {
+ return flavor == DataFlavor.imageFlavor;
+ }
+
+ @Override public Image getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
+ if (flavor == DataFlavor.imageFlavor) {
+ return image;
+ } else {
+ throw new UnsupportedFlavorException(flavor);
+ }
+ }
+ };
+ cb.setContents(t, null);
+ }
+
+ protected static final Path SAVE_LOCATION = Paths.get(
+ System.getProperty("user.home"), "Library", "Application Support", "tile_draw.ser"
+ );
+
+ public static SaveData loadData() throws IOException, ClassNotFoundException, Util.ImageLoadException {
+ SaveData data;
+ try (InputStream input = Files.newInputStream(SAVE_LOCATION)) {
+ data = (SaveData) new ObjectInputStream(input).readObject();
+ }
+ for (TileConfigModel m : data.tileModels) {
+ m.loadImage();
+ }
+ return data;
+ }
+
+ public static void saveData(SaveData data) throws IOException {
+ try (OutputStream output = Files.newOutputStream(SAVE_LOCATION)) {
+ new ObjectOutputStream(output).writeObject(data);
+ }
+ }
+}