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    }