From 5ae81b1f423effc7081dbe8b9dbb004da7c371c2 Mon Sep 17 00:00:00 2001 From: Jakob Cornell Date: Wed, 3 Feb 2021 19:26:20 -0600 Subject: [PATCH] Add support for multiple tile sets This is a breaking change in the save format; the application discards previously saved data if it's incompatible. --- src/net/jcornell/tile_draw/Main.java | 22 +- src/net/jcornell/tile_draw/SaveData.java | 10 +- .../jcornell/tile_draw/TileConfigModel.java | 10 +- ...roller.java => TileSetEditController.java} | 175 +++++++------ .../tile_draw/TileSetsController.java | 243 ++++++++++++++++++ src/net/jcornell/tile_draw/util/Util.java | 8 +- 6 files changed, 370 insertions(+), 98 deletions(-) rename src/net/jcornell/tile_draw/{TileSelectController.java => TileSetEditController.java} (53%) create mode 100644 src/net/jcornell/tile_draw/TileSetsController.java diff --git a/src/net/jcornell/tile_draw/Main.java b/src/net/jcornell/tile_draw/Main.java index c9bbf56..98cf8c7 100644 --- a/src/net/jcornell/tile_draw/Main.java +++ b/src/net/jcornell/tile_draw/Main.java @@ -1,10 +1,12 @@ package net.jcornell.tile_draw; +import javax.swing.JFrame; import java.awt.Window; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.awt.Dimension; import net.jcornell.tile_draw.util.Util; @@ -16,20 +18,16 @@ public class Main { data = Util.loadData(); } catch (IOException | ClassNotFoundException | Util.ImageLoadException e) { e.printStackTrace(); + data = new SaveData(new ArrayList<>(), null); } - 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; + JFrame window = new JFrame("Tile Draw"); + window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + TileSetsController controller = new TileSetsController(data.tileSets, data.fileChooserDir); + TileSetsSwingView view = new TileSetsSwingView(controller); + controller.view = view; + window.setContentPane(view.contentPane); + window.pack(); window.setLocationRelativeTo(null); window.setVisible(true); } diff --git a/src/net/jcornell/tile_draw/SaveData.java b/src/net/jcornell/tile_draw/SaveData.java index 79b3e27..2b077c5 100644 --- a/src/net/jcornell/tile_draw/SaveData.java +++ b/src/net/jcornell/tile_draw/SaveData.java @@ -4,17 +4,19 @@ import java.io.Serializable; import java.io.File; import java.util.List; +import net.jcornell.tile_draw.TileSetsController.TileSet; + public class SaveData implements Serializable { - private static final long serialVersionUID = 0; + private static final long serialVersionUID = 1; - public List tileModels; + public List tileSets; public File fileChooserDir; public SaveData() {} - public SaveData(List tileModels, File fileChooserDir) { - this.tileModels = tileModels; + public SaveData(List tileSets, File fileChooserDir) { + this.tileSets = tileSets; this.fileChooserDir = fileChooserDir; } } diff --git a/src/net/jcornell/tile_draw/TileConfigModel.java b/src/net/jcornell/tile_draw/TileConfigModel.java index 3d5d3e7..f518c2e 100644 --- a/src/net/jcornell/tile_draw/TileConfigModel.java +++ b/src/net/jcornell/tile_draw/TileConfigModel.java @@ -24,7 +24,15 @@ public class TileConfigModel implements Serializable { public void loadImage() throws Util.ImageLoadException { imageView = new ImageIcon( - Util.extractImage(imageFile).getScaledInstance(-1, TileSelectController.THUMBNAIL_HEIGHT, 0) + Util.extractImage(imageFile).getScaledInstance(-1, TileSetEditController.THUMBNAIL_HEIGHT, 0) ); } + + public TileConfigModel copy() { + try { + return new TileConfigModel(imageFile, multiplicity); + } catch (Util.ImageLoadException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/net/jcornell/tile_draw/TileSelectController.java b/src/net/jcornell/tile_draw/TileSetEditController.java similarity index 53% rename from src/net/jcornell/tile_draw/TileSelectController.java rename to src/net/jcornell/tile_draw/TileSetEditController.java index c2dd379..6f7656a 100644 --- a/src/net/jcornell/tile_draw/TileSelectController.java +++ b/src/net/jcornell/tile_draw/TileSetEditController.java @@ -2,20 +2,29 @@ package net.jcornell.tile_draw; import java.awt.Dialog; import java.awt.Dimension; +import javax.swing.JTextField; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Image; import java.io.File; +import java.awt.event.FocusEvent; import java.io.IOException; import java.util.Arrays; +import javax.swing.JDialog; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; import java.util.List; import java.util.stream.Collectors; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; +import javax.swing.table.TableCellEditor; import javax.swing.ImageIcon; +import java.awt.Insets; import javax.swing.JButton; import javax.swing.JFileChooser; +import javax.swing.JComponent; +import java.awt.Component; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPanel; @@ -25,72 +34,75 @@ 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.TileSetsController.TileSet; import net.jcornell.tile_draw.util.JSpinnerTableCellEditor; +import net.jcornell.tile_draw.util.Util; -public class TileSelectController extends AbstractTableModel { +public class TileSetEditController 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 static interface EventListener { + public void onSubmitTileSet(TileSet value); + } - public TileSelectController(List tileModels, File fileChooserDir) { - this.tileModels = tileModels; - view = new TileSelectView(this); - this.fileChooserDir = fileChooserDir; - view.setCanBegin(!tileModels.isEmpty()); + public final TileSet tileSet; + public TileSetEditSwingView view; + public final TileSetsController parent; + public final EventListener listener; + protected final TileSet originalValue; + + public TileSetEditController(TileSet tileSet, EventListener listener, TileSetsController parent) { + this.tileSet = tileSet.copy(); + this.parent = parent; + this.listener = listener; + originalValue = tileSet; } public void addTiles(List toAdd) { - int oldSize = tileModels.size(); - tileModels.addAll(toAdd); - super.fireTableRowsInserted(oldSize, tileModels.size()); - view.setCanBegin(true); - saveData(); + int oldSize = tileSet.tileModels.size(); + tileSet.tileModels.addAll(toAdd); + super.fireTableRowsInserted(oldSize, tileSet.tileModels.size()); } public void dropTiles(List toDrop) { for (TileConfigModel m : toDrop) { - int row = tileModels.indexOf(m); - tileModels.remove(m); + int row = tileSet.tileModels.indexOf(m); + tileSet.tileModels.remove(m); super.fireTableRowsDeleted(row, row); } - view.setCanBegin(!tileModels.isEmpty()); - saveData(); } public List getSelectedTiles() { - return tileModels.stream() + return + tileSet.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 File getChooserDir() { + return parent.fileChooserDir; } - public void saveData() { - SaveData data = new SaveData(tileModels, fileChooserDir); - try { - Util.saveData(data); - } catch (IOException e) {} + public void setChooserDir(File newDir) { + parent.setFileChooserDir(newDir); + } + + public void submitChanges() { + listener.onSubmitTileSet(tileSet); + } + + public boolean isClean() { + return tileSet.equals(originalValue); } @Override public int getRowCount() { - return tileModels.size(); + return tileSet.tileModels.size(); } @Override @@ -105,18 +117,17 @@ public class TileSelectController extends AbstractTableModel { @Override public Object getValueAt(int row, int col) { - TileConfigModel m = tileModels.get(row); + TileConfigModel m = tileSet.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); + TileConfigModel m = tileSet.tileModels.get(row); if (col == 0) { m.selected = (boolean) value; } else if (col == 2) { m.multiplicity = (int) value; - saveData(); } else { throw new AssertionError(); } @@ -134,41 +145,46 @@ public class TileSelectController extends AbstractTableModel { } -class TileSelectView { - protected final TileSelectController controller; - protected final JTable grid; - protected final JButton beginButton; - public final JFrame window; +class TileSetEditSwingView { + protected final TileSetEditController controller; + protected JTextField nameField; + protected JTable grid; + protected TableCellEditor multColumnEditor; + public JOptionPane optionPane; + public JDialog dialog; - public TileSelectView(TileSelectController controller) { + public TileSetEditSwingView(TileSetEditController controller, Component dialogParent) { 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; + populateUi(dialogParent); } private static TileConfigModel tryTileBuild(File imageFile) { try { - return new TileConfigModel(imageFile, TileSelectController.DEFAULT_MULTIPLICITY); + return new TileConfigModel(imageFile, TileSetEditController.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()); + protected void populateUi(Component dialogParent) { + grid = new JTable(controller); + grid.setRowHeight(TileSetEditController.THUMBNAIL_HEIGHT); + JSpinner spinner = new JSpinner( + new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1) + ); + multColumnEditor = new JSpinnerTableCellEditor(spinner); + grid.getColumnModel().getColumn(2).setCellEditor(multColumnEditor); + + JPanel panel = new JPanel(new GridBagLayout()); + + nameField = new JTextField(controller.originalValue.name); + GridBagConstraints nameFieldC = new GridBagConstraints(); + nameFieldC.gridx = 0; + nameFieldC.gridy = GridBagConstraints.RELATIVE; + nameFieldC.fill = GridBagConstraints.HORIZONTAL; + nameFieldC.insets = new Insets(0, 0, 10, 0); + nameFieldC.ipady = 5; + panel.add(nameField, nameFieldC); JScrollPane tilesScrollPane = new JScrollPane(grid); tilesScrollPane.setPreferredSize(new Dimension(400, 150)); @@ -179,7 +195,7 @@ class TileSelectView { tilesPaneC.fill = GridBagConstraints.BOTH; tilesPaneC.weightx = 1; tilesPaneC.weighty = 1; - frame.add(tilesScrollPane, tilesPaneC); + panel.add(tilesScrollPane, tilesPaneC); JButton deleteBtn = new JButton("Delete selected"); deleteBtn.addActionListener(e -> { @@ -188,24 +204,24 @@ class TileSelectView { JButton addBtn = new JButton("Add…"); addBtn.addActionListener(e -> { - JFileChooser chooser = new JFileChooser(controller.fileChooserDir); + JFileChooser chooser = new JFileChooser(controller.getChooserDir()); chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); chooser.setMultiSelectionEnabled(true); int result = chooser.showOpenDialog(addBtn); - controller.fileChooserDir = chooser.getCurrentDirectory(); - controller.saveData(); + controller.setChooserDir(chooser.getCurrentDirectory()); if (result == JFileChooser.APPROVE_OPTION) { File[] files = chooser.getSelectedFiles(); List newModels = Arrays.stream(files) - .map(TileSelectView::tryTileBuild) + .map(TileSetEditSwingView::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 + panel, "Failed to load one or more files as images", "Image load error", JOptionPane.WARNING_MESSAGE ); } - List okay = newModels.stream() + List okay = + newModels.stream() .filter(m -> m != null) .collect(Collectors.toList()) ; @@ -213,30 +229,31 @@ class TileSelectView { } }); - 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); + btnBarC.insets = new Insets(10, 0, 5, 0); + panel.add(buttonRow, btnBarC); + + optionPane = new JOptionPane(panel, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION); + dialog = optionPane.createDialog(dialogParent, "Tile set editor"); + dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + } - frame.pack(); - return frame; + public void finishEditing() { + multColumnEditor.stopCellEditing(); } - public void setCanBegin(boolean canBegin) { - beginButton.setEnabled(canBegin); + public void submitChanges() { + controller.tileSet.name = nameField.getText(); + controller.submitChanges(); } } diff --git a/src/net/jcornell/tile_draw/TileSetsController.java b/src/net/jcornell/tile_draw/TileSetsController.java new file mode 100644 index 0000000..aa88821 --- /dev/null +++ b/src/net/jcornell/tile_draw/TileSetsController.java @@ -0,0 +1,243 @@ +package net.jcornell.tile_draw; + +import javax.swing.JComponent; +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JDialog; +import javax.swing.JScrollPane; +import javax.swing.JFrame; +import java.util.stream.Collectors; +import javax.swing.ListSelectionModel; +import javax.swing.JOptionPane; +import java.util.List; +import java.util.ArrayList; +import java.io.Serializable; +import java.io.File; +import java.io.IOException; +import java.awt.BorderLayout; +import java.awt.Dialog; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import net.jcornell.tile_draw.TileSetsController.TileSet; +import net.jcornell.tile_draw.util.Util; + + +public class TileSetsController { + public static class TileSet implements Serializable { + private static final long serialVersionUID = 0; + + public String name; + public List tileModels; + + public TileSet(String name, List tileModels) { + this.name = name; + this.tileModels = tileModels; + } + + public TileSet copy() { + return new TileSet(name, tileModels.stream().map(TileConfigModel::copy).collect(Collectors.toList())); + } + + @Override public boolean equals(Object otherObj) { + TileSet other = (TileSet) otherObj; + return other.name.equals(name) && other.tileModels.equals(tileModels); + } + } + + protected final List tileSets; + protected TileSetsSwingView view; + public File fileChooserDir; + + public TileSetsController(List tileSets, File fileChooserDir) { + this.tileSets = tileSets; + this.fileChooserDir = fileChooserDir; + } + + public void addSet(TileSet set) { + tileSets.add(set); + view.addSet(set); + saveData(); + } + + protected void setSetAt(int index, TileSet value) { + tileSets.set(index, value); + view.setSetAt(index, value); + saveData(); + } + + public void deleteSetAt(int index) { + tileSets.remove(index); + view.deleteSetAt(index); + saveData(); + } + + public void saveData() { + try { + Util.saveData(new SaveData(tileSets, fileChooserDir)); + } catch (IOException e) { + view.onSaveFailed(e); + throw new RuntimeException(e); + } + } + + public void setFileChooserDir(File value) { + fileChooserDir = value; + saveData(); + } + + public void beginEdit(int setIndex) { + TileSetEditController.EventListener listener = new TileSetEditController.EventListener() { + @Override public void onSubmitTileSet(TileSet value) { + setSetAt(setIndex, value); + } + }; + + TileSet target = tileSets.get(setIndex); + TileSetEditController subController = new TileSetEditController(target, listener, this); + TileSetEditSwingView subView = view.getEditView(subController); + subController.view = subView; + view.doEdit(subView); + } + + public void beginDraw(int setIndex) { + DrawController c = new DrawController(tileSets.get(setIndex).tileModels); + c.drawTile(); + Dialog dialog = c.view.dialog; + dialog.pack(); + dialog.setResizable(false); + dialog.setLocationRelativeTo(view.contentPane); + dialog.setVisible(true); + } +} + + +class TileSetsSwingView { + protected final TileSetsController controller; + protected final DefaultListModel viewModel; + protected final JList setList; + protected final JButton deleteButton, editButton, beginButton; + + public final JComponent contentPane; + + protected static class TileSetWrapper { + protected final TileSet tileSet; + + public TileSetWrapper(TileSet tileSet) { + this.tileSet = tileSet; + } + + public String toString() { + return tileSet.name; + } + } + + public TileSetsSwingView(TileSetsController controller) { + this.controller = controller; + viewModel = new DefaultListModel<>(); + for (TileSet tileSet : controller.tileSets) { + viewModel.addElement(new TileSetWrapper(tileSet)); + } + setList = new JList<>(viewModel); + setList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + contentPane = new JPanel(new BorderLayout()); + JPanel buttonBar = new JPanel(); + + JButton addButton = new JButton("Add"); + addButton.addActionListener(e -> { + String newName = (String) JOptionPane.showInputDialog( + contentPane, + "Enter a name for the new tile set", + "New tile set", + JOptionPane.PLAIN_MESSAGE + ); + if (newName != null) { + controller.addSet(new TileSet(newName, new ArrayList<>())); + } + }); + buttonBar.add(addButton); + + deleteButton = new JButton("Delete"); + deleteButton.addActionListener(e -> { + int sel = setList.getSelectedIndex(); + if (sel != -1) { + int response = JOptionPane.showConfirmDialog( + contentPane, "Delete this tile set?", "Confirm delete", + JOptionPane.OK_CANCEL_OPTION + ); + if (response == JOptionPane.OK_OPTION) { + controller.deleteSetAt(sel); + } + } + }); + buttonBar.add(deleteButton); + + editButton = new JButton("Edit"); + editButton.addActionListener(e -> { + controller.beginEdit(setList.getSelectedIndex()); + }); + buttonBar.add(editButton); + + beginButton = new JButton("Begin"); + beginButton.addActionListener(e -> { + controller.beginDraw(setList.getSelectedIndex()); + }); + buttonBar.add(beginButton); + + setList.addListSelectionListener(e -> updateButtonStates()); + + JScrollPane setPane = new JScrollPane(setList); + + contentPane.add(setPane, BorderLayout.CENTER); + contentPane.add(buttonBar, BorderLayout.PAGE_END); + updateButtonStates(); + } + + public void addSet(TileSet set) { + viewModel.addElement(new TileSetWrapper(set)); + } + + public void setSetAt(int index, TileSet value) { + viewModel.set(index, new TileSetWrapper(value)); + updateButtonStates(); + } + + public void deleteSetAt(int index) { + viewModel.removeElementAt(index); + } + + public TileSetEditSwingView getEditView(TileSetEditController subController) { + return new TileSetEditSwingView(subController, contentPane); + } + + public void doEdit(TileSetEditSwingView editView) { + editView.dialog.setVisible(true); + editView.finishEditing(); + Object value = editView.optionPane.getValue(); + if (value != null && (int) value == JOptionPane.OK_OPTION) { + editView.submitChanges(); + } + } + + public void updateButtonStates() { + int selIndex = setList.getSelectedIndex(); + boolean hasSelection = selIndex != -1; + deleteButton.setEnabled(hasSelection); + editButton.setEnabled(hasSelection); + + boolean canBegin = hasSelection && !controller.tileSets.get(selIndex).tileModels.isEmpty(); + beginButton.setEnabled(canBegin); + } + + public void onSaveFailed(Throwable exception) { + JOptionPane.showMessageDialog( + contentPane, + "Error saving data: " + exception.toString(), + "Save error", + JOptionPane.ERROR_MESSAGE + ); + } +} diff --git a/src/net/jcornell/tile_draw/util/Util.java b/src/net/jcornell/tile_draw/util/Util.java index 225c45a..a890411 100644 --- a/src/net/jcornell/tile_draw/util/Util.java +++ b/src/net/jcornell/tile_draw/util/Util.java @@ -21,6 +21,7 @@ import javax.swing.JOptionPane; import net.jcornell.tile_draw.SaveData; import net.jcornell.tile_draw.TileConfigModel; +import net.jcornell.tile_draw.TileSetsController.TileSet; public final class Util { @@ -93,12 +94,15 @@ public final class Util { ); public static SaveData loadData() throws IOException, ClassNotFoundException, Util.ImageLoadException { + Files.createDirectories(SAVE_LOCATION.getParent()); SaveData data; try (InputStream input = Files.newInputStream(SAVE_LOCATION)) { data = (SaveData) new ObjectInputStream(input).readObject(); } - for (TileConfigModel m : data.tileModels) { - m.loadImage(); + for (TileSet set : data.tileSets) { + for (TileConfigModel m : set.tileModels) { + m.loadImage(); + } } return data; } -- 2.30.2