001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2003, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.gui.swing;
018:
019: // J2SE dependencies
020: import java.util.Set;
021: import java.util.Map;
022: import java.util.Date;
023: import java.util.Locale;
024: import java.util.HashMap;
025: import java.util.LinkedHashSet;
026: import java.text.Format;
027: import java.text.DateFormat;
028: import java.text.NumberFormat;
029: import java.text.DecimalFormat;
030: import java.text.SimpleDateFormat;
031:
032: // Swing and AWT dependencies
033: import javax.swing.JPanel;
034: import javax.swing.JLabel;
035: import javax.swing.JFrame;
036: import javax.swing.JComboBox;
037: import javax.swing.MutableComboBoxModel;
038: import javax.swing.DefaultComboBoxModel;
039: import javax.swing.BorderFactory;
040: import java.awt.Color;
041: import java.awt.Component;
042: import java.awt.GridBagLayout;
043: import java.awt.GridBagConstraints;
044: import java.awt.event.ActionEvent;
045: import java.awt.event.ActionListener;
046:
047: // Geotools dependencies
048: import org.geotools.measure.Angle;
049: import org.geotools.measure.AngleFormat;
050: import org.geotools.measure.CoordinateFormat;
051: import org.geotools.geometry.GeneralDirectPosition;
052: import org.geotools.resources.Arguments;
053: import org.geotools.resources.Utilities;
054: import org.geotools.resources.SwingUtilities;
055: import org.geotools.resources.i18n.Vocabulary;
056: import org.geotools.resources.i18n.VocabularyKeys;
057:
058: /**
059: * Select the pattern to use for {@linkplain Format formating} numbers, angles or dates.
060: * This widget can be used with one of {@link Format} objects working with pattern, like
061: * {@link DecimalFormat}, {@link SimpleDateFormat} or {@link AngleFormat}.
062: *
063: * @since 2.0
064: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/FormatChooser.java $
065: * @version $Id: FormatChooser.java 23632 2006-12-29 22:13:51Z desruisseaux $
066: * @author Martin Desruisseaux
067: */
068: public class FormatChooser extends JPanel {
069: /**
070: * The maximum number of items to keep in the history list.
071: */
072: private static final int HISTORY_SIZE = 50;
073:
074: /**
075: * The color for error message.
076: */
077: private static final Color ERROR_COLOR = Color.RED;
078:
079: /**
080: * A set of default patterns for differents locales. Keys are {@link Locale} object
081: * and values are {@code String[][]} with arrays in the following order: number
082: * patterns, date patterns and angle patterns.
083: */
084: private static final Map PATTERNS = new HashMap();
085:
086: /**
087: * A set of default pattern for {@link AngleFormat}.
088: */
089: private static final String[] ANGLE_PATTERNS = new String[] {
090: "D.d°", "D.dd°", "D.ddd°", "D°MM'", "D°MM.m'", "D°MM.mm'",
091: "D°MM.mmm'", "D°MM'SS\"", "D°MM'SS.s\"" };
092:
093: /**
094: * The format to configure by this {@code FormatChooser}.
095: */
096: protected Format format;
097:
098: /**
099: * A sample value for the "preview" text.
100: */
101: private Object value;
102:
103: /**
104: * The panel in which to edit the pattern.
105: */
106: private final JComboBox choices = new JComboBox();
107:
108: /**
109: * The preview text. This is the {@code value} formated using {@code format}.
110: */
111: private final JLabel preview = new JLabel();
112:
113: /**
114: * Constructs a pattern chooser for the given format.
115: *
116: * @param format The format to configure. The default implementation accept instance of
117: * {@link DecimalFormat}, {@link SimpleDateFormat} or {@link AngleFormat}.
118: * @throws IllegalArgumentException if the format is invalid.
119: */
120: public FormatChooser(final Format format)
121: throws IllegalArgumentException {
122: super (new GridBagLayout());
123: final String[] patterns = getPatterns(format);
124: if (patterns != null) {
125: final MutableComboBoxModel model = (MutableComboBoxModel) choices
126: .getModel();
127: for (int i = 0; i < patterns.length; i++) {
128: model.addElement(patterns[i]);
129: }
130: }
131: choices.setEditable(true); // Must be invoked before 'setFormat'.
132: value = suggestSampleValue(format);
133: setFormat(format);
134:
135: final Vocabulary resources = Vocabulary
136: .getResources(getDefaultLocale());
137: final GridBagConstraints c = new GridBagConstraints();
138: c.gridx = 0;
139: c.insets.right = 6;
140: c.gridy = 0;
141: add(new JLabel(resources.getLabel(VocabularyKeys.FORMAT)), c);
142: c.gridy++;
143: c.insets.top = 3;
144: add(new JLabel(resources.getLabel(VocabularyKeys.PREVIEW)), c);
145: c.insets.right = 0;
146: c.gridx++;
147: c.weightx = 1;
148: c.fill = c.HORIZONTAL;
149: c.gridy = 0;
150: c.insets.top = 0;
151: add(choices, c);
152: c.gridy++;
153: c.insets.top = 3;
154: add(preview, c);
155: choices.getEditor().getEditorComponent().requestFocus();
156: choices.addActionListener(new ActionListener() {
157: public void actionPerformed(final ActionEvent event) {
158: applyPattern(false);
159: }
160: });
161: setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
162: }
163:
164: /**
165: * Returns a set of patterns for formatting in the given locale, or {@code null} if none.
166: *
167: * @param format for which to get a set of default patterns.
168: * @todo Need a way to find the format locale.
169: */
170: private static synchronized String[] getPatterns(final Format format) {
171: final Locale locale = Locale.getDefault();
172: String[][] patterns = (String[][]) PATTERNS.get(locale);
173: if (patterns == null) {
174: patterns = new String[3][];
175: }
176: if (format instanceof NumberFormat) {
177: if (patterns[0] == null) {
178: patterns[0] = getNumberPatterns(locale);
179: }
180: return patterns[0];
181: }
182: if (format instanceof DateFormat) {
183: if (patterns[1] == null) {
184: patterns[1] = getDatePatterns(locale);
185: }
186: return patterns[1];
187: }
188: if (format instanceof AngleFormat
189: || format instanceof CoordinateFormat) {
190: if (patterns[2] == null) {
191: patterns[2] = ANGLE_PATTERNS;
192: }
193: return patterns[2];
194: }
195: return null;
196: }
197:
198: /**
199: * Returns a set of patterns for formatting numbers in the given locale.
200: * Note: this method is costly and should be invoked only once for a given locale.
201: */
202: private static String[] getNumberPatterns(final Locale locale) {
203: final Set patterns = new LinkedHashSet();
204: int type = 0;
205: fill: while (true) {
206: final int digits;
207: final NumberFormat format;
208: switch (type++) {
209: case 0:
210: format = NumberFormat.getInstance(locale);
211: digits = -1;
212: break;
213: case 1:
214: format = NumberFormat.getNumberInstance(locale);
215: digits = 4;
216: break;
217: case 2:
218: format = NumberFormat.getPercentInstance(locale);
219: digits = 2;
220: break;
221: case 3:
222: format = NumberFormat.getCurrencyInstance(locale);
223: digits = -1;
224: break;
225: default:
226: break fill;
227: }
228: if (format instanceof DecimalFormat) {
229: final DecimalFormat decimal = (DecimalFormat) format;
230: patterns.add(decimal.toLocalizedPattern());
231: for (int i = 0; i <= digits; i++) {
232: format.setMinimumFractionDigits(i);
233: format.setMaximumFractionDigits(i);
234: patterns.add(decimal.toLocalizedPattern());
235: }
236: }
237: }
238: return (String[]) patterns.toArray(new String[patterns.size()]);
239: }
240:
241: /**
242: * Returns a set of patterns for formatting dates in the given locale.
243: * Note: this method is costly and should be invoked only once for a given locale.
244: */
245: private static String[] getDatePatterns(final Locale locale) {
246: final int[] codes = { SimpleDateFormat.SHORT,
247: SimpleDateFormat.MEDIUM, SimpleDateFormat.LONG,
248: SimpleDateFormat.FULL };
249: final Set patterns = new LinkedHashSet();
250: for (int i = 0; i < codes.length; i++) {
251: for (int j = -1; j < codes.length; j++) {
252: final DateFormat format;
253: if (j < 0) {
254: format = DateFormat.getDateInstance(codes[i],
255: locale);
256: } else {
257: format = DateFormat.getDateTimeInstance(codes[i],
258: codes[j], locale);
259: }
260: if (format instanceof SimpleDateFormat) {
261: patterns.add(((SimpleDateFormat) format)
262: .toLocalizedPattern());
263: }
264: }
265: }
266: return (String[]) patterns.toArray(new String[patterns.size()]);
267: }
268:
269: /**
270: * Suggest a sample value for the given format, or {@code null} if this
271: * method has no suggestion.
272: *
273: * @param format The format.
274: * @return A sample value for the specified format, or {@code null} if none.
275: */
276: private static Object suggestSampleValue(final Format format) {
277: if (format instanceof NumberFormat) {
278: return new Double(39.3); // Could be any random value.
279: }
280: if (format instanceof DateFormat) {
281: return new Date(); // Could be any random value.
282: }
283: if (format instanceof AngleFormat) {
284: return new Angle(39.3); // Could be any random value.
285: }
286: if (format instanceof CoordinateFormat) {
287: final int dimension = ((CoordinateFormat) format)
288: .getCoordinateReferenceSystem()
289: .getCoordinateSystem().getDimension();
290: final GeneralDirectPosition point = new GeneralDirectPosition(
291: dimension);
292: for (int i = 0; i < dimension; i++) {
293: point.setOrdinate(i, (i & 1) == 0 ? 39.3 : 27.9); // Could be any random value.
294: }
295: return point;
296: }
297: return null;
298: }
299:
300: /**
301: * Returns the current format.
302: */
303: public Format getFormat() {
304: return format;
305: }
306:
307: /**
308: * Set the format to configure. The default implementation accept instance of
309: * {@link DecimalFormat}, {@link SimpleDateFormat} or {@link AngleFormat}. If
310: * more format class are wanted, methods {@link #getPattern} and {@link #setPattern}
311: * should be overridden.
312: *
313: * @param format The format to congifure.
314: * @throws IllegalArgumentException if the format is invalid.
315: */
316: public void setFormat(final Format format)
317: throws IllegalArgumentException {
318: final Format old = this .format;
319: this .format = format;
320: try {
321: update();
322: } catch (IllegalStateException exception) {
323: this .format = old;
324: /*
325: * The format is not one of recognized type. Since this format was given in argument
326: * (rather then the internal format field), Change the exception type for consistency
327: * with the usual specification.
328: */
329: final IllegalArgumentException e;
330: e = new IllegalArgumentException(exception
331: .getLocalizedMessage());
332: e.initCause(exception);
333: throw e;
334: }
335: firePropertyChange("format", old, format);
336: }
337:
338: /**
339: * Returns the sample value to format as a "preview" text.
340: * If no such object is defined, then this method returns {@code null}.
341: */
342: public Object getSampleValue() {
343: return value;
344: }
345:
346: /**
347: * Sets the sample value to format as a "preview" text. The value should
348: * be an object formatable with {@link #getFormat}.
349: *
350: * @param value The value to format, or {@code null}.
351: * @throws IllegalArgumentException if the value can't be formatted.
352: */
353: public void setSampleValue(final Object value)
354: throws IllegalArgumentException {
355: preview.setText(value != null ? format.format(value) : null);
356: preview.setForeground(getForeground());
357: final Object old = this .value;
358: this .value = value;
359: firePropertyChange("sampleValue", old, value);
360: }
361:
362: /**
363: * Returns the localized pattern for the {@linkplain #getFormat current format}.
364: * The default implementation recognize {@link DecimalFormat}, {@link SimpleDateFormat}
365: * and {@link AngleFormat} instances.
366: *
367: * @return The pattern for the current format.
368: * @throws IllegalStateException is the current format is not one of recognized type.
369: */
370: public String getPattern() throws IllegalStateException {
371: if (format instanceof DecimalFormat) {
372: return ((DecimalFormat) format).toLocalizedPattern();
373: }
374: if (format instanceof SimpleDateFormat) {
375: return ((SimpleDateFormat) format).toLocalizedPattern();
376: }
377: if (format instanceof AngleFormat) {
378: return ((AngleFormat) format).toPattern();
379: }
380: if (format instanceof CoordinateFormat) {
381: final CoordinateFormat format = (CoordinateFormat) this .format;
382: for (int i = format.getCoordinateReferenceSystem()
383: .getCoordinateSystem().getDimension(); --i >= 0;) {
384: final Format sub = format.getFormat(i);
385: if (sub instanceof AngleFormat) {
386: return ((AngleFormat) sub).toPattern();
387: }
388: }
389: }
390: throw new IllegalStateException(Utilities
391: .getShortClassName(format));
392: }
393:
394: /**
395: * Sets the localized pattern for the {@linkplain #getFormat current format}.
396: * The default implementation recognize {@link DecimalFormat}, {@link SimpleDateFormat}
397: * and {@link AngleFormat} instances.
398: *
399: * @param pattern The pattern for the current format.
400: * @throws IllegalStateException is the current format is not one of recognized type.
401: * @throws IllegalArgumentException if the specified pattern is invalid.
402: */
403: public void setPattern(final String pattern)
404: throws IllegalStateException, IllegalArgumentException {
405: if (format instanceof DecimalFormat) {
406: ((DecimalFormat) format).applyLocalizedPattern(pattern);
407: } else if (format instanceof SimpleDateFormat) {
408: ((SimpleDateFormat) format).applyLocalizedPattern(pattern);
409: } else if (format instanceof AngleFormat) {
410: ((AngleFormat) format).applyPattern(pattern);
411: } else if (format instanceof CoordinateFormat) {
412: ((CoordinateFormat) format).setAnglePattern(pattern);
413: } else {
414: throw new IllegalStateException(Utilities
415: .getShortClassName(format));
416: }
417: update();
418: }
419:
420: /**
421: * Update the preview text according the current format pattern.
422: */
423: final void update() {
424: choices.setSelectedItem(getPattern());
425: try {
426: preview
427: .setText(value != null ? format.format(value)
428: : null);
429: preview.setForeground(getForeground());
430: } catch (IllegalArgumentException exception) {
431: /*
432: * The value can't be formatted. Replace the
433: * value by the format error message.
434: */
435: preview.setText(exception.getLocalizedMessage());
436: preview.setForeground(ERROR_COLOR);
437: }
438: }
439:
440: /**
441: * Applies the currently selected pattern. If {@code add} is {@code true},
442: * then the pattern is added to the combo box list.
443: *
444: * @param add {@code true} for adding the pattern to the combo box list.
445: * @return {@code true} if the pattern is valid.
446: */
447: private boolean applyPattern(final boolean add) {
448: String pattern = choices.getSelectedItem().toString();
449: if (pattern.trim().length() == 0) {
450: update();
451: return false;
452: }
453: try {
454: setPattern(pattern);
455: } catch (RuntimeException exception) {
456: /*
457: * The pattern is not valid. Replace the value by an error message.
458: */
459: preview.setText(exception.getLocalizedMessage());
460: preview.setForeground(ERROR_COLOR);
461: return false;
462: }
463: if (add) {
464: final DefaultComboBoxModel model = (DefaultComboBoxModel) choices
465: .getModel();
466: pattern = choices.getSelectedItem().toString();
467: final int index = model.getIndexOf(pattern);
468: if (index > 0) {
469: model.removeElementAt(index);
470: }
471: if (index != 0) {
472: model.insertElementAt(pattern, 0);
473: }
474: final int size = model.getSize();
475: while (size > HISTORY_SIZE) {
476: model.removeElementAt(size - 1);
477: }
478: if (size != 0) {
479: choices.setSelectedIndex(0);
480: }
481: }
482: return true;
483: }
484:
485: /**
486: * Shows a dialog box requesting input from the user. The dialog box will be
487: * parented to {@code owner}. If {@code owner} is contained into a
488: * {@link javax.swing.JDesktopPane}, the dialog box will appears as an internal
489: * frame. This method can be invoked from any thread (may or may not be the
490: * <i>Swing</i> thread).
491: *
492: * @param owner The parent component for the dialog box,
493: * or {@code null} if there is no parent.
494: * @param title The dialog box title.
495: * @return {@code true} if user pressed the "Ok" button, or
496: * {@code false} otherwise (e.g. pressing "Cancel"
497: * or closing the dialog box from the title bar).
498: */
499: public boolean showDialog(final Component owner, final String title) {
500: final String old = getPattern();
501: while (SwingUtilities.showOptionDialog(owner, this , title)) {
502: if (applyPattern(true)) {
503: return true;
504: }
505: }
506: setPattern(old);
507: return false;
508: }
509:
510: /**
511: * Show this component. This method is used mostly in order
512: * to check the look of this widget from the command line.
513: */
514: public static void main(final String[] args) {
515: final Arguments arguments = new Arguments(args);
516: Locale.setDefault(arguments.locale);
517: new FormatChooser(new AngleFormat()).showDialog(null, Utilities
518: .getShortName(FormatChooser.class));
519: System.exit(0);
520: }
521: }
|