/** * */ package javax.swing.origamist; import java.awt.Component; import java.awt.Container; import java.awt.Dialog; import java.awt.FlowLayout; import java.awt.Frame; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.MessageFormat; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map.Entry; import java.util.ResourceBundle; import javax.swing.AbstractAction; import javax.swing.AbstractButton; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; import javax.swing.UIManager; import javax.swing.event.DocumentEvent; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; import cz.cuni.mff.peckam.java.origamist.math.AngleUnit; import cz.cuni.mff.peckam.java.origamist.math.MathHelper; import cz.cuni.mff.peckam.java.origamist.services.ServiceLocator; import cz.cuni.mff.peckam.java.origamist.services.interfaces.ConfigurationManager; import cz.cuni.mff.peckam.java.origamist.utils.UniversalDocumentListener; /** * A dialog for selecting angles. * * @author Martin Pecka */ public class AngleSelectionDialog extends JDialog { /** */ private static final long serialVersionUID = 3740375500491682514L; /** The currently selected unit. */ public static final String UNIT_PROPERTY = "unit"; /** The message to display. */ protected Object message = ""; /** The selected angle. */ protected Double angle = null; /** The value (in radians) of the input field, if any. */ protected Double customValue = null; /** The selected unit. */ protected AngleUnit unit = null; /** The default unit. */ protected AngleUnit defaultUnit; /** The angles that will be suggested as radio buttons (in radians). */ protected final Double[] suggestedAngles = new Double[] { 0d, Math.PI / 2d, Math.PI }; /** The default value. */ protected Double defaultValue; /** The lower/upper bound for the entered angle. If <code>null</code>, no bound is applied. */ protected Double lowerBound, upperBound; /** The label that displays the current bounds. */ protected JLabel boundsLabel; /** The text field for inputting custom angles. */ protected JTextField inputField; /** The label that shows custom angle units after the input field. */ protected JLabel afterInputLabel; /** Group for angle unit radio buttons. */ protected ButtonGroup angleUnitGroup; /** Angle unit radio button. */ protected JRadioButton degreesBtn, radsBtn, gradsBtn; /** Group for angle values. */ protected ButtonGroup valuesGroup; /** The radio buttons for suggested values. Keys are the suggested angles in radians. */ protected HashMap<Double, JRadioButton> suggestedValues = new LinkedHashMap<Double, JRadioButton>( suggestedAngles.length); /** The radio button for custom angle selection. */ protected JRadioButton customAngle; /** Buttons for submitting the dialog. */ protected JButton okButton, cancelButton; /** If the dialog has been closed with the OK button, this is true. */ protected boolean closedByOKButton = false; /** The resource bundle this class can use. */ protected ResourceBundle messages = ResourceBundle.getBundle(getClass().getName(), ServiceLocator.get(ConfigurationManager.class) .get().getLocale()); /** * Create a dialog with the defaults set to 90 degrees. * * @param owner The owner of this dialog. * @param message The message to display. * @param title Title of the dialog window. */ public AngleSelectionDialog(Window owner, Object message, String title) { this(owner, message, title, AngleUnit.DEGREE, 90d, null, null); } /** * Create the dialog. * <p> * The <em>normalized</em> (using {@link AngleUnit#normalize(double)}) entered value is tested against the given * bounds. * * @param owner The owner of this dialog. * @param message The message to display. * @param title Title of the dialog window. * @param defaultUnit The default unit. If <code>null</code>, degrees will be selected. * @param defaultValue The default value. If <code>null</code>, the custom field will be focused and empty. */ public AngleSelectionDialog(Window owner, Object message, String title, AngleUnit defaultUnit, Double defaultValue, Double lowerBound, Double upperBound) { super(owner, title, Dialog.ModalityType.APPLICATION_MODAL); this.message = message; this.defaultValue = defaultValue; this.defaultUnit = defaultUnit; init(); } /** * Create a dialog with the defaults set to 90 degrees. * * @param owner The owner of this dialog. * @param message The message to display. * @param title Title of the dialog window. */ public AngleSelectionDialog(Frame owner, Object message, String title) { this(owner, message, title, AngleUnit.DEGREE, 90d); } /** * @param owner The owner of this dialog. * @param message The message to display. * @param title Title of the dialog window. * @param defaultUnit The default unit. If <code>null</code>, degrees will be selected. * @param defaultValue The default value. If <code>null</code>, the custom field will be focused and empty. */ public AngleSelectionDialog(Frame owner, Object message, String title, AngleUnit defaultUnit, Double defaultValue) { super(owner, title, Dialog.ModalityType.APPLICATION_MODAL); this.message = message; this.defaultValue = defaultValue; this.defaultUnit = defaultUnit; init(); } /** * Create a dialog with the defaults set to 90 degrees. * * @param owner The owner of this dialog. * @param message The message to display. * @param title Title of the dialog window. */ public AngleSelectionDialog(Dialog owner, Object message, String title) { this(owner, message, title, AngleUnit.DEGREE, 90d); } /** * @param owner The owner of this dialog. * @param message The message to display. * @param title Title of the dialog window. * @param defaultUnit The default unit. If <code>null</code>, degrees will be selected. * @param defaultValue The default value. If <code>null</code>, the custom field will be focused and empty. */ public AngleSelectionDialog(Dialog owner, Object message, String title, AngleUnit defaultUnit, Double defaultValue) { super(owner, title, Dialog.ModalityType.APPLICATION_MODAL); this.message = message; this.defaultValue = defaultValue; this.defaultUnit = defaultUnit; init(); } /** * Create a dialog with the defaults set to 90 degrees. * * @param message The message to display. * @param title Title of the dialog window. */ public AngleSelectionDialog(Object message, String title) { this(message, title, AngleUnit.DEGREE, 90d); } /** * @param message The message to display. * @param title Title of the dialog window. * @param defaultUnit The default unit. If <code>null</code>, degrees will be selected. * @param defaultValue The default value. If <code>null</code>, the custom field will be focused and empty. */ public AngleSelectionDialog(Object message, String title, AngleUnit defaultUnit, Double defaultValue) { super((Frame) null, title, true); this.message = message; this.defaultValue = defaultValue; this.defaultUnit = defaultUnit != null ? defaultUnit : AngleUnit.DEGREE; init(); } /** * Initialize the dialog. */ protected void init() { createComponents(); buildLayout(); setResizable(false); if (angleUnitGroup.getSelection() != null) angleUnitGroup.setSelected(angleUnitGroup.getSelection(), false); AngleUnit preferredUnit = ServiceLocator.get(ConfigurationManager.class).get().getPreferredAngleUnit(); if (preferredUnit == null || preferredUnit == AngleUnit.DEGREE) degreesBtn.doClick(); else if (preferredUnit == AngleUnit.GRAD) gradsBtn.doClick(); else radsBtn.doClick(); if (defaultValue == null) { customAngle.setSelected(true); } else { Double value = defaultUnit.convertTo(defaultValue, AngleUnit.RAD); if (suggestedValues.get(value) == null) { customAngle.setSelected(true); inputField.setText(unit.getNiceValue(defaultValue)); } else { suggestedValues.get(value).doClick(); } inputField.requestFocusInWindow(); } addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { if (!closedByOKButton) new CancelAction().actionPerformed(new ActionEvent(AngleSelectionDialog.this, 0, "cancel")); } }); pack(); setLocationRelativeTo(null); } /** * Set the bounds of the angles that can be entered. * * The entered values will be first normalized (using {@link AngleUnit#normalize(double)}) and then compared to the * given bounds. * * @param lowerBound The lower bound for the entered angle in defaultUnit. If <code>null</code>, no bound is * applied. * @param upperBound The upper bound for the entered angle in defaultUnit. If <code>null</code>, no bound is * applied. * @param unit The unit of the bounds. */ public void setBounds(Double lowerBound, Double upperBound, AngleUnit unit) { this.lowerBound = lowerBound == null ? null : unit.convertTo(lowerBound, AngleUnit.RAD); this.upperBound = upperBound == null ? null : unit.convertTo(upperBound, AngleUnit.RAD); updateBoundsLabel(); } /** * Update the text of the bounds label. */ protected void updateBoundsLabel() { double min = AngleUnit.RAD.convertTo(lowerBound != null ? lowerBound : 0, unit); double max = upperBound == null ? unit.getMaxValue() : AngleUnit.RAD.convertTo(upperBound, unit); String minText = unit.formatValue(min); String maxText = unit.formatValue(max); boundsLabel.setText(MessageFormat.format(messages.getString("boundsLabel"), new Object[] { minText, maxText })); pack(); } /** * Initialize Swing components. */ protected void createComponents() { KeyListener keyListener = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) okButton.doClick(); else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) cancelButton.doClick(); } }; boundsLabel = new JLabel(); addPropertyChangeListener(UNIT_PROPERTY, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { updateBoundsLabel(); } }); angleUnitGroup = new ButtonGroup(); degreesBtn = new JRadioButton(new AngleUnitAction(AngleUnit.DEGREE)); angleUnitGroup.add(degreesBtn); degreesBtn.addKeyListener(new FocusingKeyListener(degreesBtn)); radsBtn = new JRadioButton(new AngleUnitAction(AngleUnit.RAD)); angleUnitGroup.add(radsBtn); radsBtn.addKeyListener(new FocusingKeyListener(radsBtn)); gradsBtn = new JRadioButton(new AngleUnitAction(AngleUnit.GRAD)); angleUnitGroup.add(gradsBtn); gradsBtn.addKeyListener(new FocusingKeyListener(gradsBtn)); valuesGroup = new ButtonGroup(); for (Double angle : suggestedAngles) { final JRadioButton btn = new JRadioButton(new SuggestedAngleAction(angle)); btn.addKeyListener(keyListener); btn.addKeyListener(new FocusingKeyListener(btn)); suggestedValues.put(angle, btn); valuesGroup.add(btn); } customAngle = new JRadioButton(new CustomAngleAction()); valuesGroup.add(customAngle); customAngle.setFocusable(false); inputField = new JTextField(10); inputField.getDocument().addDocumentListener(new UniversalDocumentListener() { @Override protected void update(DocumentEvent e) { String text = inputField.getText(); try { customValue = unit.convertTo(unit.parseValue(text), AngleUnit.RAD); angle = customValue; } catch (NumberFormatException ex) { customValue = null; } } }); inputField.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { if (inputField.getText().length() == 0 && valuesGroup.getSelection() != null && valuesGroup.getSelection() != customAngle.getModel() && angle != null) { inputField.setText(unit.getNiceValue(AngleUnit.RAD.convertTo(angle, unit))); pack(); } if (valuesGroup.getSelection() != customAngle.getModel()) { customAngle.doClick(); inputField.requestFocusInWindow(); } } @Override public void focusLost(FocusEvent e) { if (angle != null && Math.abs(AngleUnit.RAD.normalize(angle) - angle) > MathHelper.EPSILON) inputField.setText(unit.getNiceValue(unit.normalize(AngleUnit.RAD.convertTo(angle, unit)))); } }); inputField.addKeyListener(keyListener); addPropertyChangeListener(UNIT_PROPERTY, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (customValue != null) { double newVal = AngleUnit.RAD.convertTo(customValue, unit); if (Math.abs(Math.rint(newVal) - newVal) < MathHelper.EPSILON) newVal = Math.rint(newVal); inputField.setText(unit.getNiceValue(newVal)); pack(); } } }); afterInputLabel = new JLabel(); addPropertyChangeListener(UNIT_PROPERTY, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { afterInputLabel.setText(unit.getUnit()); pack(); } }); okButton = new JButton(new OKAction()); cancelButton = new JButton(new CancelAction()); } /** * Add the Swing components to a layout. */ protected void buildLayout() { Container content = getContentPane(); content.setLayout(new FormLayout("pref", "pref,$lgap,pref,$lgap,pref,$lgap,pref")); CellConstraints cc = new CellConstraints(); JPanel angleUnits = new JPanel(); content.add(angleUnits, cc.xy(1, 1)); angleUnits.setBorder(BorderFactory.createTitledBorder(messages.getString("units.title"))); angleUnits.add(degreesBtn); angleUnits.add(radsBtn); angleUnits.add(gradsBtn); JPanel angleValues = new JPanel(new FormLayout("pref", "pref,$lgap,pref")); content.add(angleValues, cc.xy(1, 3)); angleValues.setBorder(BorderFactory.createTitledBorder(messages.getString("values.title"))); JPanel suggestedValues = new JPanel(); angleValues.add(suggestedValues, cc.xy(1, 1)); for (Entry<Double, JRadioButton> e : this.suggestedValues.entrySet()) suggestedValues.add(e.getValue()); JPanel customValue = new JPanel(); angleValues.add(customValue, cc.xy(1, 3)); customValue.add(customAngle); customValue.add(inputField); customValue.add(afterInputLabel); content.add(boundsLabel, cc.xy(1, 5)); JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); content.add(buttonsPanel, cc.xy(1, 7)); buttonsPanel.add(okButton); buttonsPanel.add(cancelButton); } /** * Show the dialog, wait for a user action, and then return the seleted angle. * * @return The selected angle, or <code>null</code> if no angle was specified or the dialog was cancelled. */ public Double getAngle() { closedByOKButton = false; setVisible(true); dispose(); return angle != null ? AngleUnit.RAD.normalize(angle) : null; } /** * @return True if the currently selected angle is within the given bounds. */ protected boolean isAngleInBounds() { return (lowerBound == null || angle + MathHelper.EPSILON >= lowerBound) && (upperBound == null || angle <= upperBound + MathHelper.EPSILON); } /** * Action to change the current angle units. * * @author Martin Pecka */ protected class AngleUnitAction extends AbstractAction { /** */ private static final long serialVersionUID = 6938098480887384631L; /** The unit this action represents. */ protected final AngleUnit unit; /** * @param unit The unit this action represents. */ public AngleUnitAction(AngleUnit unit) { super(unit.toString()); this.unit = unit; } @Override public void actionPerformed(ActionEvent e) { AngleUnit oldUnit = AngleSelectionDialog.this.unit; AngleSelectionDialog.this.unit = unit; AngleSelectionDialog.this.firePropertyChange(UNIT_PROPERTY, oldUnit, unit); } } /** * Action for selecting a suggested angle value. * * @author Martin Pecka */ protected class SuggestedAngleAction extends AbstractAction { /** */ private static final long serialVersionUID = -8793340281827504735L; /** The value of this action. */ protected final double value; public SuggestedAngleAction(double value) { super(unit != null ? unit.formatValue(value, AngleUnit.RAD) : Double.toString(value)); this.value = value; AngleSelectionDialog.this.addPropertyChangeListener(UNIT_PROPERTY, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { putValue(Action.NAME, unit.formatValue(SuggestedAngleAction.this.value, AngleUnit.RAD)); pack(); } }); } @Override public void actionPerformed(ActionEvent e) { angle = value; } } /** * Action for selecting that a custom angle will be put in. * * @author Martin Pecka */ protected class CustomAngleAction extends AbstractAction { /** */ private static final long serialVersionUID = 7732332151890591919L; public CustomAngleAction() { super(messages.getString("customAngle.label")); } @Override public void actionPerformed(ActionEvent e) { inputField.requestFocusInWindow(); angle = customValue; } } /** * Confirm the dialog. * * @author Martin Pecka */ protected class OKAction extends AbstractAction { /** */ private static final long serialVersionUID = 8308947712033535447L; public OKAction() { super(UIManager.getString("OptionPane.okButtonText", ServiceLocator.get(ConfigurationManager.class).get() .getLocale())); } @Override public void actionPerformed(ActionEvent e) { okButton.requestFocusInWindow(); // this is important; calling doClick() doesn't request the focus and // therefore the changes made to inputField may not have been updated if (!(valuesGroup.getSelection() == customAngle.getModel() && customValue == null)) { if (isAngleInBounds()) { closedByOKButton = true; ServiceLocator.get(ConfigurationManager.class).get().setPreferredAngleUnit(unit); setVisible(false); } else { JOptionPane.showMessageDialog(null, messages.getString("notInBounds.message"), messages.getString("notInBounds.title"), JOptionPane.ERROR_MESSAGE); } } else { JOptionPane.showMessageDialog(null, messages.getString("badnumber.message"), messages.getString("badnumber.title"), JOptionPane.ERROR_MESSAGE); } } } /** * Cancel the dialog. * * @author Martin Pecka */ protected class CancelAction extends AbstractAction { /** */ private static final long serialVersionUID = 3484909152830981442L; public CancelAction() { super(UIManager.getString("OptionPane.cancelButtonText", ServiceLocator.get(ConfigurationManager.class) .get().getLocale())); } @Override public void actionPerformed(ActionEvent e) { angle = null; setVisible(false); } } /** * A key listener that traverses focus between radio buttons under the same parent using arrow keys. * * @author Martin Pecka */ protected class FocusingKeyListener extends KeyAdapter { /** The button that is the source of this event. */ protected Component button; /** * @param button The button that is the source of this event. */ public FocusingKeyListener(Component button) { this.button = button; } @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_DOWN) { Component comp = button.getFocusCycleRootAncestor().getFocusTraversalPolicy() .getComponentBefore(button.getParent(), button); if (comp instanceof AbstractButton && comp.getParent() == button.getParent()) { ((AbstractButton) comp).doClick(); comp.requestFocusInWindow(); } } else if (e.getKeyCode() == KeyEvent.VK_RIGHT || e.getKeyCode() == KeyEvent.VK_UP) { Component comp = button.getFocusCycleRootAncestor().getFocusTraversalPolicy() .getComponentAfter(button.getParent(), button); if (comp instanceof AbstractButton && comp.getParent() == button.getParent()) { ((AbstractButton) comp).doClick(); comp.requestFocusInWindow(); } } } } }