/*******************************************************************************
* Copyright (c) 2005, 2009 Eric Wuillai.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Eric Wuillai (eric@wdev91.com) - initial API and implementation
*******************************************************************************/
package org.eclipse.nebula.widgets.formattedtext;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Hashtable;
import java.util.Locale;
import java.util.TimeZone;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
/**
* This class provides formatting of <code>Date</code> values in a
* <code>FormattedText</code>. Supports a subset of date and time patterns
* defined in <code>SimpleDateFormat</code> for input.<p>
*
* <h4>Edit Patterns</h4>
* Edit patterns are composed of letters defining the parts of the mask, and
* characters defining the separators.<p>
* The following pattern letters are defined:
* <blockquote>
* <table border=0 cellspacing=3 cellpadding=0 summary="Chart shows pattern letters, date/time component, presentation, and examples.">
* <tr bgcolor="#ccccff">
* <th align=left>Letter</th>
* <th align=left>Date or Time Part</th>
* <th align=left>Examples</th>
* </tr>
* <tr>
* <td><code>y</code></td>
* <td>Year</td>
* <td><code>2006</code>; <code>06</code></td>
* </tr>
* <tr bgcolor="#eeeeff">
* <td><code>M</code></td>
* <td>Month in year</td>
* <td><code>07</code></td>
* </tr>
* <tr>
* <td><code>d</code></td>
* <td>Day in month</td>
* <td><code>10</code></td>
* </tr>
* <tr bgcolor="#eeeeff">
* <td><code>H</code></td>
* <td>Hour in day (0-23)</td>
* <td><code>0</code></td>
* </tr>
* <tr>
* <td><code>h</code></td>
* <td>Hour in am/pm (1-12)</td>
* <td><code>12</code></td>
* </tr>
* <tr bgcolor="#eeeeff">
* <td><code>m</code></td>
* <td>Minute in hour</td>
* <td><code>30</code></td>
* </tr>
* <tr>
* <td><code>s</code></td>
* <td>Second in minute</td>
* <td><code>55</code></td>
* </tr>
* <tr bgcolor="#eeeeff">
* <td><code>S</code></td>
* <td>Millisecond</td>
* <td><code>978</code></td>
* </tr>
* <tr>
* <td><code>a</code></td>
* <td>Am/pm marker</td>
* <td><code>PM</code></td>
* </tr>
* </table>
* </blockquote>
* Edit patterns are limited to numeric formats (except am/pm marker). Variable
* length fields and separators composed of more than one character are
* supported for input.
*
* <h4>Display Patterns</h4>
* Display patterns are associated to a <code>SimpleDateFormat</code> object.
* So they have to be compatible with it.
*
* <h4>Examples</h4>
* <ul>
* <li><code>new DateTimeFormatter("MM/dd/yyyy")</code> - 8 jul 2006 will edit
* and display as "07/08/2006".</li>
* <li><code>new DateTimeFormatter("d/M/yyyy H:m, "dd MMM yyyy HH:mm")</code>-
* 8 jul 2006, 15:05 will edit as "8/7/2006 15:5" and display as
* "08 Jul 2006 15:05".</li>
* </ul>
*/
public class DateTimeFormatter extends AbstractFormatter {
/** Cache of patterns by locale ISO3 codes */
protected static Hashtable cachedPatterns = new Hashtable();
/** Numbers formatter */
private static NumberFormat nf;
/** Calendar containing the current value */
protected Calendar calendar;
/** Date formatter for display */
protected SimpleDateFormat sdfDisplay;
/** Input mask */
protected StringBuffer inputMask;
/** Current edited value */
protected StringBuffer inputCache;
/** Fields descriptions */
protected FieldDesc[] fields;
/** Number of fields in edit pattern */
protected int fieldCount;
/** Year limit for 2 digits year field */
protected int yearStart;
/** Key listener on the Text widget */
protected KeyListener klistener;
/** Focus listener on the Text widget */
protected FocusListener flistener;
/** Filter for modify events */
protected Listener modifyFilter;
/** The Locale used by this formatter */
protected Locale locale;
private class FieldDesc {
/** Time field in Calendar */
int field;
/** Minimum length of the field in chars */
int minLen;
/** Maximum length of the field in chars */
int maxLen;
/** true if the field is empty, else false */
boolean empty;
/** true if the field contains a valid value, else false */
boolean valid;
char index;
int pos;
int curLen;
}
static {
nf = NumberFormat.getIntegerInstance();
nf.setGroupingUsed(false);
}
/**
* Constructs a new instance with all defaults :
* <ul>
* <li>edit mask in SHORT format for both date and time parts for the default locale</li>
* <li>display mask identical to the edit mask</li>
* <li>default locale</li>
* </ul>
*/
public DateTimeFormatter() {
this(null, null, Locale.getDefault());
}
/**
* Constructs a new instance with default edit and display masks for the given
* locale.
*
* @param loc locale
*/
public DateTimeFormatter(Locale loc) {
this(null, null, loc);
}
/**
* Constructs a new instance with the given edit mask. Display mask is
* identical to the edit mask, and locale is the default one.
*
* @param editPattern edit mask
*/
public DateTimeFormatter(String editPattern) {
this(editPattern, null, Locale.getDefault());
}
/**
* Constructs a new instance with the given edit mask and locale. Display mask
* is identical to the edit mask.
*
* @param editPattern edit mask
* @param loc locale
*/
public DateTimeFormatter(String editPattern, Locale loc) {
this(editPattern, null, loc);
}
/**
* Constructs a new instance with the given edit and display masks. Uses the
* default locale.
*
* @param editPattern edit mask
* @param displayPattern display mask
*/
public DateTimeFormatter(String editPattern, String displayPattern) {
this(editPattern, displayPattern, Locale.getDefault());
}
/**
* Constructs a new instance with the given masks and locale.
*
* @param editPattern edit mask
* @param displayPattern display mask
* @param loc locale
*/
public DateTimeFormatter(String editPattern, String displayPattern, Locale loc) {
// Set the default value
calendar = Calendar.getInstance(loc);
if ( yearStart == -1 ) {
calendar.setTime(sdfDisplay.get2DigitYearStart());
yearStart = calendar.get(Calendar.YEAR) % 100;
}
calendar.setTimeInMillis(0);
// Creates the formatter for the edit value
if ( editPattern == null ) {
editPattern = getDefaultEditPattern(loc);
}
compile(editPattern);
// Creates the formatter for the display value
if ( displayPattern == null ) {
displayPattern = editPattern;
}
sdfDisplay = new SimpleDateFormat(displayPattern, loc);
locale = loc;
// Instantiate the key listener
klistener = new KeyListener() {
public void keyPressed(KeyEvent e) {
if ( e.stateMask != 0 ) return;
switch ( e.keyCode ) {
case SWT.ARROW_UP :
arrow(1);
break;
case SWT.ARROW_DOWN :
arrow(-1);
break;
default :
return;
}
e.doit = false;
}
public void keyReleased(KeyEvent e) {
}
};
// Instantiate the focus listener
flistener = new FocusListener() {
String lastInput;
public void focusGained(FocusEvent e) {
int p = text.getCaretPosition();
setInputCache();
String t = inputCache.toString();
if ( ! t.equals(lastInput ) ) {
Display display = text.getDisplay();
try {
display.addFilter(SWT.Modify, modifyFilter);
updateText(inputCache.toString(), p);
} finally {
display.removeFilter(SWT.Modify, modifyFilter);
}
}
}
public void focusLost(FocusEvent e) {
if ( text != null ) {
lastInput = text.getText();
}
}
};
modifyFilter = new Listener() {
public void handleEvent(Event event) {
event.type = SWT.None;
}
};
}
/**
* Adjust a field length in the mask to a given length.
*
* @param b begin position (inclusive)
* @param e end position (exclusive)
* @param l new length
* @return new end position
*/
private int ajustMask(int b, int e, int l) {
char c = inputMask.charAt(b);
while ( e - b + 1 < l ) {
inputMask.insert(e++, c);
}
while ( e - b + 1 > l ) {
inputMask.deleteCharAt(e--);
}
return e;
}
/**
* Executes the increment or decrement of the date value when an arrow UP or
* DOWN is pressed.<br>
* The calendar field at the position of cursor is incremented / decremented.
* If the field value exceeds its range, the next larger field is incremented
* or decremented and the field value is adjusted back into its range.<p>
*
* If the calendar is empty, it is initialized with the current date. In these
* case, the arrow is ignored.
*
* @param inc +1 for increment, -1 for decrement
*/
private void arrow(int inc) {
int p = text.getCaretPosition();
int l = inputMask.length();
if ( p == l ) return;
char m = inputMask.charAt(p);
if ( m == '*' ) return;
FieldDesc f = getField(p, 0);
int b = f.pos;
if ( countValid() == 0 ) {
setValue(new Date());
} else if ( f.field == Calendar.YEAR && f.maxLen <= 2 ) {
int year = calendar.get(Calendar.YEAR) % 100 + inc;
year += year >= yearStart ? 1900 : 2000;
calendar.set(f.field, year);
} else {
calendar.add(f.field, inc);
}
setInputCache();
locateField(f, 0);
ignore = true;
updateText(inputCache.toString(),
Math.min(f.pos + p - b, f.pos + f.curLen - 1));
ignore = false;
}
/**
* Clear a part of the input cache.<br>
* Characters are replaced by spaces in the fields, but separators are
* preserved.
*
* @param b beginning index (inclusive)
* @param e end index (exclusive)
*/
private void clear(int b, int e) {
char m;
int i = b, from = 0;
FieldDesc field;
while ( i < e ) {
m = inputMask.charAt(i);
if ( m == '*' ) {
from = ++i;
continue;
}
field = getField(i, from);
final int numCharsLeftOfRangeToClear= i-field.pos;
while ( i < e && field.curLen - numCharsLeftOfRangeToClear > 0 ) {
inputCache.deleteCharAt(i);
inputMask.deleteCharAt(i);
e--;
field.curLen--;
}
while ( field.curLen < field.minLen ) {
inputCache.insert(field.pos + field.curLen, SPACE);
inputMask.insert(field.pos + field.curLen, field.index);
i++;
e++;
field.curLen++;
}
updateFieldValue(field, false);
}
}
/**
* Compiles a given edit pattern, initializing <code>inputMask</code> and
* <code>inputCache</code>. The content of the edit pattern is analyzed char
* by char and the array of field descriptors is initialized.
* Pattern chars allowed are : y, M, d, H, h, s, S, a.
* The presence of other pattern chars defined in <code>SimpleDateFormat</code>
* will raised an <code>IllegalArgumentException</code>.
*
* @param editPattern edit pattern
* @throws IllegalArgumentException pattern is invalid
*/
private void compile(String editPattern) {
inputMask = new StringBuffer();
inputCache = new StringBuffer();
fields = new FieldDesc[10];
int fi = 0;
int length = editPattern.length();
for (int i = 0; i < length; i++) {
char c = editPattern.charAt(i);
int l = 1;
while ( i < length - 1 && editPattern.charAt(i + 1) == c ) {
i++;
l++;
}
isValidCharPattern(c);
switch ( c ) {
case 'y' : // Year
fields[fi] = new FieldDesc();
fields[fi].field = Calendar.YEAR;
fields[fi].minLen = fields[fi].maxLen = l <= 2 ? 2 : 4;
if ( fields[fi].maxLen == 2 ) {
yearStart = -1;
}
break;
case 'M' : // Month
fields[fi] = new FieldDesc();
fields[fi].field = Calendar.MONTH;
fields[fi].minLen = Math.min(l, 2);
fields[fi].maxLen = 2;
break;
case 'd' : // Day in month
fields[fi] = new FieldDesc();
fields[fi].field = Calendar.DAY_OF_MONTH;
fields[fi].minLen = Math.min(l, 2);
fields[fi].maxLen = 2;
break;
case 'H' : // Hour (0-23)
fields[fi] = new FieldDesc();
fields[fi].field = Calendar.HOUR_OF_DAY;
fields[fi].minLen = Math.min(l, 2);
fields[fi].maxLen = 2;
break;
case 'h' : // Hour (1-12 AM-PM)
fields[fi] = new FieldDesc();
fields[fi].field = Calendar.HOUR;
fields[fi].minLen = Math.min(l, 2);
fields[fi].maxLen = 2;
break;
case 'm' : // Minutes
fields[fi] = new FieldDesc();
fields[fi].field = Calendar.MINUTE;
fields[fi].minLen = Math.min(l, 2);
fields[fi].maxLen = 2;
break;
case 's' : // Seconds
fields[fi] = new FieldDesc();
fields[fi].field = Calendar.SECOND;
fields[fi].minLen = Math.min(l, 2);
fields[fi].maxLen = 2;
break;
case 'S' : // Milliseconds
fields[fi] = new FieldDesc();
fields[fi].field = Calendar.MILLISECOND;
fields[fi].minLen = Math.min(l, 3);
fields[fi].maxLen = 3;
break;
case 'a' : // AM-PM marker
fields[fi] = new FieldDesc();
fields[fi].field = Calendar.AM_PM;
fields[fi].minLen = fields[fi].maxLen = 2;
break;
default :
for (int j = 0; j < l; j++) {
inputMask.append('*');
inputCache.append(c);
}
continue;
}
fields[fi].empty = true;
fields[fi].valid = false;
calendar.clear(fields[fi].field);
char k = (char) ('0' + fi);
for (int j = 0; j < fields[fi].minLen; j++) {
inputMask.append(k);
inputCache.append(SPACE);
}
fields[fi].index = k;
fi++;
}
fieldCount = fi;
}
/**
* Returns the count of valid fields. Value returned is between 0 and
* fieldcount.
*
* @return Count of valid fields
*/
private int countValid() {
int count = 0;
for (int i = 0; i < fieldCount; i++) {
if ( fields[i].valid ) count++;
}
return count;
}
/**
* Called when the formatter is replaced by an other one in the <code>FormattedText</code>
* control. Allow to release resources like additional listeners.<p>
*
* Removes the <code>KeyListener</code> on the text widget.
*
* @see ITextFormatter#detach()
*/
public void detach() {
text.removeKeyListener(klistener);
text.removeFocusListener(flistener);
}
/**
* Returns the default edit pattern for the given <code>Locale</code>.<p>
*
* A <code>DateFormat</code> object is instantiated with SHORT format for
* both date and time parts for the given locale. The corresponding pattern
* string is then retrieved by calling the <code>toPattern</code>.<p>
*
* Default patterns are stored in a cache with ISO3 language and country codes
* as key. So they are computed only once by locale.
*
* @param loc locale
* @return edit pattern for the locale
*/
public String getDefaultEditPattern(Locale loc) {
if ( loc == null ) {
loc = Locale.getDefault();
}
String key = "DT" + loc.toString();
String pattern = (String) cachedPatterns.get(key);
if ( pattern == null ) {
DateFormat df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, loc);
if ( ! (df instanceof SimpleDateFormat) ) {
throw new IllegalArgumentException("No default pattern for locale " + loc.getDisplayName());
}
StringBuffer buffer = new StringBuffer();
buffer.append(((SimpleDateFormat) df).toPattern());
int i;
if ( buffer.indexOf("yyy") < 0 && (i = buffer.indexOf("yy")) >= 0 ) {
buffer.insert(i, "yy");
}
pattern = buffer.toString();
cachedPatterns.put(key, pattern);
}
return pattern;
}
/**
* Returns the current value formatted for display.
* This method is called by <code>FormattedText</code> when the <code>Text</code>
* widget looses focus.
* The displayed value is the result of formatting on the <code>calendar</code>
* with a <code>SimpleDateFormat<code> for the display pattern passed in
* constructor. In case the input is invalid (eg. blanks fields), the edit
* string is returned in place of the display string.
*
* @return display string if valid, edit string else
* @see ITextFormatter#getDisplayString()
*/
public String getDisplayString() {
return isValid() ? sdfDisplay.format(calendar.getTime()) : getEditString();
}
/**
* Returns the current value formatted for input.
* This method is called by <code>FormattedText</code> when the <code>Text</code>
* widget gains focus.
* The value returned is the content of the StringBuilder used as cache.
*
* @return edit string
* @see ITextFormatter#getEditString()
*/
public String getEditString() {
return inputCache.toString();
}
/**
* Returns the field descriptor corresponding to a given position in the
* <code>inputMask</code>. The current starting position and length of the
* field are set in the descriptor.
*
* @param p position in mask of the field
* @param from starting position in mask to search for the beginning of the field
* @return Field descriptor
*/
private FieldDesc getField(int p, int from) {
FieldDesc f;
if ( p >= inputMask.length() ) {
p = inputMask.length() - 1;
}
char c = inputMask.charAt(p);
while ( c == '*' ) {
p--;
if ( p < 0 ) {
return null;
}
c = inputMask.charAt(p);
}
f = fields[c - '0'];
locateField(f, from);
return f;
}
/**
* Returns a string representing the current value of a given field based on
* the content of the calendar.
*
* @param f field descriptor
* @return formatted value of field
*/
private String getFormattedValue(FieldDesc f) {
StringBuffer value = new StringBuffer();
if ( f.valid ) {
int v = calendar.get(f.field);
switch ( f.field ) {
case Calendar.MONTH :
v++;
break;
case Calendar.HOUR :
if ( v == 0 ) v = 12;
break;
case Calendar.AM_PM :
return sdfDisplay.getDateFormatSymbols().getAmPmStrings()[v];
default:
break;
}
value.append(v);
if ( value.length() > f.maxLen ) {
value.delete(0, value.length() - f.maxLen);
} else while ( value.length() < f.minLen ) {
value.insert(0, '0');
}
} else {
while ( value.length() < f.minLen ) {
value.append(SPACE);
}
}
return value.toString();
}
/**
* Returns the current Locale used by this formatter.
*
* @return Current Locale used
*/
public Locale getLocale() {
return locale;
}
/**
* Returns the current value of the text control if it is a valid
* <code>Date</code>.<br>
* The date is valid if all the input fields are set. If invalid, returns
* <code>null</code>.
*
* @return current date value if valid, <code>null</code> else
* @see ITextFormatter#getValue()
*/
public Object getValue() {
return isValid() ? calendar.getTime() : null;
}
/**
* Returns the type of value this {@link ITextFormatter} handles,
* i.e. returns in {@link #getValue()}.<br>
* A DateTimeFormatter always returns a Date value.
*
* @return The value type.
*/
public Class getValueType() {
return Date.class;
}
/**
* Inserts a sequence of characters in the input buffer. The current content
* of the buffer is override. The new position of the cursor is computed and
* returned.
*
* @param txt String of characters to insert
* @param p Starting position of insertion
* @return New position of the cursor
*/
private int insert(String txt, int p) {
FieldDesc fd = null;
int i = 0, from = 0, t;
char c, m, o;
while ( i < txt.length() ) {
c = txt.charAt(i);
if ( p < inputMask.length() ) {
m = inputMask.charAt(p);
if ( m == '*' && inputCache.charAt(p) == c ) {
from = p;
i++;
p++;
fd = null;
continue;
}
} else {
m = inputMask.charAt(inputMask.length() - 1);
}
if ( fd == null ) {
fd = getField(p, from);
for (int j = fd.pos; j < p; j++) {
if ( inputCache.charAt(j) == SPACE ) {
inputCache.setCharAt(j, '0');
}
}
}
t = Character.getType(c);
if ( t == Character.DECIMAL_DIGIT_NUMBER && fd.field != Calendar.AM_PM ) {
o = '#';
if ( m != '*' && p < inputMask.length() ) {
o = inputCache.charAt(p);
inputCache.setCharAt(p, c);
} else if ( fd.curLen < fd.maxLen ) {
inputCache.insert(p, c);
inputMask.insert(p, inputMask.charAt(fd.pos));
fd.curLen++;
} else {
beep();
return p;
}
// if ( ! updateFieldValue(fd, p < fd.pos + fd.curLen - 1) ) {
if ( ! updateFieldValue(fd, true) ) {
if ( o != '#' ) {
inputCache.setCharAt(p, o);
} else {
inputCache.deleteCharAt(p);
inputMask.deleteCharAt(p);
fd.curLen--;
}
beep();
return p;
}
i++;
p++;
} else if ( fd.field == Calendar.AM_PM
&& (t == Character.UPPERCASE_LETTER
|| t == Character.LOWERCASE_LETTER) ) {
String[] ampm = sdfDisplay.getDateFormatSymbols().getAmPmStrings();
if ( Character.toLowerCase(c) == Character.toLowerCase(ampm[0].charAt(0)) ) {
t = 0;
} else if ( Character.toLowerCase(c) == Character.toLowerCase(ampm[1].charAt(0)) ) {
t = 1;
} else {
beep();
return p;
}
inputCache.replace(fd.pos, fd.pos + fd.curLen, ampm[t]);
while ( fd.curLen < ampm[t].length() ) {
inputMask.insert(p, m);
fd.curLen++;
}
while ( fd.curLen > ampm[t].length() ) {
inputMask.deleteCharAt(p);
fd.curLen--;
}
updateFieldValue(fd, false);
i++;
p = fd.pos + fd.curLen;
} else {
t = fd.pos + fd.curLen;
if ( t < inputCache.length() && c == inputCache.charAt(t) && i == txt.length() - 1 ) {
p = getNextFieldPos(fd);
} else {
beep();
}
return p;
}
}
if ( fd != null && p == fd.pos + fd.curLen && fd.curLen == fd.maxLen ) {
p = getNextFieldPos(fd);
}
return p;
}
/**
* Returns <code>true</code> if current edited value is empty, else returns
* <code>false</code>.<br>
* For a datetime, the value is considered empty if each field composing the
* datetime pattern contains an empty string.
*
* @return true if empty, else false
*/
public boolean isEmpty() {
for (int i = 0; i < fieldCount; i++) {
if ( ! fields[i].empty ) return false;
}
return true;
}
/**
* Returns <code>true</code> if current edited value is valid, else returns
* <code>false</code>. An empty value is considered as invalid.
*
* @return true if valid, else false
* @see ITextFormatter#isValid()
*/
public boolean isValid() {
return countValid() == fieldCount;
}
/**
* Checks if a given char is valid for the edit pattern. This method must be
* override to restrict the edit pattern in subclasses.
*
* @param c pattern char
* @throws IllegalArgumentException if not valid
*/
protected void isValidCharPattern(char c) {
switch (c) {
case 'D' :
case 'G' :
case 'w' :
case 'W' :
case 'F' :
case 'E' :
case 'k' :
case 'K' :
case 'z' :
case 'Z' :
throw new IllegalArgumentException("Invalid datetime pattern");
}
}
/**
* Searches the current start position and length of a given field, starting
* search to the given index.
*
* @param f field descriptor
* @param from index to begin to search
*/
private void locateField(FieldDesc f, int from) {
while ( inputMask.charAt(from) != f.index ) {
from++;
}
f.pos = from;
int l = from + 1;
while ( l < inputMask.length() && inputMask.charAt(l) == f.index ) {
l++;
}
f.curLen = l - from;
}
/**
* Returns the start position in cache of the next field of a given field.
*
* @param f field descriptor
* @return start position of next field
*/
private int getNextFieldPos(FieldDesc f) {
int p = f.pos + f.curLen;
while ( p < inputMask.length() && inputMask.charAt(p) == '*' ) {
p++;
}
return p;
}
/**
* Resets the full content of the input cache, based on input mask, fields
* descriptors and current value of the calendar.
*/
private void setInputCache() {
int l = inputCache.length();
for (int i = 0; i < l; i++) {
char c = inputMask.charAt(i);
if ( c == '*' ) {
continue;
}
int j = i;
while ( j < l - 1 && inputMask.charAt(j + 1) == c ) {
j++;
}
FieldDesc f = fields[c - '0'];
String value = getFormattedValue(f);
inputCache.replace(i, j + 1, value);
i = ajustMask(i, j, value.length());
l = inputCache.length();
}
}
/**
* Sets a new <code>Locale</code> on this formatter.
*
* @param loc locale
*/
public void setLocale(Locale loc) {
sdfDisplay.setDateFormatSymbols(new DateFormatSymbols(loc));
Calendar newCal = Calendar.getInstance(calendar.getTimeZone(), loc);
newCal.setTimeInMillis(0);
for (int i = 0; i < fieldCount; i++) {
if ( fields[i].valid ) {
newCal.set(fields[i].field, calendar.get(fields[i].field));
} else {
newCal.clear(fields[i].field);
}
}
calendar = newCal;
locale = loc;
}
/**
* Sets the <code>Text</code> widget that will be managed by this formatter.<p>
*
* The ancestor is override to add a key listener on the text widget.
*
* @param text Text widget
* @see ITextFormatter#setText(Text)
*/
public void setText(Text text) {
super.setText(text);
text.addKeyListener(klistener);
text.addFocusListener(flistener);
}
/**
* Sets the time zone with the given time zone value. The time zone is applied
* to both the <code>Calendar</code> used as value cache, and the
* <code>SimpleDateFormat</code> used for display mask.
*
* @param zone Time zone
*/
public void setTimeZone(TimeZone zone) {
if ( zone == null ) SWT.error(SWT.ERROR_NULL_ARGUMENT);
sdfDisplay.setTimeZone(zone);
calendar.setTimeZone(zone);
}
/**
* Sets the value to edit. The value provided must be a <code>Date</code>.
*
* @param value date value
* @throws IllegalArgumentException if not a date
* @see ITextFormatter#setValue(java.lang.Object)
*/
public void setValue(Object value) {
if ( value instanceof Date ) {
if ( value != null ) {
calendar.setTime((Date) value);
}
for (int i = 0; i < fieldCount; i++) {
fields[i].valid = (value != null);
fields[i].empty = ! fields[i].valid;
}
setInputCache();
} else if ( value == null ) {
clear(0, inputCache.length());
} else {
SWT.error(SWT.ERROR_INVALID_ARGUMENT);
}
}
/**
* Update a calendar field with the current value string of the given field
* in the input cache. The string value is converted according to the field
* type. If the conversion is invalid, or if the value is out of the field
* limits, it is rejected. Else the corresponding field is updated in the
* calendar.
*
* If the checkLimits flag is set to true, we try to replace the last digit
* of the field by 0 (if over max) or a 1 (if under min). If the resulting
* value is valid, then the input cache is updated.
*
* @param f field descriptor
* @param checkLimits <code>true</code> to check limits, else <code>false</code>
* @return <code>true</code> if calendar has been updated, <code>false</code> if value is rejected
*/
private boolean updateFieldValue(FieldDesc f, boolean checkLimits) {
String s = inputCache.substring(f.pos, f.pos + f.curLen).trim();
f.empty = false;
if ( s.length() == 0 || s.indexOf(SPACE) >= 0 ) {
calendar.clear(f.field);
f.empty = true;
f.valid = false;
} else if ( f.field == Calendar.AM_PM ) {
calendar.set(f.field, sdfDisplay.getDateFormatSymbols()
.getAmPmStrings()[0].equals(s) ? 0 : 1);
f.valid = true;
} else {
int v = 0;
try {
v = nf.parse(s).intValue();
} catch (ParseException e) {
e.printStackTrace(System.err);
}
if ( v == 0 && f.field <= Calendar.DAY_OF_MONTH && s.length() < f.maxLen) {
calendar.clear(f.field);
f.valid = false;
} else {
if ( f.field == Calendar.YEAR && f.maxLen <= 2 ) {
v += v >= yearStart ? 1900 : 2000;
} else if ( f.field == Calendar.HOUR && v == 12 ) {
v = 0;
}
int min = calendar.getActualMinimum(f.field);
int max = calendar.getActualMaximum(f.field);
if ( f.field == Calendar.MONTH ) {
min++;
max++;
}
if ( v < min || v > max ) {
if ( ! checkLimits ) {
return false;
}
if ( v > max ) {
v = (v / 10) * 10;
if ( v > max ) {
return false;
}
inputCache.setCharAt(f.pos + f.curLen - 1, '0');
} else if ( f.curLen == f.maxLen ) {
v = (v / 10) * 10 + 1;
if ( v < min ) {
return false;
}
inputCache.setCharAt(f.pos + f.curLen - 1, '1');
}
}
calendar.set(f.field, f.field == Calendar.MONTH ? v - 1 : v);
f.valid = true;
}
}
return true;
}
/**
* Handles a <code>VerifyEvent</code> sent when the text is about to be modified.
* This method is the entry point of all operations of formatting.
*
* @see org.eclipse.swt.events.VerifyListener#verifyText(org.eclipse.swt.events.VerifyEvent)
*/
public void verifyText(VerifyEvent e) {
if ( ignore ) {
return;
}
e.doit = false;
if ( e.keyCode == SWT.BS || e.keyCode == SWT.DEL ) {
clear(e.start, e.end);
} else {
e.start = insert(e.text, e.start);
}
updateText(inputCache.toString(), e.start);
}
}