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 }