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}