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 */
022package csheets.ext.style.ui;
023
024import java.awt.BorderLayout;
025import java.awt.Color;
026import java.awt.Component;
027import java.awt.Dimension;
028import java.awt.event.ActionEvent;
029import java.awt.event.ActionListener;
030import java.text.DateFormat;
031import java.text.DecimalFormat;
032import java.text.Format;
033import java.text.NumberFormat;
034import java.text.SimpleDateFormat;
035import java.util.Date;
036import java.util.LinkedHashSet;
037import java.util.Locale;
038import java.util.Set;
039import java.util.SortedSet;
040import java.util.TreeSet;
041
042import javax.swing.BorderFactory;
043import javax.swing.DefaultComboBoxModel;
044import javax.swing.Icon;
045import javax.swing.JComboBox;
046import javax.swing.JLabel;
047import javax.swing.JOptionPane;
048import 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")
056public 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}