changeset 190:934fb859da8e

Added DateTime
author Frank Benoit <benoit@tionex.de>
date Mon, 03 Mar 2008 21:23:28 +0100
parents ac6ff7065cac
children 5db57b8ff1a9
files dwt/dwthelper/utils.d dwt/widgets/DateTime.d
diffstat 2 files changed, 1338 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/dwt/dwthelper/utils.d	Mon Mar 03 00:34:00 2008 +0100
+++ b/dwt/dwthelper/utils.d	Mon Mar 03 21:23:28 2008 +0100
@@ -247,6 +247,15 @@
     return res;
 }
 
+public int lastIndexOf(char[] str, char ch){
+    return lastIndexOf( str, ch, str.length );
+}
+public int lastIndexOf(char[] str, char ch, int formIndex){
+    int res = tango.text.Util.locatePrior( str, ch, formIndex );
+    if( res is str.length ) res = -1;
+    return res;
+}
+
 public char[] substring( char[] str, int start ){
     return str[ start .. $ ].dup;
 }
@@ -286,6 +295,10 @@
     return src == other;
 }
 
+public bool equalsIgnoreCase( char[] src, char[] other ){
+    return tango.text.Unicode.toFold(src) == tango.text.Unicode.toFold(other);
+}
+
 public char[] toLowerCase( char[] src ){
     return tango.text.Unicode.toLower( src );
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dwt/widgets/DateTime.d	Mon Mar 03 21:23:28 2008 +0100
@@ -0,0 +1,1325 @@
+/*******************************************************************************
+ * Copyright (c) 2000, 2007 IBM Corporation and others.
+ * 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:
+ *     IBM Corporation - initial API and implementation
+ * Port to the D programming language:
+ *     Frank Benoit <benoit@tionex.de>
+ *******************************************************************************/
+module dwt.widgets.DateTime;
+
+import dwt.DWT;
+import dwt.DWTException;
+import dwt.events.SelectionEvent;
+import dwt.events.SelectionListener;
+import dwt.graphics.Color;
+import dwt.graphics.Font;
+import dwt.graphics.GC;
+import dwt.graphics.Point;
+import dwt.graphics.Rectangle;
+import dwt.internal.gtk.OS;
+import dwt.internal.Compatibility;
+
+import dwt.widgets.Composite;
+import dwt.widgets.Listener;
+import dwt.widgets.Button;
+import dwt.widgets.Event;
+import dwt.widgets.Text;
+import dwt.widgets.TypedListener;
+
+import dwt.dwthelper.Runnable;
+import tango.text.convert.Format;
+import tango.util.Convert;
+import dwt.dwthelper.Runnable;
+import dwt.dwthelper.utils;
+
+static import tango.text.Util;
+static import tango.text.locale.Core;
+static import tango.text.Text;
+static import tango.time.WallClock;
+static import tango.time.chrono.Gregorian;
+static import tango.time.chrono.Calendar;
+
+alias tango.text.Text.Text!(char) TangoText;
+
+private class Calendar{
+    enum {
+        AM,
+        PM
+    }
+    enum {
+        AM_PM,
+        HOUR,
+        MINUTE,
+        SECOND,
+        MONTH,
+        YEAR,
+        DAY_OF_MONTH,
+        DAY_SELECTED,
+        MONTH_CHANGED,
+        HOUR_OF_DAY,
+    }
+    private static const int[] MONTH_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
+    static private Calendar instance;
+
+    private int second;
+    private int minute;
+    private int hour;
+    private int dayofmonth;
+    private int month;
+    private int year;
+
+    static Calendar getInstance(){
+        if( instance is null ){
+            synchronized {
+                if( instance is null ){
+                    instance = new Calendar;
+                }
+            }
+        }
+        return instance;
+    }
+
+    int getActualMaximum(int field){
+        switch( field ){
+        case YEAR:
+            return 2100;
+        case MONTH:
+            return MONTH_DAYS.length -1;
+        case DAY_OF_MONTH:
+            return MONTH_DAYS[month];
+        case HOUR:
+            return 11;
+        case HOUR_OF_DAY:
+            return 23;
+        case MINUTE:
+            return 59;
+        case SECOND:
+            return 59;
+        case AM_PM:
+            return PM;
+        default: assert( false, Format( "no matching switch case for field {}.", field ));
+        }
+    }
+
+    int getActualMinimum(int field){
+        switch( field ){
+        case YEAR:
+            return 1900;
+        case MONTH:
+            return 0;
+        case DAY_OF_MONTH:
+            return 1;
+        case HOUR:
+        case HOUR_OF_DAY:
+            return 0;
+        case MINUTE:
+            return 0;
+        case SECOND:
+            return 0;
+        case AM_PM:
+            return AM;
+        default: assert( false, Format( "no matching switch case for field {}.", field ));
+        }
+    }
+
+    int getMaximum(int field){
+        switch( field ){
+        case YEAR:
+            return 2100;
+        case MONTH:
+            return 11;
+        case DAY_OF_MONTH:
+            return 31;
+        case HOUR:
+            return 11;
+        case HOUR_OF_DAY:
+            return 23;
+        case MINUTE:
+            return 59;
+        case SECOND:
+            return 59;
+        case AM_PM:
+            return PM;
+        default: assert( false, Format( "no matching switch case for field {}.", field ));
+        }
+    }
+
+    int getMinimum(int field){
+        switch( field ){
+        case YEAR:
+            return 1900;
+        case MONTH:
+            return 0;
+        case DAY_OF_MONTH:
+            return 1;
+        case HOUR:
+        case HOUR_OF_DAY:
+            return 0;
+        case MINUTE:
+            return 0;
+        case SECOND:
+            return 0;
+        case AM_PM:
+            return AM;
+        default: assert( false, Format( "no matching switch case for field {}.", field ));
+        }
+    }
+    int get(int field){
+        switch( field ){
+        case YEAR:
+            return year;
+        case MONTH:
+            return month;
+        case DAY_OF_MONTH:
+            return dayofmonth;
+        case HOUR:
+            return hour;
+        case HOUR_OF_DAY:
+            return hour % 12;
+        case MINUTE:
+            return minute;
+        case SECOND:
+            return second;
+        case AM_PM:
+            return ( hour < 12 ) ? AM : PM;
+        default: assert( false, Format( "no matching switch case for field {}.", field ));
+        }
+    }
+
+    void set(int field, int value){
+        switch( field ){
+        case YEAR:
+            year = value;
+            break;
+        case MONTH:
+            assert( value >= 0 && value < 12 );
+            month = value;
+            break;
+        case DAY_OF_MONTH:
+            assert( value > 0 && value <= getActualMaximum( DAY_OF_MONTH ) );
+            dayofmonth = value;
+            break;
+        case HOUR:
+            assert( value >= 0 && value < 12 );
+            hour = value;
+            break;
+        case HOUR_OF_DAY:
+            assert( value >= 0 && value < 24 );
+            hour = value;
+            break;
+        case MINUTE:
+            assert( value >= 0 && value < 60 );
+            minute = value;
+            break;
+        case SECOND:
+            assert( value >= 0 && value < 60 );
+            second = value;
+            break;
+        case AM_PM:
+            if( get(field) is AM ){
+                if( value is AM ){
+                    return;
+                }
+                else{
+                    hour += 12;
+                }
+            }
+            else{ // PM
+                if( value is AM ){
+                    hour -= 12;
+                }
+                else{
+                    return;
+                }
+            }
+            break;
+        default: assert( false, Format( "no matching switch case for field {}.", field ));
+        }
+    }
+
+    void roll(int field, int value){
+        switch( field ){
+        case YEAR:
+            year = value;
+            break;
+        case MONTH:
+            month += value;
+            month %= 12;
+            break;
+        case DAY_OF_MONTH:
+            dayofmonth += value;
+            dayofmonth %= getActualMaximum( DAY_OF_MONTH );
+            break;
+        case HOUR:
+        case HOUR_OF_DAY:
+            hour += value;
+            hour %= 24;
+            break;
+        case MINUTE:
+            minute += value;
+            minute %= 60;
+            break;
+        case SECOND:
+            second += value;
+            second %= 60;
+            break;
+        case AM_PM:
+            set( AM_PM, get( AM_PM ) is AM ? PM : AM );
+            break;
+        default: assert( false, Format( "no matching switch case for field {}.", field ));
+        }
+    }
+}
+
+
+private class DateFormatSymbols {
+    private const char[][] ampm = [ "AM"[], "PM" ];
+    char[][] getAmPmStrings(){
+        return ampm;
+    }
+}
+
+/+
+private int getCalendarActualMaximum( tango.time.Time.Time time, int field ){
+    int year = cal.getYear( time );
+    int month = cal.getMonth( time );
+    tango.time.Time.TimeOfDay tod = tango.time.Time.TimeOfDay( time.ticks );
+    switch( field ){
+    case YEAR: return tango.time.chrono.Gregorian.Gregorian.MAX_YEAR;
+    case MONTH: return cal.getMonthsInYear( year, tango.time.chrono.Gregorian.Gregorian.AD_ERA ) -1; // Jan is 0, so max is 11
+    case DAY_OF_MONTH:return cal.getDaysInMonth( year, month, tango.time.chrono.Gregorian.Gregorian.AD_ERA ) -1; // 1st is 0
+    case HOUR:return 23;
+    case MINUTE:return 59;
+    case SECOND:return 59;
+    case AM_PM:return PM;
+    default:
+    }
+}
+
+private int getCalendarActualMinimum( tango.time.Time.Time time, int field ){
+    switch( field ){
+    case YEAR: return 1800;
+    case MONTH: return 1;
+    case DAY_OF_MONTH:return 1;
+    case HOUR:return 0;
+    case MINUTE:return 0;
+    case SECOND:return 0;
+    case AM_PM:return AM;
+    default:
+    }
+}
+private int getCalendarField( tango.time.Time.Time time, int field ){
+    switch( field ){
+    case YEAR: return cal.getYear( time );
+    case MONTH: return cal.getMonth( time );
+    case DAY_OF_MONTH:return cal.getDayOfMonth( time );
+    default:
+    }
+    tango.time.Time.TimeOfDay tod = tango.time.Time.TimeOfDay( time.ticks );
+    switch( field ){
+    case HOUR:return tod.hours;
+    case MINUTE:return tod.minutes;
+    case SECOND:return tod.seconds;
+    case AM_PM:return tod.hours > 12;
+    default:
+    }
+    assert(false);
+}
+enum {
+    AM,
+    PM,
+    AM_PM,
+    HOUR,
+    MINUTE,
+    SECOND,
+    MONTH,
+    YEAR,
+    DAY_OF_MONTH,
+    DAY_SELECTED,
+    MONTH_CHANGED,
+    HOUR_OF_DAY
+}
+
++/
+
+/**
+ * Instances of this class are selectable user interface
+ * objects that allow the user to enter and modify date
+ * or time values.
+ * <p>
+ * Note that although this class is a subclass of <code>Composite</code>,
+ * it does not make sense to add children to it, or set a layout on it.
+ * </p>
+ * <dl>
+ * <dt><b>Styles:</b></dt>
+ * <dd>DATE, TIME, CALENDAR, SHORT, MEDIUM, LONG</dd>
+ * <dt><b>Events:</b></dt>
+ * <dd>Selection</dd>
+ * </dl>
+ * <p>
+ * Note: Only one of the styles DATE, TIME, or CALENDAR may be specified,
+ * and only one of the styles SHORT, MEDIUM, or LONG may be specified.
+ * </p><p>
+ * IMPORTANT: This class is <em>not</em> intended to be subclassed.
+ * </p>
+ *
+ * @since 3.3
+ */
+public class DateTime : Composite {
+    int day, month, year, hours, minutes, seconds;
+
+    static const int MIN_YEAR = 1752; // Gregorian switchover in North America: September 19, 1752
+    static const int MAX_YEAR = 9999;
+
+    /* Emulated DATE and TIME variables */
+    Calendar calendar;
+    DateFormatSymbols formatSymbols;
+    Button down, up;
+    Text text;
+    char[] format;
+    Point[] fieldIndices;
+    int[] fieldNames;
+    int fieldCount, currentField = 0, characterCount = 0;
+    bool ignoreVerify = false;
+    static const char[] DEFAULT_SHORT_DATE_FORMAT = "MM/YYYY";
+    static const char[] DEFAULT_MEDIUM_DATE_FORMAT = "MM/DD/YYYY";
+    static const char[] DEFAULT_LONG_DATE_FORMAT = "MM/DD/YYYY";
+    static const char[] DEFAULT_SHORT_TIME_FORMAT = "HH:MM AM";
+    static const char[] DEFAULT_MEDIUM_TIME_FORMAT = "HH:MM:SS AM";
+    static const char[] DEFAULT_LONG_TIME_FORMAT = "HH:MM:SS AM";
+
+
+
+/**
+ * Constructs a new instance of this class given its parent
+ * and a style value describing its behavior and appearance.
+ * <p>
+ * The style value is either one of the style constants defined in
+ * class <code>DWT</code> which is applicable to instances of this
+ * class, or must be built by <em>bitwise OR</em>'ing together
+ * (that is, using the <code>int</code> "|" operator) two or more
+ * of those <code>DWT</code> style constants. The class description
+ * lists the style constants that are applicable to the class.
+ * Style bits are also inherited from superclasses.
+ * </p>
+ *
+ * @param parent a composite control which will be the parent of the new instance (cannot be null)
+ * @param style the style of control to construct
+ *
+ * @exception IllegalArgumentException <ul>
+ *    <li>ERROR_NULL_ARGUMENT - if the parent is null</li>
+ * </ul>
+ * @exception DWTException <ul>
+ *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
+ *    <li>ERROR_INVALID_SUBCLASS - if this class is not an allowed subclass</li>
+ * </ul>
+ *
+ * @see DWT#DATE
+ * @see DWT#TIME
+ * @see DWT#CALENDAR
+ * @see Widget#checkSubclass
+ * @see Widget#getStyle
+ */
+public this (Composite parent, int style) {
+    super (parent, checkStyle (style));
+    if ((this.style & DWT.CALENDAR) is 0) {
+        /* DWT.DATE and DWT.TIME */
+        calendar = Calendar.getInstance();
+        formatSymbols = new DateFormatSymbols();
+
+        text = new Text(this, DWT.SINGLE);
+        if ((this.style & DWT.DATE) !is 0) {
+            setFormat((this.style & DWT.SHORT) !is 0 ? DEFAULT_SHORT_DATE_FORMAT : (this.style & DWT.LONG) !is 0 ? DEFAULT_LONG_DATE_FORMAT : DEFAULT_MEDIUM_DATE_FORMAT);
+        } else { // DWT.TIME
+            setFormat((this.style & DWT.SHORT) !is 0 ? DEFAULT_SHORT_TIME_FORMAT : (this.style & DWT.LONG) !is 0 ? DEFAULT_LONG_TIME_FORMAT : DEFAULT_MEDIUM_TIME_FORMAT);
+        }
+        text.setText(getFormattedString(this.style));
+        Listener listener = new class () Listener {
+            public void handleEvent(Event event) {
+                switch(event.type) {
+                    case DWT.KeyDown: onKeyDown(event); break;
+                    case DWT.FocusIn: onFocusIn(event); break;
+                    case DWT.FocusOut: onFocusOut(event); break;
+                    case DWT.MouseDown: onMouseClick(event); break;
+                    case DWT.MouseUp: onMouseClick(event); break;
+                    case DWT.Verify: onVerify(event); break;
+                    default:
+                }
+            }
+        };
+        text.addListener(DWT.KeyDown, listener);
+        text.addListener(DWT.FocusIn, listener);
+        text.addListener(DWT.FocusOut, listener);
+        text.addListener(DWT.MouseDown, listener);
+        text.addListener(DWT.MouseUp, listener);
+        text.addListener(DWT.Verify, listener);
+        up = new Button(this, DWT.ARROW | DWT.UP);
+        //up.setToolTipText(DWT.getMessage ("SWT_Up")); //$NON-NLS-1$
+        down = new Button(this, DWT.ARROW | DWT.DOWN);
+        //down.setToolTipText(DWT.getMessage ("SWT_Down")); //$NON-NLS-1$
+        up.addListener(DWT.Selection, new class() Listener {
+            public void handleEvent(Event event) {
+                incrementField(+1);
+                text.setFocus();
+            }
+        });
+        down.addListener(DWT.Selection, new class() Listener {
+            public void handleEvent(Event event) {
+                incrementField(-1);
+                text.setFocus();
+            }
+        });
+        addListener(DWT.Resize, new class() Listener {
+            public void handleEvent(Event event) {
+                onResize(event);
+            }
+        });
+    }
+}
+
+static int checkStyle (int style) {
+    /*
+    * Even though it is legal to create this widget
+    * with scroll bars, they serve no useful purpose
+    * because they do not automatically scroll the
+    * widget's client area.  The fix is to clear
+    * the DWT style.
+    */
+    style &= ~(DWT.H_SCROLL | DWT.V_SCROLL);
+    style = checkBits (style, DWT.DATE, DWT.TIME, DWT.CALENDAR, 0, 0, 0);
+    return checkBits (style, DWT.MEDIUM, DWT.SHORT, DWT.LONG, 0, 0, 0);
+}
+
+/**
+ * Adds the listener to the collection of listeners who will
+ * be notified when the control is selected by the user, by sending
+ * it one of the messages defined in the <code>SelectionListener</code>
+ * interface.
+ * <p>
+ * <code>widgetSelected</code> is called when the user changes the control's value.
+ * <code>widgetDefaultSelected</code> is not called.
+ * </p>
+ *
+ * @param listener the listener which should be notified
+ *
+ * @exception IllegalArgumentException <ul>
+ *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
+ * </ul>
+ * @exception DWTException <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) {
+    checkWidget ();
+    if (listener is null) error (DWT.ERROR_NULL_ARGUMENT);
+    TypedListener typedListener = new TypedListener (listener);
+    addListener (DWT.Selection, typedListener);
+    addListener (DWT.DefaultSelection, typedListener);
+}
+
+protected void checkSubclass () {
+    if (!isValidSubclass ()) error (DWT.ERROR_INVALID_SUBCLASS);
+}
+
+public Point computeSize (int wHint, int hHint, bool changed) {
+    checkWidget ();
+    int width = 0, height = 0;
+    if (wHint is DWT.DEFAULT || hHint is DWT.DEFAULT) {
+        if ((style & DWT.CALENDAR) !is 0) {
+            // TODO: CALENDAR computeSize
+            width = 300;
+            height = 200;
+        } else {
+            /* DWT.DATE and DWT.TIME */
+            GC gc = new GC(text);
+            Point textSize = gc.stringExtent(getComputeSizeString(style));
+            gc.dispose();
+            Rectangle trim = text.computeTrim(0, 0, textSize.x, textSize.y);
+            Point buttonSize = up.computeSize(DWT.DEFAULT, DWT.DEFAULT, changed);
+            width = trim.width + buttonSize.x;
+            height = Math.max(trim.height, buttonSize.y);
+        }
+    }
+    if (width is 0) width = DEFAULT_WIDTH;
+    if (height is 0) height = DEFAULT_HEIGHT;
+    if (wHint !is DWT.DEFAULT) width = wHint;
+    if (hHint !is DWT.DEFAULT) height = hHint;
+    int border = getBorderWidth ();
+    width += border * 2; height += border * 2;
+    return new Point (width, height);
+}
+
+void createHandle (int index) {
+    if ((style & DWT.CALENDAR) !is 0) {
+        state |= HANDLE;
+        fixedHandle = cast(GtkWidget*)OS.g_object_new (display.gtk_fixed_get_type (), null);
+        if (fixedHandle is null) error (DWT.ERROR_NO_HANDLES);
+        OS.gtk_fixed_set_has_window (fixedHandle, true);
+        handle = cast(GtkWidget*)OS.gtk_calendar_new ();
+        if (handle is null) error (DWT.ERROR_NO_HANDLES);
+        OS.gtk_container_add (fixedHandle, handle);
+        if (OS.GTK_VERSION >= OS.buildVERSION(2, 4, 0)) {
+            OS.gtk_calendar_set_display_options(handle, OS.GTK_CALENDAR_SHOW_HEADING | OS.GTK_CALENDAR_SHOW_DAY_NAMES);
+        } else {
+            OS.gtk_calendar_display_options(handle, OS.GTK_CALENDAR_SHOW_HEADING | OS.GTK_CALENDAR_SHOW_DAY_NAMES);
+        }
+    } else {
+        super.createHandle(index);
+    }
+}
+
+void createWidget (int index) {
+    super.createWidget (index);
+    if ((style & DWT.CALENDAR) !is 0) {
+        getDate();
+    }
+}
+
+void commitCurrentField() {
+    if (characterCount > 0) {
+        characterCount = 0;
+        int fieldName = fieldNames[currentField];
+        int start = fieldIndices[currentField].x;
+        int end = fieldIndices[currentField].y;
+        char[] value = text.getText(start, end - 1);
+        int s = value.lastIndexOf(' ');
+        if (s !is -1) value = value.substring(s + 1);
+        int newValue = unformattedIntValue(fieldName, value, characterCount is 0, calendar.getActualMaximum(fieldName));
+        if (newValue !is -1) setTextField(fieldName, newValue, true, true);
+    }
+}
+
+char[] formattedStringValue(int fieldName, int value, bool adjust) {
+    if (fieldName is Calendar.AM_PM) {
+        char[][] ampm = formatSymbols.getAmPmStrings();
+        return ampm[value];
+    }
+    if (adjust) {
+        if (fieldName is Calendar.HOUR && value is 0) {
+            return to!(char[])(12);
+        }
+        if (fieldName is Calendar.MONTH) {
+            return to!(char[])(value + 1);
+        }
+    }
+    return to!(char[])(value);
+}
+
+char[] getComputeSizeString(int style) {
+    if ((style & DWT.DATE) !is 0) {
+        return (style & DWT.SHORT) !is 0 ? DEFAULT_SHORT_DATE_FORMAT : (style & DWT.LONG) !is 0 ? DEFAULT_LONG_DATE_FORMAT : DEFAULT_MEDIUM_DATE_FORMAT;
+    }
+    // DWT.TIME
+    return (style & DWT.SHORT) !is 0 ? DEFAULT_SHORT_TIME_FORMAT : (style & DWT.LONG) !is 0 ? DEFAULT_LONG_TIME_FORMAT : DEFAULT_MEDIUM_TIME_FORMAT;
+}
+
+int getFieldIndex(int fieldName) {
+    for (int i = 0; i < fieldCount; i++) {
+        if (fieldNames[i] is fieldName) {
+            return i;
+        }
+    }
+    return -1;
+}
+
+char[] getFormattedString(int style) {
+    if ((style & DWT.TIME) !is 0) {
+        char[][] ampm = formatSymbols.getAmPmStrings();
+        int h = calendar.get(Calendar.HOUR); if (h is 0) h = 12;
+        int m = calendar.get(Calendar.MINUTE);
+        int s = calendar.get(Calendar.SECOND);
+        int a = calendar.get(Calendar.AM_PM);
+        if ((style & DWT.SHORT) !is 0) return "" ~ (h < 10 ? " " : "") ~ to!(char[])(h) ~ ":" ~ (m < 10 ? " " : "") ~ to!(char[])(m) ~ " " ~ ampm[a];
+        return "" ~ (h < 10 ? " " : "") ~ to!(char[])(h) ~ ":" ~ (m < 10 ? " " : "") ~ to!(char[])(m) ~ ":" ~ (s < 10 ? " " : "") ~ to!(char[])(s) ~ " " ~ ampm[a];
+    }
+    /* DWT.DATE */
+    int y = calendar.get(Calendar.YEAR);
+    int m = calendar.get(Calendar.MONTH) + 1;
+    int d = calendar.get(Calendar.DAY_OF_MONTH);
+    if ((style & DWT.SHORT) !is 0) return "" ~ (m < 10 ? " " : "") ~ to!(char[])(m) ~ "/" ~ to!(char[])(y);
+    return "" ~ (m < 10 ? " " : "") ~ to!(char[])(m) ~ "/" ~ (d < 10 ? " " : "") ~ to!(char[])(d) ~ "/" ~ to!(char[])(y);
+}
+
+void getDate() {
+    uint y;
+    uint m;
+    uint d;
+    OS.gtk_calendar_get_date(handle, &y, &m, &d);
+    year = y;
+    month = m;
+    day = d;
+}
+
+/**
+ * Returns the receiver's date, or day of the month.
+ * <p>
+ * The first day of the month is 1, and the last day depends on the month and year.
+ * </p>
+ *
+ * @return a positive integer beginning with 1
+ *
+ * @exception DWTException <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>
+ */
+public int getDay () {
+    checkWidget ();
+    if ((style & DWT.CALENDAR) !is 0) {
+        getDate();
+        return day;
+    } else {
+        return calendar.get(Calendar.DAY_OF_MONTH);
+    }
+}
+
+/**
+ * Returns the receiver's hours.
+ * <p>
+ * Hours is an integer between 0 and 23.
+ * </p>
+ *
+ * @return an integer between 0 and 23
+ *
+ * @exception DWTException <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>
+ */
+public int getHours () {
+    checkWidget ();
+    if ((style & DWT.CALENDAR) !is 0) {
+        return hours;
+    } else {
+        return calendar.get(Calendar.HOUR_OF_DAY);
+    }
+}
+
+/**
+ * Returns the receiver's minutes.
+ * <p>
+ * Minutes is an integer between 0 and 59.
+ * </p>
+ *
+ * @return an integer between 0 and 59
+ *
+ * @exception DWTException <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>
+ */
+public int getMinutes () {
+    checkWidget ();
+    if ((style & DWT.CALENDAR) !is 0) {
+        return minutes;
+    } else {
+        return calendar.get(Calendar.MINUTE);
+    }
+}
+
+/**
+ * Returns the receiver's month.
+ * <p>
+ * The first month of the year is 0, and the last month is 11.
+ * </p>
+ *
+ * @return an integer between 0 and 11
+ *
+ * @exception DWTException <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>
+ */
+public int getMonth () {
+    checkWidget ();
+    if ((style & DWT.CALENDAR) !is 0) {
+        getDate();
+        return month;
+    } else {
+        return calendar.get(Calendar.MONTH);
+    }
+}
+
+/**
+ * Returns the receiver's seconds.
+ * <p>
+ * Seconds is an integer between 0 and 59.
+ * </p>
+ *
+ * @return an integer between 0 and 59
+ *
+ * @exception DWTException <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>
+ */
+public int getSeconds () {
+    checkWidget ();
+    if ((style & DWT.CALENDAR) !is 0) {
+        return seconds;
+    } else {
+        return calendar.get(Calendar.SECOND);
+    }
+}
+
+/**
+ * Returns the receiver's year.
+ * <p>
+ * The first year is 1752 and the last year is 9999.
+ * </p>
+ *
+ * @return an integer between 1752 and 9999
+ *
+ * @exception DWTException <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>
+ */
+public int getYear () {
+    checkWidget ();
+    if ((style & DWT.CALENDAR) !is 0) {
+        getDate();
+        return year;
+    } else {
+        return calendar.get(Calendar.YEAR);
+    }
+}
+
+override int gtk_day_selected (GtkWidget* widget) {
+    sendSelectionEvent ();
+    return 0;
+}
+
+override int gtk_month_changed (GtkWidget* widget) {
+    sendSelectionEvent ();
+    return 0;
+}
+
+void hookEvents () {
+    super.hookEvents();
+    if ((style & DWT.CALENDAR) !is 0) {
+        OS.g_signal_connect_closure (handle, OS.day_selected.ptr, display.closures [DAY_SELECTED], false);
+        OS.g_signal_connect_closure (handle, OS.month_changed.ptr, display.closures [MONTH_CHANGED], false);
+    }
+}
+
+bool isValid(int fieldName, int value) {
+    Calendar validCalendar;
+    if ((style & DWT.CALENDAR) !is 0) {
+        validCalendar = Calendar.getInstance();
+        validCalendar.set(Calendar.YEAR, year);
+        validCalendar.set(Calendar.MONTH, month);
+    } else {
+        validCalendar = calendar;
+    }
+    int min = validCalendar.getActualMinimum(fieldName);
+    int max = validCalendar.getActualMaximum(fieldName);
+    return value >= min && value <= max;
+}
+
+void incrementField(int amount) {
+    int fieldName = fieldNames[currentField];
+    int value = calendar.get(fieldName);
+    if (fieldName is Calendar.HOUR) {
+        int max = calendar.getMaximum(Calendar.HOUR);
+        int min = calendar.getMinimum(Calendar.HOUR);
+        if ((value is max && amount is 1) || (value is min && amount is -1)) {
+            int temp = currentField;
+            currentField = getFieldIndex(Calendar.AM_PM);
+            setTextField(Calendar.AM_PM, (calendar.get(Calendar.AM_PM) + 1) % 2, true, true);
+            currentField = temp;
+        }
+    }
+    setTextField(fieldName, value + amount, true, true);
+}
+
+void onKeyDown(Event event) {
+    int fieldName;
+    switch (event.keyCode) {
+        case DWT.ARROW_RIGHT:
+        case DWT.KEYPAD_DIVIDE:
+            // a right arrow or a valid separator navigates to the field on the right, with wraping
+            selectField((currentField + 1) % fieldCount);
+            break;
+        case DWT.ARROW_LEFT:
+            // navigate to the field on the left, with wrapping
+            int index = currentField - 1;
+            selectField(index < 0 ? fieldCount - 1 : index);
+            break;
+        case DWT.ARROW_UP:
+        case DWT.KEYPAD_ADD:
+            // set the value of the current field to value + 1, with wrapping
+            commitCurrentField();
+            incrementField(+1);
+            break;
+        case DWT.ARROW_DOWN:
+        case DWT.KEYPAD_SUBTRACT:
+            // set the value of the current field to value - 1, with wrapping
+            commitCurrentField();
+            incrementField(-1);
+            break;
+        case DWT.HOME:
+            // set the value of the current field to its minimum
+            fieldName = fieldNames[currentField];
+            setTextField(fieldName, calendar.getActualMinimum(fieldName), true, true);
+            break;
+        case DWT.END:
+            // set the value of the current field to its maximum
+            fieldName = fieldNames[currentField];
+            setTextField(fieldName, calendar.getActualMaximum(fieldName), true, true);
+            break;
+        default:
+            switch (event.character) {
+                case '/':
+                case ':':
+                case '-':
+                case '.':
+                    // a valid separator navigates to the field on the right, with wraping
+                    selectField((currentField + 1) % fieldCount);
+                    break;
+                default:
+            }
+    }
+}
+
+void onFocusIn(Event event) {
+    selectField(currentField);
+}
+
+void onFocusOut(Event event) {
+    commitCurrentField();
+}
+
+void onMouseClick(Event event) {
+    if (event.button !is 1) return;
+    Point sel = text.getSelection();
+    for (int i = 0; i < fieldCount; i++) {
+        if (sel.x >= fieldIndices[i].x && sel.x <= fieldIndices[i].y) {
+            currentField = i;
+            break;
+        }
+    }
+    selectField(currentField);
+}
+
+void onResize(Event event) {
+    Rectangle rect = getClientArea ();
+    int width = rect.width;
+    int height = rect.height;
+    Point buttonSize = up.computeSize(DWT.DEFAULT, height);
+    int buttonHeight = buttonSize.y / 2;
+    text.setBounds(0, 0, width - buttonSize.x, height);
+    up.setBounds(width - buttonSize.x, 0, buttonSize.x, buttonHeight);
+    down.setBounds(width - buttonSize.x, buttonHeight, buttonSize.x, buttonHeight);
+}
+
+void onVerify(Event event) {
+    if (ignoreVerify) return;
+    event.doit = false;
+    int fieldName = fieldNames[currentField];
+    int start = fieldIndices[currentField].x;
+    int end = fieldIndices[currentField].y;
+    int length_ = end - start;
+    char[] newText = event.text;
+    if (fieldName is Calendar.AM_PM) {
+        char[][] ampm = formatSymbols.getAmPmStrings();
+        if (newText.equalsIgnoreCase(ampm[Calendar.AM].substring(0, 1)) || newText.equalsIgnoreCase(ampm[Calendar.AM])) {
+            setTextField(fieldName, Calendar.AM, true, false);
+        } else if (newText.equalsIgnoreCase(ampm[Calendar.PM].substring(0, 1)) || newText.equalsIgnoreCase(ampm[Calendar.PM])) {
+            setTextField(fieldName, Calendar.PM, true, false);
+        }
+        return;
+    }
+    if (characterCount > 0) {
+        try {
+            to!(int)(newText);
+        } catch (ConversionException ex) {
+            return;
+        }
+        char[] value = text.getText(start, end - 1);
+        int s = value.lastIndexOf(' ');
+        if (s !is -1) value = value.substring(s + 1);
+        newText = value ~ newText;
+    }
+    int newTextLength = newText.length;
+    bool first = characterCount is 0;
+    characterCount = (newTextLength < length_) ? newTextLength : 0;
+    int max = calendar.getActualMaximum(fieldName);
+    int min = calendar.getActualMinimum(fieldName);
+    int newValue = unformattedIntValue(fieldName, newText, characterCount is 0, max);
+    if (newValue is -1) {
+        characterCount = 0;
+        return;
+    }
+    if (first && newValue is 0 && length_ > 1) {
+        setTextField(fieldName, newValue, false, false);
+    } else if (min <= newValue && newValue <= max) {
+        setTextField(fieldName, newValue, characterCount is 0, characterCount is 0);
+    } else {
+        if (newTextLength >= length_) {
+            newText = newText.substring(newTextLength - length_ + 1);
+            newValue = unformattedIntValue(fieldName, newText, characterCount is 0, max);
+            if (newValue !is -1) {
+                characterCount = length_ - 1;
+                if (min <= newValue && newValue <= max) {
+                    setTextField(fieldName, newValue, characterCount is 0, true);
+                }
+            }
+        }
+    }
+}
+
+void releaseWidget () {
+    super.releaseWidget();
+    //TODO: need to do anything here?
+}
+
+/**
+ * Removes the listener from the collection of listeners who will
+ * be notified when the control is selected by the user.
+ *
+ * @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 DWTException <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) {
+    checkWidget ();
+    if (listener is null) error (DWT.ERROR_NULL_ARGUMENT);
+    if (eventTable is null) return;
+    eventTable.unhook (DWT.Selection, listener);
+    eventTable.unhook (DWT.DefaultSelection, listener);
+}
+
+void selectField(int index) {
+    if (index !is currentField) {
+        commitCurrentField();
+    }
+    final int start = fieldIndices[index].x;
+    final int end = fieldIndices[index].y;
+    Point pt = text.getSelection();
+    if (index is currentField && start is pt.x && end is pt.y) return;
+    currentField = index;
+    display.asyncExec(new class( start, end ) Runnable {
+        int start, end;
+        this( int start, int end ){
+            this.start = start; this.end = end;
+        }
+        public void run() {
+            if (!text.isDisposed()) {
+                char[] value = text.getText(start, end - 1);
+                int s = value.lastIndexOf(' ');
+                if (s is -1 ) s = start;
+                else s = start + s + 1;
+                text.setSelection(s, end);
+            }
+        }
+    });
+}
+
+void sendSelectionEvent () {
+    uint y;
+    uint m;
+    uint d;
+    OS.gtk_calendar_get_date(handle, &y, &m, &d);
+    //TODO: hours, minutes, seconds?
+    if (d !is day ||
+        m !is month ||
+        y !is year) {
+        year = y;
+        month = m;
+        day = d;
+        postEvent (DWT.Selection);
+    }
+}
+
+public void setBackground(Color color) {
+    checkWidget();
+    super.setBackground(color);
+    if (text !is null) text.setBackground(color);
+}
+
+public void setFont(Font font) {
+    checkWidget();
+    super.setFont(font);
+    if (text !is null) text.setFont(font);
+    redraw();
+}
+
+public void setForeground(Color color) {
+    checkWidget();
+    super.setForeground(color);
+    if (text !is null) text.setForeground(color);
+}
+
+/*public*/ void setFormat(char[] string) {
+    checkWidget();
+    // TODO: this needs to be locale sensitive
+    fieldCount = (style & DWT.DATE) !is 0 ? ((style & DWT.SHORT) !is 0 ? 2 : 3) : ((style & DWT.SHORT) !is 0 ? 3 : 4);
+    fieldIndices = new Point[fieldCount];
+    fieldNames = new int[fieldCount];
+    if ((style & DWT.DATE) !is 0) {
+        fieldNames[0] = Calendar.MONTH;
+        fieldIndices[0] = new Point(0, 2);
+        if ((style & DWT.SHORT) !is 0) {
+            fieldNames[1] = Calendar.YEAR;
+            fieldIndices[1] = new Point(3, 7);
+        } else {
+            fieldNames[1] = Calendar.DAY_OF_MONTH;
+            fieldIndices[1] = new Point(3, 5);
+            fieldNames[2] = Calendar.YEAR;
+            fieldIndices[2] = new Point(6, 10);
+        }
+    } else { /* DWT.TIME */
+        fieldNames[0] = Calendar.HOUR;
+        fieldIndices[0] = new Point(0, 2);
+        fieldNames[1] = Calendar.MINUTE;
+        fieldIndices[1] = new Point(3, 5);
+        if ((style & DWT.SHORT) !is 0) {
+            fieldNames[2] = Calendar.AM_PM;
+            fieldIndices[2] = new Point(6, 8);
+        } else {
+            fieldNames[2] = Calendar.SECOND;
+            fieldIndices[2] = new Point(6, 8);
+            fieldNames[3] = Calendar.AM_PM;
+            fieldIndices[3] = new Point(9, 11);
+        }
+    }
+}
+
+void setField(int fieldName, int value) {
+    if (calendar.get(fieldName) is value) return;
+    if (fieldName is Calendar.AM_PM) {
+        calendar.roll(Calendar.HOUR_OF_DAY, 12); // TODO: needs more work for setFormat and locale
+    }
+    calendar.set(fieldName, value);
+    notifyListeners(DWT.Selection, new Event());
+}
+
+void setTextField(int fieldName, int value, bool commit, bool adjust) {
+    if (commit) {
+        int max = calendar.getActualMaximum(fieldName);
+        int min = calendar.getActualMinimum(fieldName);
+        if (fieldName is Calendar.YEAR) {
+            max = MAX_YEAR;
+            min = MIN_YEAR;
+            /* Special case: convert 1 or 2-digit years into reasonable 4-digit years. */
+            int currentYear = Calendar.getInstance().get(Calendar.YEAR);
+            int currentCentury = (currentYear / 100) * 100;
+            if (value < (currentYear + 30) % 100) value += currentCentury;
+            else if (value < 100) value += currentCentury - 100;
+        }
+        if (value > max) value = min; // wrap
+        if (value < min) value = max; // wrap
+    }
+    int start = fieldIndices[currentField].x;
+    int end = fieldIndices[currentField].y;
+    text.setSelection(start, end);
+    char[] newValue = formattedStringValue(fieldName, value, adjust);
+    TangoText buffer = new TangoText(newValue);
+    /* Convert leading 0's into spaces. */
+    int prependCount = end - start - buffer.length();
+    for (int i = 0; i < prependCount; i++) {
+        buffer.prepend(' ');
+    }
+    newValue = buffer.toString();
+    ignoreVerify = true;
+    text.insert(newValue);
+    ignoreVerify = false;
+    selectField(currentField);
+    if (commit) setField(fieldName, value);
+}
+
+/**
+ * Sets the receiver's date, or day of the month, to the specified day.
+ * <p>
+ * The first day of the month is 1, and the last day depends on the month and year.
+ * </p>
+ *
+ * @param day a positive integer beginning with 1
+ *
+ * @exception DWTException <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>
+ */
+public void setDay (int day) {
+    checkWidget ();
+    if (!isValid(Calendar.DAY_OF_MONTH, day)) return;
+    if ((style & DWT.CALENDAR) !is 0) {
+        this.day = day;
+        OS.gtk_calendar_select_day(handle, day);
+    } else {
+        calendar.set(Calendar.DAY_OF_MONTH, day);
+        updateControl();
+    }
+}
+
+/**
+ * Sets the receiver's hours.
+ * <p>
+ * Hours is an integer between 0 and 23.
+ * </p>
+ *
+ * @param hours an integer between 0 and 23
+ *
+ * @exception DWTException <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>
+ */
+public void setHours (int hours) {
+    checkWidget ();
+    if (!isValid(Calendar.HOUR_OF_DAY, hours)) return;
+    if ((style & DWT.CALENDAR) !is 0) {
+        this.hours = hours;
+    } else {
+        calendar.set(Calendar.HOUR_OF_DAY, hours);
+        updateControl();
+    }
+}
+
+/**
+ * Sets the receiver's minutes.
+ * <p>
+ * Minutes is an integer between 0 and 59.
+ * </p>
+ *
+ * @param minutes an integer between 0 and 59
+ *
+ * @exception DWTException <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>
+ */
+public void setMinutes (int minutes) {
+    checkWidget ();
+    if (!isValid(Calendar.MINUTE, minutes)) return;
+    if ((style & DWT.CALENDAR) !is 0) {
+        this.minutes = minutes;
+    } else {
+        calendar.set(Calendar.MINUTE, minutes);
+        updateControl();
+    }
+}
+
+/**
+ * Sets the receiver's month.
+ * <p>
+ * The first month of the year is 0, and the last month is 11.
+ * </p>
+ *
+ * @param month an integer between 0 and 11
+ *
+ * @exception DWTException <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>
+ */
+public void setMonth (int month) {
+    checkWidget ();
+    if (!isValid(Calendar.MONTH, month)) return;
+    if ((style & DWT.CALENDAR) !is 0) {
+        this.month = month;
+        OS.gtk_calendar_select_month(handle, month, year);
+    } else {
+        calendar.set(Calendar.MONTH, month);
+        updateControl();
+    }
+}
+
+/**
+ * Sets the receiver's seconds.
+ * <p>
+ * Seconds is an integer between 0 and 59.
+ * </p>
+ *
+ * @param seconds an integer between 0 and 59
+ *
+ * @exception DWTException <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>
+ */
+public void setSeconds (int seconds) {
+    checkWidget ();
+    if (!isValid(Calendar.SECOND, seconds)) return;
+    if ((style & DWT.CALENDAR) !is 0) {
+        this.seconds = seconds;
+    } else {
+        calendar.set(Calendar.SECOND, seconds);
+        updateControl();
+    }
+}
+
+/**
+ * Sets the receiver's year.
+ * <p>
+ * The first year is 1752 and the last year is 9999.
+ * </p>
+ *
+ * @param year an integer between 1752 and 9999
+ *
+ * @exception DWTException <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>
+ */
+public void setYear (int year) {
+    checkWidget ();
+    //if (!isValid(Calendar.YEAR, year)) return;
+    if (year < MIN_YEAR || year > MAX_YEAR) return;
+    if ((style & DWT.CALENDAR) !is 0) {
+        this.year = year;
+        OS.gtk_calendar_select_month(handle, month, year);
+    } else {
+        calendar.set(Calendar.YEAR, year);
+        updateControl();
+    }
+}
+
+int unformattedIntValue(int fieldName, char[] newText, bool adjust, int max) {
+    int newValue;
+    try {
+        newValue = to!(int)(newText);
+    } catch (ConversionException ex) {
+        return -1;
+    }
+    if (fieldName is Calendar.MONTH && adjust) {
+        newValue--;
+        if (newValue is -1) newValue = max;
+    }
+    if (fieldName is Calendar.HOUR && adjust) {
+        if (newValue is 12) newValue = 0; // TODO: needs more work for setFormat and locale
+    }
+    return newValue;
+}
+
+public void updateControl() {
+    if (text !is null) {
+        char[] string = getFormattedString(style);
+        ignoreVerify = true;
+        text.setText(string);
+        ignoreVerify = false;
+    }
+    redraw();
+}
+}