/******************************************************************************* * Breakout Cave Survey Visualizer * * Copyright (C) 2014 James Edwards * * jedwards8 at fastmail dot fm * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *******************************************************************************/ package org.andork.swing.text; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; /** * A simple base class for more specialized editors that displays a read-only * view of the model's current value with a <code>JTextField<code>. Subclasses * can configure the <code>JTextField<code> to create * an editor that's appropriate for the type of model they * support and they may want to override * the <code>stateChanged</code> and <code>propertyChanged</code> methods, which * keep the model and the text field in sync. * <p> * This class defines a <code>dismiss</code> method that removes the editors * <code>ChangeListener</code> from the <code>JSpinner</code> that it's part of. * The <code>setEditor</code> method knows about * <code>DefaultEditor.dismiss</code>, so if the developer replaces an editor * that's derived from <code>JSpinner.DefaultEditor</code> its * <code>ChangeListener</code> connection back to the <code>JSpinner</code> will * be removed. However after that, it's up to the developer to manage their * editor listeners. Similarly, if a subclass overrides * <code>createEditor</code>, it's up to the subclasser to deal with their * editor subsequently being replaced (with <code>setEditor</code>). We expect * that in most cases, and in editor installed with <code>setEditor</code> or * created by a <code>createEditor</code> override, will not be replaced anyway. * <p> * This class is the <code>LayoutManager<code> for it's single * <code>JTextField</code> child. By default the child is just centered with the * parents insets. */ public class SimpleSpinnerEditor extends JPanel implements ChangeListener, PropertyChangeListener, LayoutManager { /** * An Action implementation that is always disabled. */ private static class DisabledAction implements Action { @Override public void actionPerformed(ActionEvent ae) { } @Override public void addPropertyChangeListener(PropertyChangeListener l) { } @Override public Object getValue(String key) { return null; } @Override public boolean isEnabled() { return false; } @Override public void putValue(String key, Object value) { } @Override public void removePropertyChangeListener(PropertyChangeListener l) { } @Override public void setEnabled(boolean b) { } } /** * */ private static final long serialVersionUID = 5806587753282136554L; private static final Action DISABLED_ACTION = new DisabledAction(); private boolean updatingField; /** * Constructs an editor component for the specified <code>JSpinner</code>. * This <code>DefaultEditor</code> is it's own layout manager and it is * added to the spinner's <code>ChangeListener</code> list. The constructor * creates a single <code>JTextField<code> child, * initializes it's value to be the spinner model's current value * and adds it to <code>this</code> <code>DefaultEditor</code>. * * @param spinner * the spinner whose model <code>this</code> editor will monitor * @see #getTextField * @see JSpinner#addChangeListener */ public SimpleSpinnerEditor(JSpinner spinner) { super(null); JTextField ftf = new JTextField(); ftf.setName("Spinner.textField"); ftf.putClientProperty("value", spinner.getValue()); ftf.addPropertyChangeListener(this); ftf.setHorizontalAlignment(SwingConstants.RIGHT); String toolTipText = spinner.getToolTipText(); if (toolTipText != null) { ftf.setToolTipText(toolTipText); } add(ftf); setLayout(this); spinner.addChangeListener(this); // We want the spinner's increment/decrement actions to be // active vs those of the JTextField. As such we // put disabled actions in the JTextField's actionmap. // A binding to a disabled action is treated as a nonexistant // binding. ActionMap ftfMap = ftf.getActionMap(); if (ftfMap != null) { ftfMap.put("increment", DISABLED_ACTION); ftfMap.put("decrement", DISABLED_ACTION); } } /** * This <code>LayoutManager</code> method does nothing. We're only managing * a single child and there's no support for layout constraints. * * @param name * ignored * @param child * ignored */ @Override public void addLayoutComponent(String name, Component child) { } /** * Disconnect <code>this</code> editor from the specified * <code>JSpinner</code>. By default, this method removes itself from the * spinners <code>ChangeListener</code> list. * * @param spinner * the <code>JSpinner</code> to disconnect this editor from; the * same spinner as was passed to the constructor. */ public void dismiss(JSpinner spinner) { spinner.removeChangeListener(this); } /** * Returns the <code>JSpinner</code> ancestor of this editor or null. * Typically the editor's parent is a <code>JSpinner</code> however * subclasses of <codeJSpinner</code> may override the the * <code>createEditor</code> method and insert one or more containers * between the <code>JSpinner</code> and it's editor. * * @return <code>JSpinner</code> ancestor * @see JSpinner#createEditor */ public JSpinner getSpinner() { for (Component c = this; c != null; c = c.getParent()) { if (c instanceof JSpinner) { return (JSpinner) c; } } return null; } /** * Returns the <code>JTextField</code> child of this editor. By default the * text field is the first and only child of editor. * * @return the <code>JTextField</code> that gives the user access to the * <code>SpinnerDateModel's</code> value. * @see #getSpinner * @see #getModel */ public JTextField getTextField() { return (JTextField) getComponent(0); } /** * Returns the size of the parents insets. */ private Dimension insetSize(Container parent) { Insets insets = parent.getInsets(); int w = insets.left + insets.right; int h = insets.top + insets.bottom; return new Dimension(w, h); } /** * Resize the one (and only) child to completely fill the area within the * parents insets. */ @Override public void layoutContainer(Container parent) { if (parent.getComponentCount() > 0) { Insets insets = parent.getInsets(); int w = parent.getWidth() - (insets.left + insets.right); int h = parent.getHeight() - (insets.top + insets.bottom); getComponent(0).setBounds(insets.left, insets.top, w, h); } } /** * Returns the minimum size of first (and only) child plus the size of the * parents insets. * * @param parent * the Container that's managing the layout * @return the minimum dimensions needed to lay out the subcomponents of the * specified container. */ @Override public Dimension minimumLayoutSize(Container parent) { Dimension minimumSize = insetSize(parent); if (parent.getComponentCount() > 0) { Dimension childSize = getComponent(0).getMinimumSize(); minimumSize.width += childSize.width; minimumSize.height += childSize.height; } return minimumSize; } /** * Returns the preferred size of first (and only) child plus the size of the * parents insets. * * @param parent * the Container that's managing the layout * @return the preferred dimensions to lay out the subcomponents of the * specified container. */ @Override public Dimension preferredLayoutSize(Container parent) { Dimension preferredSize = insetSize(parent); if (parent.getComponentCount() > 0) { Dimension childSize = getComponent(0).getPreferredSize(); preferredSize.width += childSize.width; preferredSize.height += childSize.height; } return preferredSize; } /** * Called by the <code>JTextField</code> <code>PropertyChangeListener</code> * . When the <code>"value"</code> property changes, which implies that the * user has typed a new number, we set the value of the spinners model. * <p> * This class ignores <code>PropertyChangeEvents</code> whose source is not * the <code>JTextField</code>, so subclasses may safely make * <code>this</code> <code>DefaultEditor</code> a * <code>PropertyChangeListener</code> on other objects. * * @param e * the <code>PropertyChangeEvent</code> whose source is the * <code>JTextField</code> created by this class. * @see #getTextField */ @Override public void propertyChange(PropertyChangeEvent e) { if (updatingField) { return; } JSpinner spinner = getSpinner(); if (spinner == null) { // Indicates we aren't installed anywhere. return; } Object source = e.getSource(); String name = e.getPropertyName(); if (source instanceof JTextField && "value".equals(name)) { Object lastValue = spinner.getValue(); // Try to set the new value try { spinner.setValue(getTextField().getClientProperty("value")); } catch (IllegalArgumentException iae) { // SpinnerModel didn't like new value, reset try { updateField(lastValue); } catch (IllegalArgumentException iae2) { // Still bogus, nothing else we can do, the // SpinnerModel and JTextField are now out // of sync. } } } } /** * This <code>LayoutManager</code> method does nothing. There isn't any * per-child state. * * @param child * ignored */ @Override public void removeLayoutComponent(Component child) { } /** * This method is called when the spinner's model's state changes. It sets * the <code>value</code> of the text field to the current value of the * spinners model. * * @param e * not used * @see #getTextField * @see JSpinner#getValue */ @Override public void stateChanged(ChangeEvent e) { JSpinner spinner = (JSpinner) e.getSource(); updateField(spinner.getValue()); } private void updateField(Object value) { updatingField = true; try { getTextField().putClientProperty("value", value); } finally { updatingField = false; } } }