/****************************************************************************
* Copyright (c) 2006-2008 Jeremy Dowdall
* 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:
* Jeremy Dowdall <jeremyd@aspencloud.com> - initial API and implementation
*****************************************************************************/
package org.eclipse.nebula.widgets.cdatetime;
import java.text.AttributedCharacterIterator;
import java.text.CharacterIterator;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.text.DateFormat.Field;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import org.eclipse.nebula.cwt.base.BaseCombo;
import org.eclipse.nebula.cwt.v.VButton;
import org.eclipse.nebula.cwt.v.VCanvas;
import org.eclipse.nebula.cwt.v.VGridLayout;
import org.eclipse.nebula.cwt.v.VLabel;
import org.eclipse.nebula.cwt.v.VLayout;
import org.eclipse.nebula.cwt.v.VNative;
import org.eclipse.nebula.cwt.v.VPanel;
import org.eclipse.nebula.cwt.v.VTracker;
import org.eclipse.nebula.widgets.cdatetime.CDT.PickerPart;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.TypedListener;
/**
* The CDateTime provides both textual and graphical means selecting a date.<br/>
* As with other combo type widgets, there are three basic styles:
* <ul>
* <li>Text only (default)</li>
* <li>Graphical only (CDT.SIMPLE)</li>
* <li>Combo - a text selector with a drop-down graphical selector (CDT.DROP_DOWN)</li>
* </ul>
* <p>
* Styles are set using the constants provided in the CDT class.
* </p>
* @see CDT
*/
public class CDateTime extends BaseCombo {
/**
* A simple class used for editing a field numerically.
*/
private class EditField {
private String buffer;
private int digits;
private int count = 0;
EditField(int digits, int initialValue) {
this.digits = digits;
buffer = Integer.toString(initialValue);
}
boolean addChar(char c) {
if(Character.isDigit(c)) {
buffer = (count > 0) ? buffer : ""; //$NON-NLS-1$
buffer += String.valueOf(c);
if(buffer.length() > digits) {
buffer = buffer.substring(buffer.length()-digits, buffer.length());
}
}
return(++count > (digits - 1));
}
int getValue() {
return Integer.parseInt(buffer);
}
void removeLastCharacter() {
if(buffer.length() > 0) {
buffer = buffer.substring(0, buffer.length() - 1);
count--;
}
}
void reset() {
count = 0;
}
public String toString() {
if(buffer.length() < digits) {
char[] ca = new char[digits - buffer.length()];
Arrays.fill(ca, '0');
buffer = String.valueOf(ca).concat(buffer);
}
return buffer;
}
}
/**
* The layout used for a "basic" CDateTime - when it is neither
* of style SIMPLE or DROP_DOWN - with style of SPINNER.<br>
* Note that there is a spinner, but no button for this style.
*/
class SpinnerLayout extends VLayout {
protected Point computeSize(VPanel panel, int wHint, int hHint, boolean flushCache) {
Point size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
Rectangle sRect = spinner.getControl().computeTrim(0, 0, 0, 0);
int sWidth = sRect.x + sRect.width - (2 * spinner.getControl().getBorderWidth()) + 1;
size.x += sWidth;
size.x++;
size.y += textMarginHeight;
if(wHint != SWT.DEFAULT) {
size.x = Math.min(size.x, wHint);
}
if(hHint != SWT.DEFAULT) {
size.y = Math.min(size.y, hHint);
}
return size;
}
protected void layout(VPanel panel, boolean flushCache) {
Rectangle cRect = panel.getClientArea();
if(cRect.isEmpty()) return;
Point tSize = text.getControl().computeSize(SWT.DEFAULT, SWT.DEFAULT);
tSize.y += textMarginHeight;
spinner.setBounds(
cRect.x,
cRect.y,
cRect.width,
tSize.y
);
Rectangle sRect = spinner.getControl().computeTrim(0, 0, 0, 0);
int sWidth = sRect.x + sRect.width - (2 * spinner.getControl().getBorderWidth()) + 1;
tSize.x = cRect.width - sWidth;
text.setBounds(
cRect.x,
cRect.y + getBorderWidth(),
tSize.x,
tSize.y
);
}
}
private static final int FIELD_NONE = -1;
private static final int DISCARD = 0;
private static final int WRAP = 1;
private static final int BLOCK = 2;
private static int convertStyle(int style) {
int rstyle = SWT.NONE;
if((style & CDT.DROP_DOWN) != 0) {
rstyle |= SWT.DROP_DOWN;
}
if((style & CDT.SIMPLE) != 0) {
rstyle |= SWT.SIMPLE;
}
if((style & CDT.READ_ONLY) != 0) {
rstyle |= SWT.READ_ONLY;
}
if((style & CDT.TEXT_LEAD) != 0) {
rstyle |= SWT.LEAD;
}
if((style & CDT.BORDER) != 0) {
rstyle |= SWT.BORDER;
}
if(win32) {
rstyle |= SWT.DOUBLE_BUFFERED;
}
return rstyle;
}
VPanel picker;
VNative<Spinner> spinner;
boolean internalFocusShift = false;
boolean rightClick = false;
private Date cancelDate;
private Calendar calendar;
private DateFormat df;
Locale locale;
TimeZone timezone;
Field[] field;
int activeField;
private boolean tabStops = false;
// Store these values so that the style can be reset automatically
// to update everything if/when the locale is changed
int style;
String pattern = null;
int format = -1;
private CDateTimePainter painter;
/**
* Delegates events to their appropriate handler
*/
Listener textListener = new Listener() {
public void handleEvent(Event event) {
switch (event.type) {
case SWT.FocusIn:
rightClick = false;
if(internalFocusShift) {
if(activeField < 0) {
fieldFirst();
updateText();
}
} else {
if(VTracker.getLastTraverse() == SWT.TRAVERSE_TAB_PREVIOUS) {
fieldLast();
} else {
fieldFirst();
}
updateText();
}
break;
case SWT.FocusOut:
if(!rightClick && !internalFocusShift) {
setActiveField(FIELD_NONE);
updateText();
}
break;
case SWT.KeyDown:
handleKey(event);
break;
case SWT.MouseDown:
if(event.button == 1) {
fieldFromTextSelection();
} else if(event.button == 2) {
fieldNext();
} else if(event.button == 3) {
rightClick = true;
}
break;
case SWT.MouseWheel:
if(event.count > 0) {
fieldAdjust(1);
} else {
fieldAdjust(-1);
}
event.doit = false;
break;
case SWT.MouseUp:
if(event.button == 1) {
fieldFromTextSelection();
}
break;
case SWT.Traverse:
handleTraverse(event);
break;
case SWT.Verify:
verify(event);
break;
}
}
};
private Point textSelectionOffset = new Point(0,0); // x = selOffset start, y = selOffset amount
private EditField editField;
private String[] separator;
private int[] calendarFields;
private boolean isTime;
private boolean isDate;
// private boolean isNull = true;
private String nullText = null;
private boolean defaultNullText = true;
private boolean singleSelection;
// private boolean dragSelection;
private Date[] selection = new Date[0];
private boolean scrollable = true;
CDateTimeBuilder builder;
VPanel pickerPanel;
/**
* Constructs a new instance of this class given its parent and a style value
* describing its behavior and appearance. The current date and the system's
* default locale are used.
* @param parent a widget which will be the parent of the new instance (cannot be null)
* @param style the style of widget to construct
*/
public CDateTime(Composite parent, int style) {
super(parent, convertStyle(style));
init(style);
}
/**
* Adds the listener to the collection of listeners who will be notified when the
* receiver's selection changes, by sending it one of the messages defined in the
* <code>SelectionListener</code> interface.
* <p>
* <code>widgetSelected</code> is called when the selection (date/time) changes.
* <code>widgetDefaultSelected</code> is when ENTER is pressed the text box.
* </p>
* The event's data field will contain the newly selected Date object.<br>
* The event's detail field will contain which Calendar Field was changed
* @param listener the listener which should be notified
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
* </ul>
* @see SelectionListener
* @see #removeSelectionListener
* @see SelectionEvent
*/
public void addSelectionListener(SelectionListener listener) {
if(listener != null) {
TypedListener typedListener = new TypedListener (listener);
addListener (SWT.Selection, typedListener);
addListener (SWT.DefaultSelection, typedListener);
}
}
/**
* Adds the textListener for the appropriate SWT events to handle incrementing fields.
*/
protected void addTextListener() {
removeTextListener();
Text control = text.getControl();
control.addListener(SWT.FocusIn, textListener);
control.addListener(SWT.FocusOut, textListener);
control.addListener(SWT.KeyDown, textListener);
control.addListener(SWT.MouseDown, textListener);
control.addListener(SWT.MouseWheel, textListener);
control.addListener(SWT.MouseUp, textListener);
control.addListener(SWT.Verify, textListener);
text.addListener(SWT.Traverse, textListener);
}
/**
* If a field is being edited (via keyboard), set the edit value to the
* active field of the calendar. Reset the count of the EditField so that a
* subsequent key press will overwrite its contents;
* @return true if the commit was successfull (the value was valid for the field) or there
* was no commit to be made (editField is null), false otherwise
*/
private boolean commitEditField() {
if(editField != null) {
int cf = getCalendarField();
int val = editField.getValue();
editField.reset();
if(cf == Calendar.MONTH) {
val--;
}
return fieldSet(cf, val, DISCARD);
}
return true;
}
/**
* If style is neither SIMPLE or DROP_DOWN, then this method simply returns,
* otherwise it creates the picker.
*/
private void createPicker() {
if(isSimple()) {
pickerPanel = panel;
setContent(panel.getComposite());
} else if(isDropDown()) {
disposePicker();
Shell shell = getContentShell();
int style = (isSimple() ? SWT.NONE : SWT.BORDER) | SWT.DOUBLE_BUFFERED;
VCanvas canvas = new VCanvas(shell, style);
pickerPanel = canvas.getPanel();
pickerPanel.setWidget(canvas);
VGridLayout layout = new VGridLayout();
layout.marginHeight = 0;
layout.marginWidth = 0;
layout.verticalSpacing = 1;
pickerPanel.setLayout(layout);
setContent(pickerPanel.getComposite());
canvas.addListener(SWT.KeyDown, new Listener() {
public void handleEvent(Event event) {
if(SWT.ESC == event.keyCode) {
event.doit = false;
if(selection.length > 0 && selection[0] != cancelDate) {
setSelection(cancelDate);
}
setOpen(false);
}
}
});
if(field.length > 1 || isTime) {
createPickerToolbar(pickerPanel);
}
}
if(isDate) {
DatePicker dp = new DatePicker(this);
dp.setScrollable(scrollable);
dp.setFields(calendarFields);
dp.updateView();
picker = dp;
} else if(isTime) {
if((style & CDT.CLOCK_DISCRETE) != 0) {
DiscreteTimePicker dtp = new DiscreteTimePicker(this);
dtp.setFields(calendarFields);
dtp.updateView();
picker = dtp;
} else {
AnalogTimePicker atp = new AnalogTimePicker(this);
atp.setFields(calendarFields);
atp.updateView();
picker = atp;
}
}
if(isDropDown()) {
picker.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
}
}
private void createPickerToolbar(VPanel parent) {
VPanel tb = new VPanel(parent, SWT.NONE);
VGridLayout layout = new VGridLayout(3, false);
layout.marginHeight = 0;
layout.marginWidth = 0;
layout.horizontalSpacing = 2;
tb.setLayout(layout);
tb.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false));
tb.setData(CDT.PickerPart, PickerPart.Toolbar);
VButton b = new VButton(tb, SWT.OK | SWT.NO_FOCUS);
b.setData(CDT.PickerPart, PickerPart.OkButton);
b.setToolTipText(Resources.getString("accept.text", locale)); //$NON-NLS-1$
b.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
b.addListener(SWT.Selection, new Listener() {
public void handleEvent(Event event) {
setOpen(false);
}
});
b = new VButton(tb, SWT.CANCEL | SWT.NO_FOCUS);
b.setData(CDT.PickerPart, PickerPart.CancelButton);
b.setToolTipText(Resources.getString("cancel.text", locale)); //$NON-NLS-1$
b.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
b.addListener(SWT.Selection, new Listener() {
public void handleEvent(Event event) {
setSelection(cancelDate);
setOpen(false);
}
});
b = new VButton(tb, SWT.NO_FOCUS);
b.setData(CDT.PickerPart, PickerPart.ClearButton);
b.setText(Resources.getString("clear.text", locale)); //$NON-NLS-1$
b.setToolTipText(Resources.getString("clear.text", locale)); //$NON-NLS-1$
b.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
b.addListener(SWT.Selection, new Listener() {
public void handleEvent(Event event) {
setOpen(false);
setSelection(null);
fireSelectionChanged();
}
});
VLabel sep = new VLabel(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
sep.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
}
// void deselect(Date date) {
// if(date != null && isSelected(date)) {
// Date[] tmp = new Date[selection.length - 1];
// for(int i = 0, j = 0; i < selection.length; i++) {
// if(!selection[i].equals(date)) {
// tmp[j++] = selection[i];
// }
// }
// setSelection(tmp);
// }
// }
//
// void deselectAll() {
// setSelectedDates((Date[]) null);
// }
private void disposePicker() {
if(content != null) {
if(picker != null) {
picker.dispose();
picker = null;
}
if(isDropDown()) {
Control c = content;
setContent(null);
c.dispose();
if(contentShell != null) {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
if(contentShell != null && !contentShell.isDisposed()) {
contentShell.dispose();
contentShell = null;
}
}
});
}
}
}
}
/**
* Adds the given amount to the active field, if there is one
*/
void fieldAdjust(int amount) {
if(!hasSelection()) {
setSelection(calendar.getTime());
fireSelectionChanged();
} else {
int cf = getCalendarField();
if(cf >= 0) {
fieldSet(cf, calendar.get(cf) + amount, WRAP);
}
}
}
void fieldFirst() {
if(Calendar.ZONE_OFFSET == getCalendarField(field[0])) {
setActiveField(1);
} else {
setActiveField(0);
}
}
/**
* Sets the active field from the select of the text box
*/
void fieldFromTextSelection() {
if(!hasSelection()) {
// setActiveField(FIELD_ALL);
fieldNext();
} else {
Point sel = text.getControl().getSelection();
AttributedCharacterIterator aci = df.formatToCharacterIterator(calendar.getTime());
if(sel.x > textSelectionOffset.x) sel.x += textSelectionOffset.y;
aci.setIndex(sel.x);
Object[] oa = aci.getAttributes().keySet().toArray();
if(oa.length == 0 && sel.x > 0) {
sel.x -= 1;
aci.setIndex(sel.x);
oa = aci.getAttributes().keySet().toArray();
}
if(oa.length > 0) {
for(int i = 0; i < field.length; i++) {
if(oa[0].equals(field[i])) {
if(Calendar.ZONE_OFFSET != getCalendarField(field[i])) {
setActiveField(i);
}
break;
}
}
updateText();
}
}
}
void fieldLast() {
if(Calendar.ZONE_OFFSET == getCalendarField(field[field.length-1])) {
setActiveField(field.length-2);
} else {
setActiveField(field.length-1);
}
}
/**
* Sets the active field to the next field; wraps if necessary and sets to last
* field if there is no current active field
*/
void fieldNext() {
fieldNext(false);
}
/**
* Sets the active field to the next field; wraps if necessary and sets to last
* field if there is no current active field
* @param If true, the text update will be asynchronous (for changes to text selection)
*/
void fieldNext(boolean async) {
if(activeField >= 0 && activeField < field.length - 1) {
if(Calendar.ZONE_OFFSET == getCalendarField(field[activeField + 1])) {
if(activeField < field.length - 2) {
setActiveField(activeField + 2);
} else {
setActiveField(0);
}
} else {
setActiveField(activeField + 1);
}
} else {
if(Calendar.ZONE_OFFSET == getCalendarField(field[0])) {
setActiveField(1);
} else {
setActiveField(0);
}
}
updateText(async);
}
/**
* Sets the active field to the previous field; wraps if necessary and sets to first
* field if there is no current active field
*/
private void fieldPrev() {
fieldPrev(false);
}
/**
* Sets the active field to the previous field; wraps if necessary and sets to first
* field if there is no current active field
* @param If true, the text update will be asynchronous (for changes to text selection)
*/
void fieldPrev(boolean async) {
if(activeField > 0 && activeField < field.length) {
if(Calendar.ZONE_OFFSET == getCalendarField(field[activeField - 1])) {
if(activeField > 1) {
setActiveField(activeField - 2);
} else {
setActiveField(field.length - 1);
}
} else {
setActiveField(activeField - 1);
}
} else {
if(Calendar.ZONE_OFFSET == getCalendarField(field[field.length - 1])) {
setActiveField(field.length - 2);
} else {
setActiveField(field.length - 1);
}
}
updateText(async);
}
/**
* Sets the given calendar field to the given value.<br>
* <b>NOTE:</b> This is NOT the active field but a field in the
* "calendar" variable.
* @param calendarField the field of calendar to set
* @param value the value to set it to
* @param style the of set to perform; if the value is valid for the given calendarField then
* this has no affect, otherwise it will take an action according to this style int:
* <ul>
* <li>DISCARD: the value will be discarded and the method returns without performing and action</li>
* <li>WRAP: if value is higher than its maximum it will be set to its minimum, and visa versa</li>
* <li>BLOCK: if value is higher than its maximum it will be set to its maximum, and visa versa</li>
* </ul>
* @return true if the field was set, false otherwise (as is possible with a DISCARD style)
*/
private boolean fieldSet(int calendarField, int value, int style) {
if(!getEditable()) {
return false;
}
if(calendarField >= 0) {
if(value > calendar.getActualMaximum(calendarField)) {
if(style == DISCARD) {
return false;
} else if(style == WRAP) {
value = calendar.getActualMinimum(calendarField);
} else if(style == BLOCK) {
value = calendar.getActualMaximum(calendarField);
}
} else if(value < calendar.getActualMinimum(calendarField)) {
if(style == DISCARD) {
return false;
} else if(style == WRAP) {
value = calendar.getActualMaximum(calendarField);
} else if(style == BLOCK) {
value = calendar.getActualMinimum(calendarField);
}
}
calendar.set(calendarField, value);
if(selection.length > 0) {
selection[0] = calendar.getTime();
}
updateText();
updatePicker();
fireSelectionChanged(calendarField);
}
return true;
}
/**
* <p>Notifies listeners that the selection for this CDateTime has changed</p>
* <p>This will fire both a regular selection event, and a default selection event.</p>
* <p>The data field is populated by {@link #getSelectedDates()}.</p>
*/
void fireSelectionChanged() {
fireSelectionChanged(false);
}
void fireSelectionChanged(boolean defaultSelection) {
if(defaultSelection && isOpen()) {
setOpen(false);
}
Event event = new Event();
event.data = getSelection();
notifyListeners(SWT.Selection, event);
if(defaultSelection) {
notifyListeners(SWT.DefaultSelection, event);
}
}
/**
* <p>Notifies listeners that a field of the selected date for this CDateTime has changed</p>
* <p>Note that this is only valid when {@link #singleSelection} is true, and will only
* fire a regular selection event (not a default selection event)</p>
* <p>The data field is populated by {@link #getSelection()} and the detail field holds
* the field which was changed.</p>
* @param field the Calendar Field which caused the change, or -1 if <code>setTime</code>
* was called (thus setting all Calendar Fields)
*/
void fireSelectionChanged(int field) {
Event event = new Event();
event.data = getSelection();
event.detail = field;
if(this.field.length == 1) {
if(isOpen()) {
setOpen(false);
}
notifyListeners(SWT.Selection, event);
notifyListeners(SWT.DefaultSelection, event);
} else {
notifyListeners(SWT.Selection, event);
}
}
VButton getButtonWidget() {
return button;
}
/**
* Gets the calendar field corresponding to the active field, if there is one.
* @return an int representing the calendar field, -1 if there isn't one.
*/
int getCalendarField() {
return hasField(activeField) ? getCalendarField(field[activeField]) : -1;
}
int getCalendarField(Field field) {
int cf = field.getCalendarField();
if(cf < 0) {
if(field.toString().indexOf("hour 1") > -1) { //$NON-NLS-1$
cf = Calendar.HOUR;
} else if(field.toString().contains("zone")) { //$NON-NLS-1$
cf = Calendar.ZONE_OFFSET;
}
}
return cf;
}
Calendar getCalendarInstance() {
return getCalendarInstance(calendar.getTimeInMillis());
}
/**
* <p><b>WARNING: Experimental API - this method may be removed in future versions</b></p>
* Get a new instance of Calendar that is initialized with
* the timezone and locale of this CDateTime, and set to the
* given date.
* @param date the date that the Calendar will be set to, or null for the current system time
* @return a new instance of Calendar
*/
public Calendar getCalendarInstance(Date date) {
if(date == null) {
return getCalendarInstance(System.currentTimeMillis());
} else {
return getCalendarInstance(date.getTime());
}
}
/**
* <p><b>WARNING: Experimental API - this method may be removed in future versions</b></p>
* Get a new instance of Calendar that is initialized with
* the timezone and locale of this CDateTime, and set to the
* given date.
* @param date the date, in millis, that the Calendar will be set to
* @return a new instance of Calendar
*/
public Calendar getCalendarInstance(long date) {
Calendar cal = Calendar.getInstance(timezone, locale);
cal.setTimeInMillis(date);
return cal;
}
Date getCalendarTime() {
return calendar.getTime();
}
long getCalendarTimeInMillis() {
return calendar.getTimeInMillis();
}
public boolean getEditable() {
return !panel.hasStyle(SWT.READ_ONLY);
}
/**
* The locale currently in use by this CDateTime.
* @return the locale
* @see #setLocale(Locale)
*/
public Locale getLocale() {
return locale;
}
/**
* Get the text which will be shown when the selection is set to null.
* Note that this will be equal to the default null text for the given
* locale unless the null text has been explicitly set using
* {@link #setNullText(String)}
* @return the text shown when the selection is null
* @see #setNullText(String)
*/
public String getNullText() {
if(nullText == null) {
if(isDate) {
return Resources.getString("null_text.date", locale); //$NON-NLS-1$
} else {
return Resources.getString("null_text.time", locale); //$NON-NLS-1$
}
}
return nullText;
}
CDateTimePainter getPainter() {
if(painter == null) {
setPainter(new CDateTimePainter());
}
return painter;
}
/**
* Get the pattern of this CDateTime as used to set its format. If the format was NOT
* set using <code>setFormat(String)</code> this will return <code>null</code>.
* @return the pattern, null if there isn't one
* @see SimpleDateFormat
* @see #setFormat(int)
* @see #setPattern(String)
*/
public String getPattern() {
return pattern;
}
/**
* Get the current selection of this CDateTime widget, or null
* if there is no selection.
* @return the current selection
*/
public Date getSelection() {
return hasSelection() ? selection[0] : null;
}
public int getStyle() {
return style;
}
public String getText() {
return checkText() ? text.getText() : null;
}
VNative<Text> getTextWidget() {
return text;
}
/**
* The timezone currently in use by this CDateTime.
* @return the timezone
* @see #setTimeZone(String)
* @see #setTimeZone(TimeZone)
*/
public TimeZone getTimeZone() {
return timezone;
}
/**
* The Key event handler
* @param event the event
*/
void handleKey(Event event) {
if(event.stateMask != 0) {
return;
}
if('\r' == event.keyCode || SWT.KEYPAD_CR == event.keyCode) {
fireSelectionChanged(true);
} else if(SWT.BS == event.keyCode || SWT.DEL == event.keyCode) {
event.doit = false;
setSelection((Date) null);
fireSelectionChanged();
} else if(!hasField(activeField) && !hasSelection()) {
event.doit = false;
} else {
switch (event.keyCode) {
case '-':
case SWT.KEYPAD_SUBTRACT:
fieldAdjust(-1);
break;
case '=':
case '+':
case SWT.KEYPAD_ADD:
fieldAdjust(1);
break;
case SWT.BS:
if(editField != null) editField.removeLastCharacter();
break;
case SWT.ARROW_DOWN:
fieldAdjust(-1);
updateText(true);
break;
case SWT.ARROW_UP:
fieldAdjust(1);
updateText(true);
break;
case SWT.ARROW_LEFT:
fieldPrev(true);
break;
case SWT.ARROW_RIGHT:
fieldNext(true);
break;
default:
if(hasField(activeField) && activeField + 1 < separator.length &&
String.valueOf(event.character).equals(separator[activeField+1])) {
fieldNext();
}
}
}
}
/**
* The Travers event handler. Note that ARROW_UP and ARROW_DOWN are
* handled in the <code>handleKey</code> method.
* @param event the event
*/
void handleTraverse(Event event) {
switch (event.detail) {
case SWT.TRAVERSE_ARROW_NEXT:
if(event.keyCode == SWT.ARROW_RIGHT) {
fieldNext();
} else if(event.keyCode == SWT.ARROW_DOWN) {
fieldAdjust(-1);
}
break;
case SWT.TRAVERSE_ARROW_PREVIOUS:
if(event.keyCode == SWT.ARROW_LEFT) {
fieldPrev();
} else if(event.keyCode == SWT.ARROW_UP) {
fieldAdjust(1);
}
break;
case SWT.CR:
fieldNext();
fireSelectionChanged();
break;
case SWT.TRAVERSE_TAB_NEXT:
if(tabStops && hasSelection()) {
if(activeField == field.length - 1
|| (activeField == field.length - 2 && Calendar.ZONE_OFFSET == getCalendarField(field[field.length - 1]))) {
event.doit = true;
} else {
event.doit = false;
if(activeField < 0) {
fieldPrev();
} else {
fieldNext();
}
}
}
break;
case SWT.TRAVERSE_TAB_PREVIOUS:
if(tabStops && hasSelection()) {
if(activeField == 0 || (activeField == 1 && Calendar.ZONE_OFFSET == getCalendarField(field[0]))) {
event.doit = true;
} else {
event.doit = false;
if(activeField < 0) {
fieldNext();
} else {
fieldPrev();
}
}
}
break;
default:
}
}
/**
* Determines if the given field number is backed by a real field.
* @param field the field number to check
* @return true if the given field number corresponds to a field in the field array
*/
private boolean hasField(int field) {
return field >= 0 && field <= this.field.length;
}
/**
* Return true if this CDateTime has one or more dates selected;
* @return true if a date is selected, false otherwise
*/
public boolean hasSelection() {
return selection.length > 0;
}
private void init(int style) {
this.style = style;
locale = Locale.getDefault();
try {
timezone = TimeZone.getDefault();
} catch (Exception e) {
timezone = TimeZone.getTimeZone("GMT"); //$NON-NLS-1$
}
calendar = Calendar.getInstance(this.timezone, this.locale);
calendar.setTime(new Date());
tabStops = (style & CDT.TAB_FIELDS) != 0;
singleSelection = ((style & CDT.SIMPLE) == 0) || ((style & CDT.MULTI) == 0);
setFormat(style);
if(!isSimple()) {
if(isDropDown()) {
if((style & CDT.BUTTON_AUTO) != 0) {
setButtonVisibility(BaseCombo.BUTTON_AUTO);
} else {
setButtonVisibility(BaseCombo.BUTTON_ALWAYS);
}
} else {
setButtonVisibility(BaseCombo.BUTTON_NEVER);
if((style & CDT.SPINNER) != 0) {
int sStyle = SWT.VERTICAL;
if(gtk && ((style & CDT.BORDER) != 0)) {
sStyle |= SWT.BORDER;
}
spinner = VNative.create(Spinner.class, panel, sStyle);
if(win32) {
spinner.setBackground(text.getControl().getBackground());
}
spinner.getControl().setMinimum(0);
spinner.getControl().setMaximum(50);
spinner.getControl().setDigits(1);
spinner.getControl().setIncrement(1);
spinner.getControl().setPageIncrement(1);
spinner.getControl().setSelection(25);
spinner.getControl().addFocusListener(new FocusAdapter() {
public void focusGained(FocusEvent e) {
internalFocusShift = true;
setFocus();
internalFocusShift = false;
}
});
spinner.getControl().addMouseListener(new MouseAdapter() {
public void mouseDown(MouseEvent e) {
if(e.button == 2) {
fieldNext();
}
}
});
spinner.getControl().addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
if(VTracker.getMouseDownButton() != 2) {
if(spinner.getControl().getSelection() > 25) {
fieldAdjust(1);
} else {
fieldAdjust(-1);
}
spinner.getControl().setSelection(25);
}
}
});
panel.setLayout(new SpinnerLayout());
}
}
updateText();
activeField = -5;
setActiveField(FIELD_NONE);
if(checkText()) {
addTextListener();
}
}
}
boolean isSelected(Date date) {
for(Date d : selection) {
if(d.equals(date)) {
return true;
}
}
return false;
}
boolean isSingleSelection() {
return singleSelection;
}
@Override
protected void postClose(Shell popup) {
disposePicker();
}
/**
* Removes the listener from the collection of listeners who will
* be notified when the receiver's selection changes.
* @param listener the listener which should no longer be notified
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that
* created the receiver</li>
* </ul>
* @see SelectionListener
* @see #addSelectionListener
*/
public void removeSelectionListener(SelectionListener listener) {
if(listener != null) {
TypedListener l = new TypedListener(listener);
removeListener(SWT.Selection, l);
removeListener(SWT.DefaultSelection, l);
}
}
/**
* Removes the textListener for the appropriate SWT events to handle incrementing fields.
*/
protected void removeTextListener() {
Text control = text.getControl();
control.removeListener(SWT.KeyDown, textListener);
control.removeListener(SWT.MouseDown, textListener);
control.removeListener(SWT.MouseWheel, textListener);
control.removeListener(SWT.MouseUp, textListener);
control.removeListener(SWT.Verify, textListener);
text.removeListener(SWT.Traverse, textListener);
}
// void select(Date date) {
// if(date != null) {
// Date[] tmp = new Date[selection.length + 1];
// System.arraycopy(selection, 0, tmp, 1, selection.length);
// tmp[0] = date;
// setSelectedDates(tmp);
// }
// }
//
// void select(Date date1, Date date2, int field, int increment) {
// if(date1 != null && date2 != null) {
// Date start = date1.before(date2) ? date1 : date2;
// Date end = date1.before(date2) ? date2 : date1;
// List<Date> tmp = new ArrayList<Date>();
// Calendar cal = getCalendarInstance(start);
// while(cal.getTime().before(end)) {
// tmp.add(cal.getTime());
// cal.add(field, increment);
// }
// tmp.add(cal.getTime());
// if(start == date2) {
// Collections.reverse(tmp);
// }
// setSelectedDates(tmp.toArray(new Date[tmp.size()]));
// }
// }
/**
* Sets the active field, which may or may not be a real field (it may also
* be <code>FIELD_NONE</code>)
* @param field the field to be set active
* @see CDateTime#hasField(int)
*/
private void setActiveField(int field) {
if(activeField != field) {
commitEditField();
editField = null;
activeField = field;
}
}
/**
* <p><b>WARNING: Experimental API - this method may be removed in future versions</b></p>
* Sets the builder that this CDateTime widget will use to
* build its graphical selector to the given builder, or to a default
* builder if the given builder is null.
* @param builder the builder to use, or null to use a default builder
*/
public void setBuilder(CDateTimeBuilder builder) {
this.builder = builder;
if(picker != null) {
disposePicker();
createPicker();
}
}
/* (non-Javadoc)
* @see org.eclipse.nebula.cwt.base.BaseCombo#setButtonImage(org.eclipse.swt.graphics.Image)
*/
public void setButtonImage(Image image) {
super.setButtonImage(image);
}
@Override
protected boolean setContentFocus() {
if(checkPicker()) {
internalFocusShift = true;
boolean result = picker.setFocus();
internalFocusShift = false;
return result;
}
return false;
}
/* (non-Javadoc)
* @see org.eclipse.nebula.cwt.base.BaseCombo#setEditable(boolean)
*/
public void setEditable(boolean editable) {
super.setEditable(editable);
if(checkPicker()) {
if(picker instanceof DatePicker) {
((DatePicker) picker).setEditable(editable);
} else {
picker.setActivatable(editable);
}
}
}
private boolean checkPicker() {
return picker != null && !picker.isDisposed();
}
/**
* Set the date and time format of this CDateTime uses style constants which correspond
* to the various forms of DateFormat.getXxxInstance(int).
* <dt><b>Valid Styles:</b></dt>
* <dd>DATE_SHORT, DATE_MEDIUM, DATE_LONG, TIME_SHORT, TIME_MEDIUM</dd>
* <p>Styles are bitwise OR'ed together, but only one "DATE" and one "TIME" may be set at a time.</p>
* Examples:<br>
* </code>setFormat(CDT.DATE_LONG);</code><br />
* </code>setFormat(CDT.DATE_SHORT | CDT.TIME_MEDIUM);</code><br />
* @param format the bitwise OR'ed Date and Time format to be set
* @throws IllegalArgumentException
* @see #getPattern()
* @see #setPattern(String)
*/
public void setFormat(int format) throws IllegalArgumentException {
int dateStyle = (format & CDT.DATE_SHORT) != 0 ? DateFormat.SHORT :
(format & CDT.DATE_MEDIUM) != 0 ? DateFormat.MEDIUM :
(format & CDT.DATE_LONG) != 0 ? DateFormat.LONG : -1;
int timeStyle = (format & CDT.TIME_SHORT) != 0 ? DateFormat.SHORT :
(format & CDT.TIME_MEDIUM) != 0 ? DateFormat.MEDIUM : -1;
String str = null;
if(dateStyle != -1 && timeStyle != -1) {
str = ((SimpleDateFormat) DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale)).toPattern();
} else if(dateStyle != -1) {
str = ((SimpleDateFormat) DateFormat.getDateInstance(dateStyle, locale)).toPattern();
} else if(timeStyle != -1) {
str = ((SimpleDateFormat) DateFormat.getTimeInstance(timeStyle, locale)).toPattern();
} else if(pattern == null) { // first call, so set to default
format = CDT.DATE_SHORT;
str = ((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, locale)).toPattern();
}
if(str != null) {
this.format = format;
setPattern(str);
}
}
/**
* Sets the Locale to be used by this CDateTime and causes all affected
* attributes to be updated<br>
* If the provided locale is the same as the current locale then this method simply
* returns. If the provided Locale is null then this CDateTime will use
* the system's default locale.<br>
* If this <code>CDateTime</code> is of style <code>DROP_DOWN</code>
* then the associated <code>CDateTime</code> will be set to the same locale.
* @param locale the Locale, or null to use the system's default
* @see #getLocale()
*/
public void setLocale(Locale locale) {
if(locale == null) locale = Locale.getDefault();
if(!this.locale.equals(locale)) {
this.locale = locale;
if(format > 0) {
setFormat(format);
} else {
setPattern(pattern);
}
updateNullText();
}
}
protected void setModifyEventProperties(Event e) {
e.data = calendar.getTime();
}
/**
* Set the text to be shown when the selection is null.
* Passing null into this method will cause the CDateTime widget
* to use a default null text for the given locale.
* @param text
*/
public void setNullText(String text) {
defaultNullText = false;
nullText = text;
updateText();
}
public void setOpen(boolean open) {
setOpen(open, null);
}
public void setOpen(boolean open, Runnable callback) {
if(open) {
cancelDate = getSelection();
createPicker();
} else {
cancelDate = null;
}
super.setOpen(open, callback);
if(hasSelection()) {
show(getSelection());
}
}
/**
* <p><b>WARNING: Experimental API - this method may be removed in future versions</b></p>
* Sets the painter that this CDateTime widget will use to
* paint its graphical selector to the given painter, or to a default
* painter if the given painter is null.
* @param painter the painter to use, or null to use a default painter
*/
public void setPainter(CDateTimePainter painter) {
if(painter != null) {
painter.setCDateTime(this);
}
this.painter = painter;
}
/**
* Set the style of this CDateTime to work with dates and / or times
* as determined by the given pattern. This will set the fields shown in the
* text box and, if <code>DROP_DOWN</code> style is set, the fields of the
* drop down component.<br>
* This method is backed by an implementation of SimpleDateFormat, and as such,
* any string pattern which is valid for SimpleDateFormat may be used.
* Examples (US Locale):<br>
* </code>setPattern("MM/dd/yyyy h:mm a");</code><br />
* </code>setPattern("'Meeting @' h:mm a 'on' EEEE, MMM dd, yyyy");</code><br />
* @param pattern the pattern to use, if it is invalid, the original is restored
* @throws IllegalArgumentException
* @see SimpleDateFormat
* @see #getPattern()
* @see #setFormat(int)
*/
public void setPattern(String pattern) throws IllegalArgumentException {
if(isOpen()) {
setOpen(false);
}
df = new SimpleDateFormat(pattern, locale);
df.setTimeZone(timezone);
if(updateFields()) {
this.pattern = pattern;
this.format = -1;
boolean wasDate = isDate;
boolean wasTime = isTime;
isDate = isTime = false;
calendarFields = new int[field.length];
for(int i = 0; i < calendarFields.length; i++) {
calendarFields[i] = getCalendarField(field[i]);
switch(calendarFields[i]) {
case Calendar.AM_PM:
case Calendar.HOUR:
case Calendar.HOUR_OF_DAY:
case Calendar.MILLISECOND:
case Calendar.MINUTE:
case Calendar.SECOND:
isTime = true;
break;
case Calendar.DAY_OF_MONTH:
case Calendar.DAY_OF_WEEK:
case Calendar.DAY_OF_WEEK_IN_MONTH:
case Calendar.DAY_OF_YEAR:
case Calendar.ERA:
case Calendar.MONTH:
case Calendar.WEEK_OF_MONTH:
case Calendar.WEEK_OF_YEAR:
case Calendar.YEAR:
isDate = true;
break;
default:
break;
}
}
if(checkButton() && ((isDate != wasDate) || (isTime != wasTime))) {
if(defaultButtonImage) {
if(isDate) {
doSetButtonImage(Resources.getIconCalendar());
} else {
doSetButtonImage(Resources.getIconClock());
}
}
updateNullText();
}
if(checkText()) {
updateText();
}
if(isSimple()) {
disposePicker();
createPicker();
}
} else {
throw new IllegalArgumentException("Problem setting pattern: \"" + pattern + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
}
void setScrollable(boolean scrollable) {
this.scrollable = scrollable;
if(isSimple() && !scrollable) {
if(picker != null && picker instanceof DatePicker) {
updatePicker();
}
}
}
/**
* Set the selection for this CDateTime to that of the provided
* <code>Date</code> object.<br>
* @param selection the new selection, or null to clear the selection
*/
public void setSelection(Date selection) {
if(getEditable()) {
if(selection == null) {
this.selection = new Date[0];
} else {
this.selection = new Date[] { selection };
}
}
if(singleSelection && this.selection.length > 0) {
show(selection);
} else {
updateText();
updatePicker();
}
}
/**
* Sets the timezone to the timezone specified by the given zoneID,
* or to the system default if the given zoneID is null.
* If the give zoneID cannot be understood, then the timezone will be
* set to GMT.
* @param zoneID the id of the timezone to use, or null to use the system default
* @see #setTimeZone(TimeZone)
*/
public void setTimeZone(String zoneID) {
if(zoneID == null) {
setTimeZone((TimeZone) null);
} else {
setTimeZone(TimeZone.getTimeZone(zoneID));
}
}
/**
* Sets the timezone to the given timezone, or to the
* system's default timezone if the given timezone is null.
* @param zone the timezone to use, or null to use the system default
* @see #setTimeZone(String)
*/
public void setTimeZone(TimeZone zone) {
if(zone == null) {
timezone = TimeZone.getDefault();
}
if(!this.timezone.equals(zone)) {
this.timezone = zone;
calendar.setTimeZone(this.timezone);
df.setTimeZone(this.timezone);
updateText();
}
}
/**
* Shows the given date if it can be shown by the selector.
* In other words, for graphical selectors such as a calendar, the visible
* range of time is moved so that the given date is visible.
* @param date the date to show
*/
public void show(Date date) {
if(date == null) {
calendar.setTime(new Date());
} else {
calendar.setTime(date);
}
updateText();
updatePicker();
}
/**
* Show the selection if it can be shown by the selector.
* Has no affect if there is no selection.
* @see #show(Date)
*/
public void showSelection() {
if(selection.length > 0) {
show(selection[0]);
}
}
@Override
public String toString() {
return getClass().getSimpleName() + " {" + getCalendarTime() + "}"; //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* inspects all of the calendar fields in the <code>field</code> array to determine what
* style is appropriate and then sets that style to the picker using the setPickerStyle method.<br>
*/
private boolean updateFields() {
Field[] bak = new Field[(field == null) ? 0 : field.length];
if(bak.length > 0) System.arraycopy(field, 0, bak, 0, field.length);
AttributedCharacterIterator aci = df.formatToCharacterIterator(calendar.getTime());
field = new Field[aci.getAllAttributeKeys().size()];
separator = new String[field.length+1]; // there can be a separator before and after
int i = 0;
Object last = null;
for(char c = aci.first(); c != CharacterIterator.DONE; c = aci.next()) {
Object[] oa = aci.getAttributes().keySet().toArray();
if(oa.length > 0) {
if(oa[0] != last && i < field.length) {
if(getCalendarField((Field) oa[0]) < 0) {
if(bak.length > 0) {
field = new Field[bak.length];
System.arraycopy(bak, 0, field, 0, bak.length);
}
return false;
} else {
field[i] = (Field) oa[0];
last = oa[0];
}
i++;
}
} else {
if(separator[i] == null) separator[i] = String.valueOf(c);
}
}
df.setLenient(false);
setActiveField(FIELD_NONE);
return true;
}
private void updateNullText() {
if(defaultNullText) {
if(isDate) {
nullText = Resources.getString("null_text.date", locale); //$NON-NLS-1$
} else {
nullText = Resources.getString("null_text.time", locale); //$NON-NLS-1$
}
if(!hasSelection()) {
updateText();
}
}
}
/**
* tell the picker to update its view of the selection and reference time
*/
private void updatePicker() {
if(picker != null) {
if(picker instanceof DatePicker) {
((DatePicker) picker).updateView();
} else if(picker instanceof AnalogTimePicker) {
((AnalogTimePicker) picker).updateView();
} else if(picker instanceof DiscreteTimePicker) {
((DiscreteTimePicker) picker).updateView();
}
}
}
/**
* This is the only way that text is set to the text box.<br>
* The selection is also set here (corresponding to the active field) as
* well as if a field is being edited, it's "edit text" is inserted for
* display.
*/
private void updateText() {
updateText(false);
}
/**
* This is the only way that text is set to the text box.<br>
* The selection is also set here (corresponding to the active field) as
* well as if a field is being edited, it's "edit text" is inserted for
* display.
* @param async If true, this operation will be performed asynchronously (for changes to text selection)
*/
private void updateText(boolean async) {
// TODO: save previous state and only update on changes...?
String buffer = hasSelection() ? df.format(getSelection()) : getNullText();
int s0 = 0;
int s1 = 0;
if(!hasSelection()) {
s0 = 0;
s1 = buffer.length();
} else if(activeField >= 0 && activeField < field.length) {
AttributedCharacterIterator aci = df.formatToCharacterIterator(getSelection());
for(char c = aci.first(); c != CharacterIterator.DONE; c = aci.next()) {
if(aci.getAttribute(field[activeField]) != null) {
s0 = aci.getRunStart();
s1 = aci.getRunLimit();
if(editField != null) {
String str = editField.toString();
buffer = buffer.substring(0, s0) + str + buffer.substring(s1);
int oldS1 = s1;
s1 = s0 +str.length();
textSelectionOffset.x = Math.min(oldS1, s1);
textSelectionOffset.y = (oldS1 - s0) - str.length();
} else {
textSelectionOffset.x = buffer.length()+1;
textSelectionOffset.y = 0;
}
break;
}
}
} else {
setActiveField(FIELD_NONE);
}
final String string = buffer;
final int selStart = s0;
final int selEnd = s1;
Runnable runnable = new Runnable() {
public void run() {
if((text != null) && (!text.isDisposed())) {
if(!string.equals(text.getText())) {
text.getControl().removeListener(SWT.Verify, textListener);
text.setText(string);
text.getControl().addListener(SWT.Verify, textListener);
}
text.getControl().setSelection(selStart, selEnd);
}
}
};
if(async) {
getDisplay().asyncExec(runnable);
} else {
getDisplay().syncExec(runnable);
}
}
/**
* The Verify Event handler.<br>
* <b>EVERYTHING</b> is blocked via this handler (Event.doit is set to false).
* Depending upon the input, a course of action is determined and the displayed
* text is updated via the <code>updateText()</code> method.
* @param e the event
* @see CDateTime#updateText()
*/
void verify(Event e) {
e.doit = false;
if(field.length == 0 || activeField == FIELD_NONE) return;
char c = e.character;
if(((e.text.length() == 1) && String.valueOf(c).equals(e.text) && Character.isDigit(c)) ||
(e.text.length() > 1) ) {
if(e.text.length() == 1) {
if(editField == null) {
int cf = getCalendarField();
if(cf >= 0) {
int digits;
switch(cf) {
case Calendar.YEAR:
digits = 4;
break;
case Calendar.DAY_OF_YEAR:
digits = 3;
break;
case Calendar.AM_PM:
case Calendar.DAY_OF_WEEK:
case Calendar.ERA:
digits = 1;
break;
default:
digits = 2;
}
editField = new EditField(digits, calendar.get(cf));
} else {
return;
}
}
if(editField.addChar(c)) {
if(commitEditField()) {
fieldNext();
} else {
editField = null;
if(selection.length > 0) {
selection[0] = calendar.getTime();
}
updateText();
}
}
if(selection.length > 0) {
selection[0] = calendar.getTime();
}
updatePicker();
} else {
try {
setSelection(df.parse(e.text));
fireSelectionChanged();
} catch (ParseException pe) {
// do nothing
}
}
}
updateText();
}
}