Initial commit
authorJakob Cornell <jakob+gpg@jcornell.net>
Mon, 7 Sep 2020 17:45:29 +0000 (12:45 -0500)
committerJakob Cornell <jakob+gpg@jcornell.net>
Mon, 7 Sep 2020 17:45:29 +0000 (12:45 -0500)
.gitignore [new file with mode: 0644]
build.xml [new file with mode: 0644]
src/net/jcornell/tile_draw/DrawController.java [new file with mode: 0644]
src/net/jcornell/tile_draw/Main.java [new file with mode: 0644]
src/net/jcornell/tile_draw/SaveData.java [new file with mode: 0644]
src/net/jcornell/tile_draw/TileConfigModel.java [new file with mode: 0644]
src/net/jcornell/tile_draw/TileSelectController.java [new file with mode: 0644]
src/net/jcornell/tile_draw/util/JSpinnerTableCellEditor.java [new file with mode: 0644]
src/net/jcornell/tile_draw/util/Util.java [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..e2e7327
--- /dev/null
@@ -0,0 +1 @@
+/out
diff --git a/build.xml b/build.xml
new file mode 100644 (file)
index 0000000..f2ab79c
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,25 @@
+<!-- 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>
diff --git a/src/net/jcornell/tile_draw/DrawController.java b/src/net/jcornell/tile_draw/DrawController.java
new file mode 100644 (file)
index 0000000..1f8ac65
--- /dev/null
@@ -0,0 +1,142 @@
+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));
+       }
+}
diff --git a/src/net/jcornell/tile_draw/Main.java b/src/net/jcornell/tile_draw/Main.java
new file mode 100644 (file)
index 0000000..c9bbf56
--- /dev/null
@@ -0,0 +1,36 @@
+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);
+       }
+}
diff --git a/src/net/jcornell/tile_draw/SaveData.java b/src/net/jcornell/tile_draw/SaveData.java
new file mode 100644 (file)
index 0000000..79b3e27
--- /dev/null
@@ -0,0 +1,20 @@
+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;
+       }
+}
diff --git a/src/net/jcornell/tile_draw/TileConfigModel.java b/src/net/jcornell/tile_draw/TileConfigModel.java
new file mode 100644 (file)
index 0000000..3d5d3e7
--- /dev/null
@@ -0,0 +1,30 @@
+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)
+               );
+       }
+}
diff --git a/src/net/jcornell/tile_draw/TileSelectController.java b/src/net/jcornell/tile_draw/TileSelectController.java
new file mode 100644 (file)
index 0000000..c2dd379
--- /dev/null
@@ -0,0 +1,242 @@
+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);
+       }
+}
diff --git a/src/net/jcornell/tile_draw/util/JSpinnerTableCellEditor.java b/src/net/jcornell/tile_draw/util/JSpinnerTableCellEditor.java
new file mode 100644 (file)
index 0000000..a0c4a9e
--- /dev/null
@@ -0,0 +1,27 @@
+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;
+       }
+}
diff --git a/src/net/jcornell/tile_draw/util/Util.java b/src/net/jcornell/tile_draw/util/Util.java
new file mode 100644 (file)
index 0000000..225c45a
--- /dev/null
@@ -0,0 +1,111 @@
+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);
+               }
+       }
+}