1 /* Copyright (C) 2005-2011 Fabio Riccardi */
2 
3 package com.lightcrafts.ui.operation.generic;
4 
5 import com.lightcrafts.ui.LightZoneSkin;
6 
7 import javax.swing.*;
8 import javax.swing.event.ChangeEvent;
9 import javax.swing.event.ChangeListener;
10 import javax.swing.event.DocumentEvent;
11 import javax.swing.event.DocumentListener;
12 import javax.swing.text.Document;
13 import java.awt.*;
14 import java.awt.event.*;
15 import java.text.NumberFormat;
16 import java.text.DecimalFormat;
17 import java.text.ParseException;
18 
19 /** This is a JTextField that listens on a ConfiguredBoundedRangeModel,
20   * updating its text when the model changes and pushing validated numeric
21   * text back into the model.
22   */
23 
24 class ConfiguredTextField
25     extends JTextField
26     implements MouseWheelListener, ChangeListener, DocumentListener
27 {
28     // Select-all and handle mouse wheel events when text fields gain focus:
29     private static final FocusListener FocusSelector = new FocusAdapter() {
30         public void focusGained(FocusEvent event) {
31             ConfiguredTextField text = (ConfiguredTextField) event.getSource();
32             text.selectAll();
33             text.addMouseWheelListener(text);
34         }
35         public void focusLost(FocusEvent event) {
36             ConfiguredTextField text = (ConfiguredTextField) event.getSource();
37             text.select(0, 0);
38             text.removeMouseWheelListener(text);
39         }
40     };
41 
42     private ConfiguredBoundedRangeModel model;
43     private NumberFormat format;
44 
45     private double min;     // The minimum numeric value for the text
46     private double max;     // The maximum numeric value for the text
47 
48     private double inc;     // An increment/decrement amount, for keystrokes
49 
50     private boolean isUpdating; // A flag to detect our own model changes
51 
ConfiguredTextField( ConfiguredBoundedRangeModel model, DecimalFormat format )52     ConfiguredTextField(
53         ConfiguredBoundedRangeModel model, DecimalFormat format
54     ) {
55         this.model = model;
56         this.format = format;
57         min = model.getConfiguredMinimum();
58         max = model.getConfiguredMaximum();
59         inc = model.getConfiguredIncrement();
60         setInputVerifier(new IntervalVerifier(min, max));
61         setHorizontalAlignment(RIGHT);
62 
63         setFont(getFont()); // Figure out the maxiumum text width
64 
65         addListeners();
66 
67         updateFromModel();
68     }
69 
setFont(Font font)70     public void setFont(Font font) {
71         super.setFont(font);
72         if (model == null) {
73             // called from base class constructor
74             return;
75         }
76         // Adjust our size to allow for the maximum value:
77 
78         double max = model.getConfiguredMaximum();
79         int widest = getWidestNumber(max);
80 
81         String tempText = Integer.toString(widest);
82         int places = format.getMaximumFractionDigits();
83         if (places > 0) {
84             tempText += ".";
85             for (int n=0; n<places; n++) {
86                 tempText += "0";
87             }
88         }
89         String text = getText();
90         setText(tempText);
91         setPreferredSize(null); // wipe previous settings
92         Dimension size = getPreferredSize();
93         setText(text);
94 
95         setMinimumSize(size);
96         setPreferredSize(size);
97     }
98 
99     // When the model changes, update our text:
stateChanged(ChangeEvent event)100     public void stateChanged(ChangeEvent event) {
101         if (isUpdating) {
102             return;
103         }
104         Object source = event.getSource();
105         if (! source.equals(model)) {
106             return;
107         }
108         updateFromModel();
109     }
110 
changedUpdate(DocumentEvent e)111     public void changedUpdate(DocumentEvent e) {
112         handleDocumentChange();
113     }
114 
insertUpdate(DocumentEvent e)115     public void insertUpdate(DocumentEvent e) {
116         handleDocumentChange();
117     }
118 
removeUpdate(DocumentEvent e)119     public void removeUpdate(DocumentEvent e) {
120         handleDocumentChange();
121     }
122 
updateFromModel()123     private void updateFromModel() {
124         double value = model.getConfiguredValue();
125         String text = format.format(value);
126         if (! text.equals(getText())) {
127             setText(text);
128             selectAll();
129         }
130     }
131 
132     // When the document changes, give verification feedback and maybe update
133     // the model:
handleDocumentChange()134     private void handleDocumentChange() {
135         InputVerifier verifier = getInputVerifier();
136         String text = getText();
137         boolean verified = verifier.verify(this);
138         if (! verified) {
139             setForeground(Color.red);
140         }
141         else {
142             try {
143                 double value = format.parse(text).doubleValue();
144                 setForeground(LightZoneSkin.Colors.ToolPanesForeground);
145                 isUpdating = true;
146                 model.setConfiguredValue(value);
147                 isUpdating = false;
148             }
149             catch (ParseException e) {
150                 // Should never happen, because of the verifier.
151                 System.err.println("Unparsable verified text: " + text);
152                 setForeground(Color.red);
153             }
154         }
155     }
156 
addListeners()157     private void addListeners() {
158 
159         // Respond to document changes with verification and model changes:
160         Document doc = getDocument();
161         doc.addDocumentListener(this);
162 
163         // Update text when the model changes:
164         model.addChangeListener(this);
165 
166         // Select-all when we gain focus, select-none when we lose:
167         addFocusListener(FocusSelector);
168 
169         // Override the default input map for spacebar, because that key stroke
170         // is used globally to access the editor's pan mode.
171         InputMap input = getInputMap(WHEN_FOCUSED);
172         KeyStroke space = KeyStroke.getKeyStroke(new Character(' '), 0);
173         input.put(space, "none");
174 
175         // Increment by largeInc on up arrow events:
176         registerKeyboardAction(
177             new ActionListener() {
178                 public void actionPerformed(ActionEvent event) {
179                     double value = model.getConfiguredValue();
180                     value = getNextRoundValueUp(value);
181                     if ((value >= min) && (value <= max)) {
182                         model.setConfiguredValue(value);
183                     }
184                 }
185             },
186             KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0),
187             WHEN_FOCUSED
188         );
189 
190         // Decrement by largeInc on down arrow events:
191         registerKeyboardAction(
192             new ActionListener() {
193                 public void actionPerformed(ActionEvent event) {
194                     double value = model.getConfiguredValue();
195                     value = getNextRoundValueDown(value);
196                     if ((value >= min) && (value <= max)) {
197                         model.setConfiguredValue(value);
198                     }
199                 }
200             },
201             KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0),
202             WHEN_FOCUSED
203         );
204     }
205 
206     // Increment/decrement by smallInc on mouse wheel events, if we're focused:
mouseWheelMoved(MouseWheelEvent event)207     public void mouseWheelMoved(MouseWheelEvent event) {
208         double value = model.getConfiguredValue();
209         int count = event.getWheelRotation();
210         int sign = (count > 0) ? 1 : -1;
211         for (int n=0; n<sign*count; n++) {
212             if (sign > 0) {
213                 value = getNextRoundValueDown(value);
214             }
215             else {
216                 value = getNextRoundValueUp(value);
217             }
218             if ((value >= min) && (value <= max)) {
219                 model.setConfiguredValue(value);
220             }
221             else {
222                 break;
223             }
224         }
225     }
226 
getNextRoundValueDown(double value)227     private double getNextRoundValueDown(double value) {
228         return getRoundValue(value - inc);
229     }
230 
getNextRoundValueUp(double value)231     private double getNextRoundValueUp(double value) {
232         return getRoundValue(value + inc);
233     }
234 
getRoundValue(double value)235     private double getRoundValue(double value) {
236         return inc * Math.round(value / inc);
237     }
238 
getWidestNumber(double max)239     private static int getWidestNumber(double max) {
240         double powTen = Math.pow(10, Math.ceil(Math.log(max) / Math.log(10)));
241         int widest = (int) Math.round(powTen);
242         if (widest == 1) {
243             // the one case where a power of ten is narrower than a
244             // smaller nonnegative integer
245             widest = 100;
246         }
247         return widest;
248     }
249 
250     // An InputVerifier that checks our text is compatible with a number range:
251 
252     private class IntervalVerifier extends InputVerifier {
253 
254         private double min;
255         private double max;
256 
IntervalVerifier(double min, double max)257         IntervalVerifier(double min, double max) {
258             this.min = min;
259             this.max = max;
260         }
261 
verify(JComponent input)262         public boolean verify(JComponent input) {
263             JTextField textField = (JTextField) input;
264             String text = textField.getText();
265             double x;
266             try {
267                 x = format.parse(text).doubleValue();
268             }
269             catch (ParseException e) {
270                 return false;
271             }
272             return ((x >= min) && (x <= max));
273         }
274     }
275 }
276