001    /**
002     * Copyright (c) 2008-2009, Piet Blok
003     * All rights reserved.
004     *
005     * Redistribution and use in source and binary forms, with or without
006     * modification, are permitted provided that the following conditions
007     * are met:
008     *
009     *   * Redistributions of source code must retain the above copyright
010     *     notice, this list of conditions and the following disclaimer.
011     *   * Redistributions in binary form must reproduce the above
012     *     copyright notice, this list of conditions and the following
013     *     disclaimer in the documentation and/or other materials provided
014     *     with the distribution.
015     *   * Neither the name of the copyright holder nor the names of the
016     *     contributors may be used to endorse or promote products derived
017     *     from this software without specific prior written permission.
018     *
019     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
020     * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
021     * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
022     * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
023     * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
024     * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
025     * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
026     * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
027     * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
028     * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
029     * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
030     */
031    
032    package org.pbjar.jxlayer.demo;
033    
034    import java.awt.BorderLayout;
035    import java.awt.Color;
036    import java.awt.Graphics2D;
037    import java.awt.RenderingHints;
038    import java.awt.event.ActionEvent;
039    import java.awt.event.ActionListener;
040    import java.awt.event.MouseWheelEvent;
041    import java.awt.event.MouseWheelListener;
042    import java.awt.geom.Path2D;
043    import java.awt.image.BufferedImage;
044    import java.beans.PropertyChangeEvent;
045    import java.beans.PropertyChangeListener;
046    import java.text.NumberFormat;
047    
048    import javax.swing.AbstractAction;
049    import javax.swing.Action;
050    import javax.swing.BorderFactory;
051    import javax.swing.ImageIcon;
052    import javax.swing.JButton;
053    import javax.swing.JPanel;
054    import javax.swing.border.TitledBorder;
055    
056    /**
057     * A component that maintains a double value that can be changed by the user.
058     * <p>
059     * <ol>
060     * <li>The user may use the mouse wheel over the whole area of the control to
061     * increase or decrease the value.</li>
062     * <li>
063     * An up button is provided to single step up.</li>
064     * <li>
065     * A down button is provided to single step down.</li>
066     * <li>
067     * A button is provided to reset the control to its initial value (this button
068     * displays the current value).</li>
069     * </ol>
070     * </p>
071     * 
072     * @author Piet Blok
073     */
074    public class WheelButton extends JPanel {
075    
076        /**
077         * Describes the increment / decrement type.
078         */
079        public static enum IncrementType {
080            /**
081             * Increment / decrement by a factor. Computation is as follows:
082             * 
083             * <pre>
084             * newValue = current * Math.pow(incrementValue, rotation);
085             * </pre>
086             */
087            Factor,
088            /**
089             * Increment / decrement with a fixed amount. Computation is as follows:
090             * 
091             * <pre>
092             * newValue = current + rotation * incrementValue;
093             * </pre>
094             */
095            Fixed,
096        }
097    
098        private static final ImageIcon upIcon, dnIcon;
099    
100        static {
101            BufferedImage image = new BufferedImage(32, 26,
102                    BufferedImage.TYPE_INT_ARGB);
103            Graphics2D g2 = image.createGraphics();
104            try {
105                Path2D triangle = new Path2D.Double();
106                triangle.moveTo(1, 25);
107                triangle.lineTo(31, 25);
108                triangle.lineTo(16, 1);
109                triangle.closePath();
110                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
111                        RenderingHints.VALUE_ANTIALIAS_ON);
112                g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,
113                        RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
114                g2.setColor(Color.GRAY);
115                g2.fill(triangle);
116                g2.setColor(Color.BLACK);
117                g2.draw(triangle);
118            } finally {
119                g2.dispose();
120            }
121            upIcon = new ImageIcon(image);
122    
123            image = new BufferedImage(32, 26, BufferedImage.TYPE_INT_ARGB);
124            g2 = image.createGraphics();
125            try {
126                Path2D triangle = new Path2D.Double();
127                triangle.moveTo(1, 1);
128                triangle.lineTo(31, 1);
129                triangle.lineTo(16, 25);
130                triangle.closePath();
131                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
132                        RenderingHints.VALUE_ANTIALIAS_ON);
133                g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,
134                        RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
135                g2.setColor(Color.GRAY);
136                g2.fill(triangle);
137                g2.setColor(Color.BLACK);
138                g2.draw(triangle);
139            } finally {
140                g2.dispose();
141            }
142            dnIcon = new ImageIcon(image);
143        }
144    
145        private final double resetValue, incrementValue;
146    
147        private final IncrementType type;
148    
149        /**
150         * The name of the double value bound property.
151         * 
152         * @see PropertyChangeListener
153         */
154        public static final String KEY_CURRENT = WheelButton.class.getName()
155                + ".currentValue";
156    
157        private static final long serialVersionUID = 1L;
158    
159        /**
160         * Construct a {@code WheelButton}.
161         * 
162         * @param name
163         *            <b>required</b> a name that will be displayed in a
164         *            {@link TitledBorder}
165         * @param resetValue
166         *            <b>required</b> the initial value
167         * @param type
168         *            <b>required</b> the {@link IncrementType}
169         * @param incrementValue
170         *            <b>required</b> the increment or factor, depends on the
171         *            specified {@link IncrementType}
172         * @param numberFormat
173         *            a {@code NumberFormat} that will be used to display the
174         *            current value, or {@code null} to display the value as a
175         *            double
176         * @param listener
177         *            a {@link PropertyChangeListener} that will handle changes of
178         *            the {@link #KEY_CURRENT} property, or {@code null}
179         */
180        public WheelButton(String name, double resetValue, IncrementType type,
181                double incrementValue, final NumberFormat numberFormat,
182                PropertyChangeListener listener) {
183            super(new BorderLayout());
184            JPanel content = new JPanel(new BorderLayout());
185            this.add(content);
186            content.setBorder(BorderFactory.createTitledBorder(name));
187            this.resetValue = resetValue;
188            this.incrementValue = incrementValue;
189            this.type = type;
190            this.putClientProperty(KEY_CURRENT, resetValue);
191            content.add(new JButton(new AbstractAction() {
192    
193                private static final long serialVersionUID = 1L;
194    
195                {
196                    this.putValue(Action.SMALL_ICON, upIcon);
197                }
198    
199                @Override
200                public void actionPerformed(ActionEvent e) {
201                    processRotation(1);
202                }
203            }), BorderLayout.PAGE_START);
204            content.add(new JButton(new AbstractAction() {
205    
206                private static final long serialVersionUID = 1L;
207    
208                {
209                    this.putValue(Action.SMALL_ICON, dnIcon);
210                }
211    
212                @Override
213                public void actionPerformed(ActionEvent e) {
214                    processRotation(-1);
215                }
216            }), BorderLayout.PAGE_END);
217            final JButton resetButton = new JButton(
218                    format(numberFormat, resetValue));
219            resetButton.addActionListener(new ActionListener() {
220    
221                @Override
222                public void actionPerformed(ActionEvent e) {
223                    WheelButton.this.putClientProperty(KEY_CURRENT,
224                            WheelButton.this.resetValue);
225                }
226            });
227            content.add(resetButton, BorderLayout.CENTER);
228            this.addPropertyChangeListener(WheelButton.KEY_CURRENT, listener);
229            this.addPropertyChangeListener(WheelButton.KEY_CURRENT,
230                    new PropertyChangeListener() {
231    
232                        @Override
233                        public void propertyChange(PropertyChangeEvent evt) {
234                            resetButton.setText(format(numberFormat, evt
235                                    .getNewValue()));
236                        }
237                    });
238            this.addMouseWheelListener(new MouseWheelListener() {
239    
240                @Override
241                public void mouseWheelMoved(MouseWheelEvent event) {
242                    /*
243                     * To sync with the arrow buttons invert the rotation.
244                     */
245                    processRotation(-event.getWheelRotation());
246                }
247            });
248        }
249    
250        /**
251         * Get the current value. The current value is a bound property.
252         * 
253         * @return the current value
254         * @see #KEY_CURRENT
255         */
256        public double getCurrentValue() {
257            return (Double) this.getClientProperty(KEY_CURRENT);
258        }
259    
260        private double calculate(double current, int rotation) {
261            double newValue;
262            switch (type) {
263            case Factor:
264                newValue = current * Math.pow(incrementValue, rotation);
265                break;
266            default:
267                newValue = current + rotation * incrementValue;
268                break;
269            }
270            return newValue;
271        }
272    
273        private String format(NumberFormat numberFormat, Object value) {
274            return numberFormat == null ? value.toString() : numberFormat
275                    .format(value);
276        }
277    
278        private void processRotation(int rotation) {
279            double current = WheelButton.this.getCurrentValue();
280            double newValue = calculate(current, rotation); //
281            double upper = calculate(newValue, 1);
282            double lower = calculate(newValue, -1);
283            /*
284             * If the new value is between the upper and lower boundary, set the new
285             * value to the reset value.
286             */
287            if (lower < WheelButton.this.resetValue
288                    && WheelButton.this.resetValue < upper) {
289                newValue = WheelButton.this.resetValue;
290            } else
291            /*
292             * If, because of floating point limitations, the new value is outside
293             * the bounds of lower and upper, set the value back to the current
294             * value.
295             */
296            if (lower >= newValue || newValue >= upper) {
297                newValue = current;
298            }
299            putClientProperty(KEY_CURRENT, newValue);
300        }
301    
302    }