package org.erikaredmark.monkeyshines.editor; import java.awt.Color; import java.awt.Component; import java.awt.FlowLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.List; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JToggleButton; import javax.swing.border.EtchedBorder; import org.erikaredmark.monkeyshines.World; import org.erikaredmark.monkeyshines.editor.TemplateEditor.TemplatePair; import org.erikaredmark.monkeyshines.editor.model.Template; import org.erikaredmark.monkeyshines.editor.model.TemplateUtils; import org.erikaredmark.monkeyshines.menu.MenuUtils; import org.erikaredmark.util.swing.layout.WrapLayout; import com.google.common.base.Function; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; /** * * Displays a collection of templates. Allows the user to select existing templates for a world, as well as add new ones or * remove some. A LevelDrawingCanvas reference is used to set the brush type to 'Template' when this panel is interacted with, and * provides the canvas with the currently active canvas * <p/> * To the side, an editor is available that allows existing templates to be modified from the palette. it is up to client code, however, * to actually 'save' this palette to a file. * <p/> * The order that the templates are displayed in the GUI (component order) is their logical ordering, which is basically whatever * comes first in the initial list. This control strives to ensure a logical ordering for the user and that editing existing templates * does not change the ordering. * * @author Erika Redmark * */ @SuppressWarnings("serial") public final class TemplatePalette extends JPanel { /** * * Initialises the palette against a primary canvas with a list of initial templates, typically taken from * some save file. An empty list may be passed but never {@code null} * * @param mainCanvas * reference to the main canvas so this palette can set the brush types when interacted with * * @param initialTemplates * listing of the initial templates to display * * @param world * world for rendering and editing the templates * * @param templateSaveAction * function that is called with a list of templates that the client should somehow save. Used when the palette * is modified by the user after creation * */ public TemplatePalette(final LevelDrawingCanvas mainCanvas, final List<Template> initialTemplates, final World world, final Function<List<Template>, Void> templateSaveAction) { // We must store this info, as we can dynamically create new template buttons and should have this already available. this.world = world; this.mainCanvas = mainCanvas; this.setLayout(new GridBagLayout() ); templateViewer = new JPanel(); templateViewer.setLayout(new WrapLayout(FlowLayout.LEFT, GRID_MARGIN_X, GRID_MARGIN_Y) ); for (Template t : initialTemplates) { addTemplate(t, false); } JScrollPane viewerScroller = new JScrollPane(templateViewer); // template viewer will take up 60% of the left side and whatever it can top/down GridBagConstraints viewerScrollerGbc = new GridBagConstraints(); viewerScrollerGbc.gridx = 0; viewerScrollerGbc.gridy = 0; viewerScrollerGbc.gridwidth = 6; viewerScrollerGbc.gridheight = 1; viewerScrollerGbc.weightx = 2.0; viewerScrollerGbc.weighty = 4.0; viewerScrollerGbc.fill = GridBagConstraints.BOTH; viewerScroller.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED) ); add(viewerScroller, viewerScrollerGbc); // Control: The ability to add new templates, edit templates, and delete templates. JPanel controlViewer = new JPanel(); controlViewer.setLayout(new FlowLayout() ); // Controls rest at the bottom left. Take the smallest space GridBagConstraints controlViewerGbc = new GridBagConstraints(); controlViewerGbc.gridx = 0; controlViewerGbc.gridy = 1; controlViewerGbc.gridwidth = 6; controlViewerGbc.gridheight = 1; controlViewerGbc.weightx = 6.0; controlViewerGbc.weighty = 1.0; add(controlViewer, controlViewerGbc); JButton addTemplate = new JButton("New"); addTemplate.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { templateEditor.clearTemplate(); } }); controlViewer.add(addTemplate); // Logic will require that only edit or remove may be toggled at any one time. Toggling one // enables or disables the other. There is no other way to change state in the palette. // Note that action listener is called AFTER state change, not BEFORE. final JToggleButton editTemplate = new JToggleButton("Edit"); controlViewer.add(editTemplate); final JToggleButton removeTemplate = new JToggleButton("Remove"); controlViewer.add(removeTemplate); JButton saveAll = new JButton("Save Templates"); controlViewer.add(saveAll); editTemplate.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { if (editTemplate.isSelected() ) { currentState = State.EDITING; removeTemplate.setEnabled(false); } else { currentState = State.PLACING; removeTemplate.setEnabled(true); } } }); removeTemplate.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { if (removeTemplate.isSelected() ) { currentState = State.DELETING; editTemplate.setEnabled(false); } else { currentState = State.PLACING; editTemplate.setEnabled(true); } } }); saveAll.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { // We generate the list of templates from how they are ordered on the GUI, which is consistent // with how they are ordered in XML and is kept consistent via proper ordering when replacing existing templates. List<Template> templatesToSave = new ArrayList<>(); BiMap<JButton, Template> buttonToTemplate = templateToButton.inverse(); Object treeLock = templateViewer.getTreeLock(); synchronized (treeLock) { for (int i = 0; i < templateViewer.getComponentCount(); ++i) { Component button = templateViewer.getComponent(i); Template t = buttonToTemplate.get(button); if (t != null) templatesToSave.add(t); } } templateSaveAction.apply(templatesToSave); } }); // Finally, on the far right side of the palette is the editor for modifying templates. templateEditor = new TemplateEditor( world, new Function<TemplatePair, Void>() { @Override public Void apply(TemplatePair pair) { // Check if the baseTemplate is null. If so, add the new one. Otherwise, replace. if (pair.base == null) { addTemplate(pair.modified, true); } else { replaceTemplate(pair.base, pair.modified); } return null; } }); templateEditor.setBorder( BorderFactory.createLineBorder(Color.BLUE) ); // template editor takes up entire right side 40% GridBagConstraints templateEditorGbc = new GridBagConstraints(); templateEditorGbc.gridx = 6; templateEditorGbc.gridy = 0; templateEditorGbc.gridwidth = 4; templateEditorGbc.gridheight = 2; templateEditorGbc.weightx = 4.0; templateEditorGbc.weighty = 1.0; add(templateEditor, templateEditorGbc); } private JButton createTemplateButton(final Template template) { JButton templateButton = new JButton(new ImageIcon(TemplateUtils.renderTemplate(template, world.getResource() ) ) ); MenuUtils.renderImageOnly(templateButton); MenuUtils.removeMargins(templateButton); templateButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { switch (currentState) { case PLACING: mainCanvas.setTemplateBrush(template); break; case EDITING: templateEditor.replaceTemplate(template); break; case DELETING: removeTemplate(template); break; } } }); return templateButton; } /** * * Adds the given template to the palette so it may be placed. The template is added to the end of the palette. * <p/> * This method does nothing if the palette already has a button for the given template * * @param template * new template to add to palette * * @param repaint * if {@code true}, components are re-painted and layed out. Constructor will call this with false * and other code will use true. * * @return * {@code true} if the template was added, {@code false} if a button for it already existed and it wasn't added * */ private boolean addTemplate(Template template, boolean repaint) { // Do not allow duplicate templates. if it already existed do not add a button for it. if (templateToButton.containsKey(template) ) return false; JButton templateButton = createTemplateButton(template); templateViewer.add(templateButton); templateToButton.put(template, templateButton); if (repaint) { getParent().revalidate(); getParent().repaint(); } return true; } /** * * Removes the given template from the palette. * * @param template * template to remove. * * @return * {@code true} if the template was removed, {@code false} if it never existed and thus was not removed * */ private boolean removeTemplate(Template template) { if (templateToButton.containsKey(template) ) { JButton oldButton = templateToButton.get(template); templateViewer.remove(oldButton); templateToButton.remove(template); getParent().revalidate(); getParent().repaint(); return true; } else { return false; } } /** * * Replaces the given template with a new one. Unlike remove/add, this preserves the location in the GUI and list. * * @param oldTemplate * old template to replace * * @param newTemplate * new template to add * * @return * {@code true} if the template was replaced, {@code false} if otherwise. * */ private boolean replaceTemplate(Template oldTemplate, Template newTemplate) { if (templateToButton.containsKey(oldTemplate) ) { JButton oldButton = templateToButton.get(oldTemplate); // We must find the 'index' of this button so we can properly update the GUI. int index = -1; Object treeLock = templateViewer.getTreeLock(); synchronized (treeLock) { for (int i = 0; i < templateViewer.getComponentCount(); ++i) { // Reference equality intended if (templateViewer.getComponent(i) == oldButton) { index = i; break; } } } templateViewer.remove(oldButton); JButton templateButton = createTemplateButton(newTemplate); templateViewer.add(templateButton, index); templateToButton.put(newTemplate, templateButton); getParent().revalidate(); getParent().repaint(); return true; } else { return false; } } // Affects what the result of clicking on a template will be. private enum State { PLACING, EDITING, DELETING; } /** * * Attempts to set the template brush for the template editor. Does nothing if the brush is incompatable * * @param brush * @param id */ public void trySetTileIdAndBrush(PaintbrushType brush, int id) { templateEditor.trySetTileIdAndBrush(brush, id); } private State currentState = State.PLACING; // Maps a template to the button controlling that template. Keeps track of all template buttons so that // the palette may be modified after construction with new/removed templates. private final BiMap<Template, JButton> templateToButton = HashBiMap.create(); // Immutable state information private final LevelDrawingCanvas mainCanvas; private final World world; private final TemplateEditor templateEditor; private final JPanel templateViewer; private static final int GRID_MARGIN_X = 14; private static final int GRID_MARGIN_Y = 14; }