From 3c7b57816e7e4f54aa447f7a04a281ae45ba1ba4 Mon Sep 17 00:00:00 2001 From: Jakob Cornell Date: Mon, 7 Sep 2020 12:45:29 -0500 Subject: [PATCH] Initial commit --- .gitignore | 1 + build.xml | 25 ++ .../jcornell/tile_draw/DrawController.java | 142 ++++++++++ src/net/jcornell/tile_draw/Main.java | 36 +++ src/net/jcornell/tile_draw/SaveData.java | 20 ++ .../jcornell/tile_draw/TileConfigModel.java | 30 +++ .../tile_draw/TileSelectController.java | 242 ++++++++++++++++++ .../util/JSpinnerTableCellEditor.java | 27 ++ src/net/jcornell/tile_draw/util/Util.java | 111 ++++++++ 9 files changed, 634 insertions(+) create mode 100644 .gitignore create mode 100644 build.xml create mode 100644 src/net/jcornell/tile_draw/DrawController.java create mode 100644 src/net/jcornell/tile_draw/Main.java create mode 100644 src/net/jcornell/tile_draw/SaveData.java create mode 100644 src/net/jcornell/tile_draw/TileConfigModel.java create mode 100644 src/net/jcornell/tile_draw/TileSelectController.java create mode 100644 src/net/jcornell/tile_draw/util/JSpinnerTableCellEditor.java create mode 100644 src/net/jcornell/tile_draw/util/Util.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2e7327 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/out diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..f2ab79c --- /dev/null +++ b/build.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/net/jcornell/tile_draw/DrawController.java b/src/net/jcornell/tile_draw/DrawController.java new file mode 100644 index 0000000..1f8ac65 --- /dev/null +++ b/src/net/jcornell/tile_draw/DrawController.java @@ -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 remaining; + protected TileConfigModel currentTile; + protected Random randSrc; + public final DrawView view; + + public DrawController(List 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 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 index 0000000..c9bbf56 --- /dev/null +++ b/src/net/jcornell/tile_draw/Main.java @@ -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 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 index 0000000..79b3e27 --- /dev/null +++ b/src/net/jcornell/tile_draw/SaveData.java @@ -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 tileModels; + public File fileChooserDir; + + public SaveData() {} + + public SaveData(List 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 index 0000000..3d5d3e7 --- /dev/null +++ b/src/net/jcornell/tile_draw/TileConfigModel.java @@ -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 index 0000000..c2dd379 --- /dev/null +++ b/src/net/jcornell/tile_draw/TileSelectController.java @@ -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 tileModels; + public final TileSelectView view; + public File fileChooserDir; + + public TileSelectController(List tileModels, File fileChooserDir) { + this.tileModels = tileModels; + view = new TileSelectView(this); + this.fileChooserDir = fileChooserDir; + view.setCanBegin(!tileModels.isEmpty()); + } + + public void addTiles(List toAdd) { + int oldSize = tileModels.size(); + tileModels.addAll(toAdd); + super.fireTableRowsInserted(oldSize, tileModels.size()); + view.setCanBegin(true); + saveData(); + } + + public void dropTiles(List toDrop) { + for (TileConfigModel m : toDrop) { + int row = tileModels.indexOf(m); + tileModels.remove(m); + super.fireTableRowsDeleted(row, row); + } + view.setCanBegin(!tileModels.isEmpty()); + saveData(); + } + + public List 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 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 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 index 0000000..a0c4a9e --- /dev/null +++ b/src/net/jcornell/tile_draw/util/JSpinnerTableCellEditor.java @@ -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 index 0000000..225c45a --- /dev/null +++ b/src/net/jcornell/tile_draw/util/Util.java @@ -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); + } + } +} -- 2.30.2