001 /*
002 * Copyright (c) 2002,2003 Martin Desruisseaux
003 * Copyright (c) 2005 Einar Pehrson <einar@pehrson.nu>.
004 *
005 * This file is part of
006 * CleanSheets - a spreadsheet application for the Java platform.
007 *
008 * CleanSheets is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License as published by
010 * the Free Software Foundation; either version 2 of the License, or
011 * (at your option) any later version.
012 *
013 * CleanSheets is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with CleanSheets; if not, write to the Free Software
020 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
021 */
022 package csheets.ext.style.ui;
023
024 import java.awt.BorderLayout;
025 import java.awt.Color;
026 import java.awt.Component;
027 import java.awt.Dimension;
028 import java.awt.event.ActionEvent;
029 import java.awt.event.ActionListener;
030 import java.text.DateFormat;
031 import java.text.DecimalFormat;
032 import java.text.Format;
033 import java.text.NumberFormat;
034 import java.text.SimpleDateFormat;
035 import java.util.Date;
036 import java.util.LinkedHashSet;
037 import java.util.Locale;
038 import java.util.Set;
039 import java.util.SortedSet;
040 import java.util.TreeSet;
041
042 import javax.swing.BorderFactory;
043 import javax.swing.DefaultComboBoxModel;
044 import javax.swing.Icon;
045 import javax.swing.JComboBox;
046 import javax.swing.JLabel;
047 import javax.swing.JOptionPane;
048 import javax.swing.JPanel;
049
050 /**
051 * A component which allows the user to select a border.
052 * @author Martin Desruisseaux
053 * @author Einar Pehrson
054 */
055 @SuppressWarnings("serial")
056 public class FormatChooser extends JPanel {
057
058 /** The maximum number of items to keep in the history list. */
059 private static final int HISTORY_SIZE = 50;
060
061 /** The color for error message. */
062 private static final Color ERROR_COLOR = Color.RED;
063
064 /** The format to configure by this <code>FormatChooser</code>. */
065 private Format format;
066
067 /** A sample value for the "preview" text. */
068 private Object value;
069
070 /** The panel in which to edit the pattern */
071 private final JComboBox choices = new JComboBox();
072
073 /** The preview label with the <code>value</code> formated using <code>format</code> */
074 private final JLabel previewLabel = new JLabel();
075
076 /**
077 * Creates a pattern chooser for the given date format.
078 * @param format the format to configure
079 * @param value the value to format
080 */
081 public FormatChooser(DateFormat format, Date value) {
082 this(getPatterns(format));
083
084 // Initializes format
085 this.value = value;
086 setFormat(format);
087 }
088
089 /**
090 * Creates a pattern chooser for the given number format.
091 * @param format the format to configure
092 * @param value the value to format
093 */
094 public FormatChooser(NumberFormat format, Number value) {
095 this(getPatterns(format));
096
097 // Initializes format
098 this.value = value;
099 setFormat(format);
100 }
101
102 /**
103 * Creates a pattern chooser for the given format.
104 * @param patterns the patterns to choose from
105 */
106 private FormatChooser(String[] patterns) {
107 // Creates format box
108 if (patterns != null)
109 choices.setModel(new DefaultComboBoxModel(patterns));
110 choices.setEditable(true);
111 choices.addActionListener(new ActionListener() {
112 public void actionPerformed(ActionEvent event) {
113 applyPattern(false);
114 }
115 });
116
117 // Creates format container
118 JPanel boxPanel = new JPanel();
119 boxPanel.add(choices);
120 boxPanel.setBorder(BorderFactory.createTitledBorder("Format"));
121
122 // Configures preview label
123 previewLabel.setHorizontalAlignment(JLabel.CENTER);
124 previewLabel.setPreferredSize(new Dimension(70, 50));
125 previewLabel.setBorder(
126 BorderFactory.createCompoundBorder(
127 BorderFactory.createTitledBorder("Preview"),
128 BorderFactory.createEmptyBorder(5, 5, 5, 5)
129 ));
130
131 // Configures layout and adds components
132 setLayout(new BorderLayout(5, 5));
133 add(boxPanel, BorderLayout.CENTER);
134 add(previewLabel, BorderLayout.SOUTH);
135 choices.getEditor().getEditorComponent().requestFocus();
136 }
137
138 /**
139 * Returns a set of patterns for formatting in the given locale,
140 * @param format for which to get a set of default patterns.
141 * @return the patterns that were found
142 */
143 private static synchronized String[] getPatterns(Format format) {
144 Locale locale = Locale.getDefault();
145 if (format instanceof NumberFormat)
146 return getNumberPatterns(locale);
147 else if (format instanceof DateFormat)
148 return getDatePatterns(locale);
149 else
150 return null;
151 }
152
153 /**
154 * Returns a set of patterns for formatting numbers in the given locale.
155 * @param locale the locale for which to fetch patterns
156 * @return the patterns that were found
157 */
158 private static String[] getNumberPatterns(Locale locale) {
159 // Collects formats
160 NumberFormat[] formats = new NumberFormat[] {
161 NumberFormat.getInstance(locale),
162 NumberFormat.getNumberInstance(locale),
163 NumberFormat.getPercentInstance(locale),
164 NumberFormat.getCurrencyInstance(locale)};
165
166 // Collects patterns
167 Set<String> patterns = new LinkedHashSet<String>();
168 for (int i = 0; i < formats.length; i++) {
169 if (formats[i] instanceof DecimalFormat) {
170 int digits = -1;
171 if (i == 1)
172 digits = 4;
173 else if (i == 2)
174 digits = 2;
175 DecimalFormat decimal = (DecimalFormat)formats[i];
176 patterns.add(decimal.toLocalizedPattern());
177 for (int decimals = 0; decimals <= digits; decimals++) {
178 decimal.setMinimumFractionDigits(decimals);
179 decimal.setMaximumFractionDigits(decimals);
180 patterns.add(decimal.toLocalizedPattern());
181 }
182 }
183 }
184 return patterns.toArray(new String[patterns.size()]);
185 }
186
187 /**
188 * Returns a set of patterns for formatting dates in the given locale.
189 * @param locale the locale for which to fetch patterns
190 * @return the patterns that were found
191 */
192 private static String[] getDatePatterns(Locale locale) {
193 // Collects formats
194 Set<DateFormat> formats = new LinkedHashSet<DateFormat>();
195 int[] codes = {DateFormat.SHORT, DateFormat.MEDIUM, DateFormat.LONG,
196 DateFormat.FULL};
197 for (int code : codes) {
198 formats.add(DateFormat.getDateInstance(code, locale));
199 formats.add(DateFormat.getTimeInstance(code, locale));
200 for (int timeCode : codes)
201 formats.add(DateFormat.getDateTimeInstance(code, timeCode, locale));
202 }
203
204 // Collects patterns
205 SortedSet<String> patterns = new TreeSet<String>();
206 for (DateFormat format : formats)
207 if (format instanceof SimpleDateFormat)
208 patterns.add(((SimpleDateFormat) format).toLocalizedPattern());
209 return patterns.toArray(new String[patterns.size()]);
210 }
211
212 /**
213 * Returns the current format.
214 * @return the current format.
215 */
216 public Format getFormat() {
217 return format;
218 }
219
220 /**
221 * Set the format to configure. The default implementation accept instance
222 * of {@link DecimalFormat} or {@link SimpleDateFormat}.
223 * @param format the format to congifure.
224 * @throws IllegalArgumentException if the format is invalid.
225 */
226 public void setFormat(Format format) throws IllegalArgumentException {
227 Format old = this.format;
228 this.format = format;
229 try {
230 update();
231 } catch (IllegalStateException exception) {
232 this.format = old;
233 // The format is not one of recognized type. Since this format was given in argument
234 // (rather then the internal format field), Change the exception type for consistency
235 // with the usual specification.
236 IllegalArgumentException e = new IllegalArgumentException(
237 exception.getLocalizedMessage());
238 e.initCause(exception);
239 throw e;
240 }
241 firePropertyChange("format", old, format);
242 }
243
244 /**
245 * Returns the localized pattern for the {@linkplain #getFormat current format}.
246 * The default implementation recognize {@link DecimalFormat} and
247 * {@link SimpleDateFormat} instances.
248 * @return The pattern for the current format.
249 * @throws IllegalStateException is the current format is not one of recognized type.
250 */
251 public String getPattern() throws IllegalStateException {
252 if (format instanceof DecimalFormat)
253 return ((DecimalFormat) format).toLocalizedPattern();
254 if (format instanceof SimpleDateFormat)
255 return ((SimpleDateFormat) format).toLocalizedPattern();
256 throw new IllegalStateException();
257 }
258
259 /**
260 * Sets the localized pattern for the {@linkplain #getFormat current format}.
261 * The default implementation recognize {@link DecimalFormat} and
262 * {@link SimpleDateFormat} instances.
263 * @param pattern The pattern for the current format.
264 * @throws IllegalStateException is the current format is not one of recognized type.
265 * @throws IllegalArgumentException if the specified pattern is invalid.
266 */
267 public void setPattern(String pattern)
268 throws IllegalStateException, IllegalArgumentException {
269 if (format instanceof DecimalFormat)
270 ((DecimalFormat) format).applyLocalizedPattern(pattern);
271 else if (format instanceof SimpleDateFormat)
272 ((SimpleDateFormat) format).applyLocalizedPattern(pattern);
273 else
274 throw new IllegalStateException();
275 update();
276 }
277
278 /**
279 * Update the preview text according the current format pattern.
280 */
281 private void update() {
282 choices.setSelectedItem(getPattern());
283 try {
284 previewLabel.setText(value!=null ? format.format(value) : null);
285 previewLabel.setForeground(getForeground());
286 } catch (IllegalArgumentException exception) {
287 previewLabel.setText(exception.getLocalizedMessage());
288 previewLabel.setForeground(ERROR_COLOR);
289 }
290 }
291
292 /**
293 * Apply the currently selected pattern. If <code>add</code> is <code>true</code>,
294 * then the pattern is added to the combo box list.
295 * @param add <code>true</code> for adding the pattern to the combo box list.
296 * @return <code>true</code> if the pattern is valid.
297 */
298 private boolean applyPattern(boolean add) {
299 String pattern = choices.getSelectedItem().toString();
300 if (pattern.trim().length() == 0) {
301 update();
302 return false;
303 }
304 try {
305 setPattern(pattern);
306 } catch (RuntimeException exception) {
307 /* The pattern is not valid. Replace the value by an error message */
308 previewLabel.setText(exception.getLocalizedMessage());
309 previewLabel.setForeground(ERROR_COLOR);
310 return false;
311 }
312 if (add) {
313 DefaultComboBoxModel model = (DefaultComboBoxModel)choices.getModel();
314 pattern = choices.getSelectedItem().toString();
315 int index = model.getIndexOf(pattern);
316 if (index > 0)
317 model.removeElementAt(index);
318 if (index != 0)
319 model.insertElementAt(pattern, 0);
320 int size = model.getSize();
321 while (size > HISTORY_SIZE)
322 model.removeElementAt(size-1);
323 if (size != 0)
324 choices.setSelectedIndex(0);
325 }
326 return true;
327 }
328
329 /**
330 * Shows a dialog box requesting input from the user.
331 * @param owner the parent component for the dialog box
332 * @param title the dialog box title
333 * @return the selected format or, if the user did not press OK, null
334 */
335 public Format showDialog(Component owner, String title) {
336 int returnValue = JOptionPane.showConfirmDialog(
337 owner,
338 this,
339 title,
340 JOptionPane.OK_CANCEL_OPTION,
341 JOptionPane.PLAIN_MESSAGE,
342 (Icon)null);
343 if (returnValue == JOptionPane.OK_OPTION)
344 if (applyPattern(true))
345 return getFormat();
346 return null;
347 }
348 }