package org.erikaredmark.monkeyshines.editor.dialog; import java.awt.BorderLayout; import java.awt.Canvas; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.image.BufferedImage; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.plaf.basic.BasicArrowButton; import org.erikaredmark.monkeyshines.World; import org.erikaredmark.monkeyshines.editor.resource.EditorResource; import org.erikaredmark.util.StringToNumber; import com.google.common.base.Optional; /** * * Allows the user to select a level screen in the world. Can be set to allow selection of non-existent * screens or prevent selection of them. * <p/> * The model for each panel is simple; the screen selected. * * @author Erika Redmark * */ public final class SelectScreenPanel extends JPanel { private static final long serialVersionUID = 1L; // The 'model' of the dialog is simply a number. No need for extra classes. private int selectedScreenId; // Level thumbnail: Current id is mapped to a thumbnail to display private final Canvas levelThumbnail; private BufferedImage levelThumbnailToDraw; // Text box containing current screen id private final JTextField screenIdText; // World reference for generating thumbnails private final World world; private final boolean allowNew; /* ---------------- Observable Properties ------------------- */ public static final String PROPERTY_SCREEN_ID = "propId"; public SelectScreenPanel(final int currentId, final World world, final boolean allowNew) { setLayout(new GridBagLayout() ); // -------- Left side: Thumbnail levelThumbnail = new Canvas() { private static final long serialVersionUID = 1L; @Override public void paint(Graphics g) { if (levelThumbnailToDraw == null) return; Graphics2D g2d = (Graphics2D) g; g2d.drawImage(levelThumbnailToDraw, 0, 0, // Dest 1 160, 100, // Dest 2 0, 0, // Src 1 160, 100, // Src 2 null); } }; GridBagConstraints levelThumbnailGbc = new GridBagConstraints(); levelThumbnailGbc.gridx = 0; levelThumbnailGbc.gridy = 0; levelThumbnailGbc.weightx = 1; levelThumbnailGbc.weighty = 1; levelThumbnailGbc.gridheight = 1; levelThumbnailGbc.gridwidth = 1; // Thumbnail size levelThumbnail.setPreferredSize(new Dimension(160, 100) ); // width and height are both /4 of normal game window add(levelThumbnail, levelThumbnailGbc); // -------- RightSide: Level id selection JPanel levelSelect = new JPanel(); levelSelect.setLayout(new BorderLayout() ); GridBagConstraints levelSelectGbc = new GridBagConstraints(); levelThumbnailGbc.gridx = 1; levelThumbnailGbc.gridy = 0; levelThumbnailGbc.weightx = 1; levelThumbnailGbc.weighty = 1; levelThumbnailGbc.gridheight = 1; levelThumbnailGbc.gridwidth = 1; add(levelSelect, levelSelectGbc); // Arrows and id selection BasicArrowButton upArrow = new BasicArrowButton(BasicArrowButton.NORTH); BasicArrowButton rightArrow = new BasicArrowButton(BasicArrowButton.EAST); BasicArrowButton downArrow = new BasicArrowButton(BasicArrowButton.SOUTH); BasicArrowButton leftArrow = new BasicArrowButton(BasicArrowButton.WEST); upArrow.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { modifyIdBy(100); } }); rightArrow.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { modifyIdBy(1); } }); downArrow.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { modifyIdBy(-100); } }); leftArrow.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { modifyIdBy(-1); } }); screenIdText = new JTextField(6); screenIdText.addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { } @Override public void keyReleased(KeyEvent e) { Optional<Integer> userValue = StringToNumber.string2Int(screenIdText.getText() ); if (userValue.isPresent() ) { setIdFromTextField(userValue.get().intValue() ); } else { // TODO show warning icon System.err.println("Non-integer value " + screenIdText.getText() ); } } @Override public void keyTyped(KeyEvent e) { } }); levelSelect.add(rightArrow, BorderLayout.EAST); levelSelect.add(upArrow, BorderLayout.NORTH); levelSelect.add(downArrow, BorderLayout.SOUTH); levelSelect.add(leftArrow, BorderLayout.WEST); levelSelect.add(screenIdText, BorderLayout.CENTER); // Set initial model values this.allowNew = allowNew; this.world = world; // this.originalScreenId = currentId; this.selectedScreenId = currentId; this.screenIdText.setText(String.valueOf(currentId) ); updateThumbnail(); } /** * * Modifies the current screen id this dialog displays. This is called by arrow buttons only, and * it syncs up with the main textbox to reflect the new value, as well as updating the model. * <p/> * The id will NOT change if the given screen does not exist AND the dialog is set to now allow * new screens. * * @param value * amount to change level screen id; may be negative * */ private void modifyIdBy(int value) { int newValue = selectedScreenId + value; if (!(this.allowNew) && !(this.world.screenIdExists(newValue) ) ) { return; } setId(newValue); screenIdText.setText(String.valueOf(selectedScreenId) ); updateThumbnail(); } /** * * Sets the current screen id. This is intended to be called ONLY from the text box so * now view syncing is done; the model value is simply set to the new value. If the * goto screen is set to not allow new screens, this will NOT update the model, and * the thumbnail update will indicate the screen model has not changed * * @param value * */ private void setIdFromTextField(int value) { // Don't update model unless we either allow news, or are looking at existing screen if (this.allowNew || this.world.screenIdExists(value) ) { setId(value); } updateThumbnail(); } /** * Called ONLY after the value has been validated (such as a panel only allowing selection of existing * level screens). * * @param value * */ private void setId(int value) { // TODO if ever required, this is where a property change should be fired. this.selectedScreenId = value; } /** * * Returns the id of the screen that is currently selected. * * @return */ public int getSelectedScreenId() { return this.selectedScreenId; } /** * * Draws the currently selected screen on the canvas. If no screen exists, draws special * NEW image. * */ private void updateThumbnail() { if (this.world.screenIdExists(selectedScreenId) ) { this.levelThumbnailToDraw = EditorResource.generateThumbnailFor(world, selectedScreenId); } else { if (this.allowNew) { this.levelThumbnailToDraw = EditorResource.getNewScreenThumbnail(); } else { this.levelThumbnailToDraw = EditorResource.getNoScreenHereThumbnail(); } } this.levelThumbnail.repaint(); } /* Forwarding methods to IObservable */ /* Issue: Accidentally overriding 'firePropertyChange' in some swing interface which is called during superclass construction, effectively * calling an overridable method in the constructor, leading to observing the 'obs' variable, which should be final and initialised first anyway, * in an unconstructed state. */ /* @Override public void addPropertyChangeListener(PropertyChangeListener listener) { obs.addPropertyChangeListener(listener); }; @Override public void removePropertyChangeListener(PropertyChangeListener listener) { obs.removePropertyChangeListener(listener); }; @Override public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { obs.addPropertyChangeListener(propertyName, listener); } @Override public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { obs.removePropertyChangeListener(propertyName, listener); } @Override public void firePropertyChange(String propertyName, Object oldVal, Object newVal) { obs.firePropertyChange(propertyName, oldVal, newVal); } @Override public void suspendPropertyChangeEvents() { obs.suspendPropertyChangeEvents(); } @Override public void resumePropertyChangeEvents() { obs.resumePropertyChangeEvents(); }*/ }