/**************************************************************************** * 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(); } }