1 /*********************************************************************************
2 * *
3 * Raptor - Rapid prototyping of Swing GUIs based on JavaBeans like Java objects *
4 * Copyright (C) 2003 XCOM AG *
5 * *
6 * This library is free software; you can redistribute it and/or *
7 * modify it under the terms of the GNU Lesser General Public *
8 * License as published by the Free Software Foundation; either *
9 * version 2.1 of the License, or (at your option) any later version. *
10 * *
11 * This library is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
14 * Lesser General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU Lesser General Public *
17 * License along with this library; if not, write to the Free Software *
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
19 * *
20 *********************************************************************************/
21 package net.sf.raptor.ui.components;
22
23 import java.text.DecimalFormat;
24 import java.text.ParseException;
25 import java.text.ParsePosition;
26
27 import javax.swing.text.AbstractDocument;
28 import javax.swing.text.AttributeSet;
29 import javax.swing.text.BadLocationException;
30 import javax.swing.text.PlainDocument;
31
32 public class NumericPlainDocument extends PlainDocument {
33
34 /**
35 * constructor
36 */
37 public NumericPlainDocument() {
38 setFormat(null);
39 }
40
41 /**
42 * constructor
43 */
44 public NumericPlainDocument(DecimalFormat format) {
45 setFormat(format);
46 }
47
48 /**
49 * constructor
50 *
51 * @param content
52 * @param format
53 */
54 public NumericPlainDocument(AbstractDocument.Content content, DecimalFormat format) {
55
56 super(content);
57 setFormat(format);
58
59 try {
60 format.parseObject(content.getString(0, content.length()), parsePos);
61 } catch (Exception e) {
62 throw new IllegalArgumentException("Initial content not a valid number");
63 }
64
65 if (parsePos.getIndex() != content.length() - 1) {
66 throw new IllegalArgumentException(
67 "Initial content not a valid number");
68 }
69 }
70
71 /**
72 * setFormat
73 *
74 * @param fmt
75 */
76 public void setFormat(DecimalFormat fmt) {
77 this.format = fmt != null ? fmt :
78 (DecimalFormat)defaultFormat.clone();
79
80 decimalSeparator =
81 format.getDecimalFormatSymbols().getDecimalSeparator();
82 groupingSeparator =
83 format.getDecimalFormatSymbols().getGroupingSeparator();
84 positivePrefix = format.getPositivePrefix();
85 positivePrefixLen = positivePrefix.length();
86 negativePrefix = format.getNegativePrefix();
87 negativePrefixLen = negativePrefix.length();
88 positiveSuffix = format.getPositiveSuffix();
89 positiveSuffixLen = positiveSuffix.length();
90 negativeSuffix = format.getNegativeSuffix();
91 negativeSuffixLen = negativeSuffix.length();
92 }
93
94 /**
95 * getFormat
96 *
97 * @return
98 */
99 public DecimalFormat getFormat() {
100 return format;
101 }
102
103 /**
104 * getNumberValue
105 *
106 * @return
107 * @throws ParseException
108 */
109 public Number getNumberValue() throws ParseException {
110 try {
111 String content = getText(0, getLength());
112 parsePos.setIndex(0);
113 Number result = format.parse(content, parsePos);
114 if (parsePos.getIndex() != getLength()) {
115 throw new ParseException(
116 "Not a valid number: " + content, 0);
117 }
118
119 return result;
120 } catch (BadLocationException e) {
121 throw new ParseException("Not a valid number", 0);
122 }
123 }
124
125 /**
126 * getLongValue
127 *
128 * @return
129 * @throws ParseException
130 */
131 public Long getLongValue() throws ParseException {
132 Number result = getNumberValue();
133 if ((result instanceof Long) == false) {
134 throw new ParseException("Not a valid long", 0);
135 }
136
137 return (Long)result;
138 }
139
140 /**
141 * getDoubleValue
142 *
143 * @return
144 * @throws ParseException
145 */
146 public Double getDoubleValue() throws ParseException {
147 Number result = getNumberValue();
148 if ((result instanceof Long) == false &&
149 (result instanceof Double) == false) {
150 throw new ParseException("Not a valid double", 0);
151 }
152
153 if (result instanceof Long) {
154 result = new Double(result.doubleValue());
155 }
156
157 return (Double)result;
158 }
159
160 /**
161 * insertString
162 */
163 public void insertString(int offset, String str, AttributeSet a) throws BadLocationException {
164 if (str == null || str.length() == 0) {
165 return;
166 }
167
168 Content content = getContent();
169 int length = content.length();
170 int originalLength = length;
171
172 parsePos.setIndex(0);
173
174 // Create the result of inserting the new data,
175 // but ignore the trailing newline
176 String targetString = content.getString(0, offset) + str +
177 content.getString(offset, length - offset - 1);
178
179 // Parse the input string and check for errors
180 do {
181 boolean gotPositive = targetString.startsWith(positivePrefix);
182 boolean gotNegative = targetString.startsWith(negativePrefix);
183
184 length = targetString.length();
185
186 // If we have a valid prefix, the parse fails if the
187 // suffix is not present and the error is reported
188 // at index 0. So, we need to add the appropriate
189 // suffix if it is not present at this point.
190 if (gotPositive == true || gotNegative == true) {
191 String suffix;
192 int suffixLength;
193 int prefixLength;
194
195 if (gotPositive == true && gotNegative == true) {
196 // This happens if one is the leading part of
197 // the other - e.g. if one is "(" and the other "(("
198 if (positivePrefixLen > negativePrefixLen) {
199 gotNegative = false;
200 } else {
201 gotPositive = false;
202 }
203 }
204
205 if (gotPositive == true) {
206 suffix = positiveSuffix;
207 suffixLength = positiveSuffixLen;
208 prefixLength = positivePrefixLen;
209 } else {
210 // Must have the negative prefix
211 suffix = negativeSuffix;
212 suffixLength = negativeSuffixLen;
213 prefixLength = negativePrefixLen;
214 }
215
216 // If the string consists of the prefix alone,
217 // do nothing, or the result won't parse.
218 if (length == prefixLength) {
219 break;
220 }
221
222 // We can't just add the suffix, because part of it
223 // may already be there. For example, suppose the
224 // negative prefix is "(" and the negative suffix is
225 // "$)". If the user has typed "(345$", then it is not
226 // correct to add "$)". Instead, only the missing part
227 // should be added, in this case ")".
228 if (targetString.endsWith(suffix) == false) {
229 int i;
230 for (i = suffixLength - 1; i > 0 ; i--) {
231 if (targetString.regionMatches(length - i,
232 suffix, 0, i)) {
233 targetString += suffix.substring(i);
234 break;
235 }
236 }
237
238 if (i == 0) {
239 // None of the suffix was present
240 targetString += suffix;
241 }
242
243 length = targetString.length();
244 }
245 }
246
247 format.parse(targetString, parsePos);
248
249 int endIndex = parsePos.getIndex();
250 if (endIndex == length) {
251 break; // Number is acceptable
252 }
253
254 // Parse ended early
255 // Since incomplete numbers don't always parse, try
256 // to work out what went wrong.
257 // First check for an incomplete positive prefix
258 if (positivePrefixLen > 0 &&
259 endIndex < positivePrefixLen &&
260 length <= positivePrefixLen &&
261 targetString.regionMatches(0, positivePrefix, 0, length)) {
262 break; // Accept for now
263 }
264
265 // Next check for an incomplete negative prefix
266 if (negativePrefixLen > 0 &&
267 endIndex < negativePrefixLen &&
268 length <= negativePrefixLen &&
269 targetString.regionMatches(0, negativePrefix, 0, length)) {
270 break; // Accept for now
271 }
272
273 // Allow a number that ends with the group
274 // or decimal separator, if these are in use
275 char lastChar = targetString.charAt(originalLength - 1);
276 int decimalIndex = targetString.indexOf(decimalSeparator);
277 if (format.isGroupingUsed() &&
278 lastChar == groupingSeparator &&
279 decimalIndex == -1) {
280 // Allow a "," but only in integer part
281 break;
282 }
283
284 if(format.isParseIntegerOnly() == false &&
285 lastChar == decimalSeparator &&
286 decimalIndex == originalLength - 1) {
287 // Allow a ".", but only one
288 break;
289 }
290
291 // No more corrections to make: must be an error
292 if (errorListener != null) {
293 errorListener.insertFailed(this, offset, str, a);
294 }
295 return;
296 } while (true == false);
297
298 // Finally, add to the model
299 super.insertString(offset, str, a);
300 }
301
302 /**
303 * addInsertErrorListener
304 *
305 * @param l
306 */
307 public void addInsertErrorListener(InsertErrorListener l) {
308 if (errorListener == null) {
309 errorListener = l;
310 return;
311 }
312 throw new IllegalArgumentException("InsertErrorListener already registered");
313 }
314
315 /**
316 * removeInsertErrorListener
317 *
318 * @param l
319 */
320 public void removeInsertErrorListener(InsertErrorListener l) {
321 if (errorListener == l) {
322 errorListener = null;
323 }
324 }
325
326 /**
327 * InsertErrorListener
328 *
329 * @author thomasg
330 *
331 * To change the template for this generated type comment go to
332 * Window>Preferences>Java>Code Generation>Code and Comments
333 */
334 public interface InsertErrorListener {
335 public abstract void insertFailed(NumericPlainDocument doc,
336 int offset, String str, AttributeSet a);
337 }
338
339 protected InsertErrorListener errorListener;
340 protected DecimalFormat format;
341 protected char decimalSeparator;
342 protected char groupingSeparator;
343 protected String positivePrefix;
344 protected String negativePrefix;
345 protected int positivePrefixLen;
346 protected int negativePrefixLen;
347 protected String positiveSuffix;
348 protected String negativeSuffix;
349 protected int positiveSuffixLen;
350 protected int negativeSuffixLen;
351 protected ParsePosition parsePos = new ParsePosition(0);
352 protected static DecimalFormat defaultFormat =
353 new DecimalFormat();
354 }