]> git.saurik.com Git - wxWidgets.git/blobdiff - src/generic/timectrl.cpp
Add wxTimePickerCtrl class.
[wxWidgets.git] / src / generic / timectrl.cpp
diff --git a/src/generic/timectrl.cpp b/src/generic/timectrl.cpp
new file mode 100644 (file)
index 0000000..38218a6
--- /dev/null
@@ -0,0 +1,672 @@
+///////////////////////////////////////////////////////////////////////////////
+// Name:        src/generic/timectrl.cpp
+// Purpose:     Generic implementation of wxTimePickerCtrl.
+// Author:      Paul Breen, Vadim Zeitlin
+// Created:     2011-09-22
+// RCS-ID:      $Id: wxhead.cpp,v 1.11 2010-04-22 12:44:51 zeitlin Exp $
+// Copyright:   (c) 2011 Vadim Zeitlin <vadim@wxwidgets.org>
+// Licence:     wxWindows licence
+///////////////////////////////////////////////////////////////////////////////
+
+// ============================================================================
+// declarations
+// ============================================================================
+
+// ----------------------------------------------------------------------------
+// headers
+// ----------------------------------------------------------------------------
+
+// for compilers that support precompilation, includes "wx.h".
+#include "wx/wxprec.h"
+
+#ifdef __BORLANDC__
+    #pragma hdrstop
+#endif
+
+#if wxUSE_TIMEPICKCTRL
+
+#ifndef WX_PRECOMP
+    #include "wx/textctrl.h"
+#endif // WX_PRECOMP
+
+#include "wx/timectrl.h"
+
+// This class is only compiled if there is no native version or if we
+// explicitly want to use both the native and generic one (this is useful for
+// testing but not much otherwise and so by default we don't use the generic
+// implementation if a native one is available).
+#if !defined(wxHAS_NATIVE_TIMEPICKERCTRL) || wxUSE_TIMEPICKCTRL_GENERIC
+
+#include "wx/generic/timectrl.h"
+
+#include "wx/dateevt.h"
+#include "wx/spinbutt.h"
+
+#ifndef wxHAS_NATIVE_TIMEPICKERCTRL
+    IMPLEMENT_DYNAMIC_CLASS(wxTimePickerCtrl, wxControl)
+#endif
+
+// ----------------------------------------------------------------------------
+// Constants
+// ----------------------------------------------------------------------------
+
+enum
+{
+    // Horizontal margin between the text and spin control.
+    HMARGIN_TEXT_SPIN = 2
+};
+
+// ----------------------------------------------------------------------------
+// wxTimePickerGenericImpl: used to implement wxTimePickerCtrlGeneric
+// ----------------------------------------------------------------------------
+
+class wxTimePickerGenericImpl : public wxEvtHandler
+{
+public:
+    wxTimePickerGenericImpl(wxTimePickerCtrlGeneric* ctrl)
+    {
+        m_text = new wxTextCtrl(ctrl, wxID_ANY, wxString());
+
+        // As this text can't be edited, don't use the standard cursor for it
+        // to avoid misleading the user. Ideally we'd also hide the caret but
+        // this is not currently supported by wxTextCtrl.
+        m_text->SetCursor(wxCURSOR_ARROW);
+
+        m_btn = new wxSpinButton(ctrl);
+
+        m_currentField = Field_Hour;
+        m_isFirstDigit = true;
+
+        // We don't support arbitrary formats currently as this requires
+        // significantly more work both here and also in wxLocale::GetInfo().
+        //
+        // For now just use either "%H:%M:%S" or "%I:%M:%S %p". It would be
+        // nice to add support to "%k" and "%l" (hours with leading blanks
+        // instead of zeros) too as this is the most common unsupported case in
+        // practice.
+        m_useAMPM = wxLocale::GetInfo(wxLOCALE_TIME_FMT).Contains("%p");
+
+        m_text->Connect
+                (
+                    wxEVT_SET_FOCUS,
+                    wxFocusEventHandler(wxTimePickerGenericImpl::OnTextSetFocus),
+                    NULL,
+                    this
+                );
+        m_text->Connect
+                (
+                    wxEVT_KEY_DOWN,
+                    wxKeyEventHandler(wxTimePickerGenericImpl::OnTextKeyDown),
+                    NULL,
+                    this
+                );
+        m_text->Connect
+                (
+                    wxEVT_LEFT_DOWN,
+                    wxMouseEventHandler(wxTimePickerGenericImpl::OnTextClick),
+                    NULL,
+                    this
+                );
+
+        m_btn->Connect
+               (
+                    wxEVT_SPIN_UP,
+                    wxSpinEventHandler(wxTimePickerGenericImpl::OnArrowUp),
+                    NULL,
+                    this
+               );
+        m_btn->Connect
+               (
+                    wxEVT_SPIN_DOWN,
+                    wxSpinEventHandler(wxTimePickerGenericImpl::OnArrowDown),
+                    NULL,
+                    this
+               );
+    }
+
+    // Set the new value.
+    void SetValue(const wxDateTime& time)
+    {
+        m_time = time.IsValid() ? time : wxDateTime::Now();
+
+        UpdateTextWithoutEvent();
+    }
+
+
+    // The text part of the control.
+    wxTextCtrl* m_text;
+
+    // The spin button used to change the text fields.
+    wxSpinButton* m_btn;
+
+    // The current time (date part is ignored).
+    wxDateTime m_time;
+
+private:
+    // The logical fields of the text control (AM/PM one may not be present).
+    enum Field
+    {
+        Field_Hour,
+        Field_Min,
+        Field_Sec,
+        Field_AMPM,
+        Field_Max
+    };
+
+    // Direction of change of time fields.
+    enum Direction
+    {
+        // Notice that the enum elements values matter.
+        Dir_Down = -1,
+        Dir_Up   = +1
+    };
+
+    // A range of character positions ("from" is inclusive, "to" -- exclusive).
+    struct CharRange
+    {
+        int from,
+            to;
+    };
+
+    // Event handlers for various events in our controls.
+    void OnTextSetFocus(wxFocusEvent& event)
+    {
+        HighlightCurrentField();
+
+        event.Skip();
+    }
+
+    // Keyboard interface here is modelled over MSW native control and may need
+    // adjustments for other platforms.
+    void OnTextKeyDown(wxKeyEvent& event)
+    {
+        const int key = event.GetKeyCode();
+
+        switch ( key )
+        {
+            case WXK_DOWN:
+                ChangeCurrentFieldBy1(Dir_Down);
+                break;
+
+            case WXK_UP:
+                ChangeCurrentFieldBy1(Dir_Up);
+                break;
+
+            case WXK_LEFT:
+                CycleCurrentField(Dir_Down);
+                break;
+
+            case WXK_RIGHT:
+                CycleCurrentField(Dir_Up);
+                break;
+
+            case WXK_HOME:
+                ResetCurrentField(Dir_Down);
+                break;
+
+            case WXK_END:
+                ResetCurrentField(Dir_Up);
+                break;
+
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                // The digits work in all keys except AM/PM.
+                if ( m_currentField != Field_AMPM )
+                {
+                    AppendDigitToCurrentField(key - '0');
+                }
+                break;
+
+            case 'A':
+            case 'P':
+                // These keys only work to toggle AM/PM field.
+                if ( m_currentField == Field_AMPM )
+                {
+                    unsigned hour = m_time.GetHour();
+                    if ( key == 'A' )
+                    {
+                        if ( hour >= 12 )
+                            hour -= 12;
+                    }
+                    else // PM
+                    {
+                        if ( hour < 12 )
+                            hour += 12;
+                    }
+
+                    if ( hour != m_time.GetHour() )
+                    {
+                        m_time.SetHour(hour);
+                        UpdateText();
+                    }
+                }
+                break;
+
+            // Do not skip the other events, just consume them to prevent the
+            // user from editing the text directly.
+        }
+    }
+
+    void OnTextClick(wxMouseEvent& event)
+    {
+        Field field wxDUMMY_INITIALIZE(Field_Max);
+        long pos;
+        switch ( m_text->HitTest(event.GetPosition(), &pos) )
+        {
+            case wxTE_HT_UNKNOWN:
+                // Don't do anything, it's better than doing something wrong.
+                return;
+
+            case wxTE_HT_BEFORE:
+                // Select the first field.
+                field = Field_Hour;
+                break;
+
+            case wxTE_HT_ON_TEXT:
+                // Find the field containing this position.
+                for ( field = Field_Hour; field <= GetLastField(); )
+                {
+                    const CharRange range = GetFieldRange(field);
+
+                    // Normally the "to" end is exclusive but we want to give
+                    // focus to some field when the user clicks between them so
+                    // count it as part of the preceding field here.
+                    if ( range.from <= pos && pos <= range.to )
+                        break;
+
+                    field = static_cast<Field>(field + 1);
+                }
+                break;
+
+            case wxTE_HT_BELOW:
+                // This shouldn't happen for single line control.
+                wxFAIL_MSG( "Unreachable" );
+                // fall through
+
+            case wxTE_HT_BEYOND:
+                // Select the last field.
+                field = GetLastField();
+                break;
+        }
+
+        ChangeCurrentField(field);
+
+        // As we don't skip the event, we also prevent the system from setting
+        // focus to this control as it does by default, so do it manually.
+        m_text->SetFocus();
+    }
+
+    void OnArrowUp(wxSpinEvent& WXUNUSED(event))
+    {
+        ChangeCurrentFieldBy1(Dir_Up);
+
+        m_text->SetFocus();
+    }
+
+    void OnArrowDown(wxSpinEvent& WXUNUSED(event))
+    {
+        ChangeCurrentFieldBy1(Dir_Down);
+
+        m_text->SetFocus();
+    }
+
+
+    // Get the range of the given field in character positions ("from" is
+    // inclusive, "to" exclusive).
+    static CharRange GetFieldRange(Field field)
+    {
+        // Currently we can just hard code the ranges as they are the same for
+        // both supported formats, if we want to support arbitrary formats in
+        // the future, we'd need to determine them dynamically by examining the
+        // format here.
+        static const CharRange ranges[] =
+        {
+            { 0, 2 },
+            { 3, 5 },
+            { 6, 8 },
+            { 9, 11},
+        };
+
+        wxCOMPILE_TIME_ASSERT( WXSIZEOF(ranges) == Field_Max,
+                               FieldRangesMismatch );
+
+        return ranges[field];
+    }
+
+    // Get the last field used depending on m_useAMPM.
+    Field GetLastField() const
+    {
+        return m_useAMPM ? Field_AMPM : Field_Sec;
+    }
+
+    // Change the current field. For convenience, accept int field here as this
+    // allows us to use arithmetic operations in the caller.
+    void ChangeCurrentField(int field)
+    {
+        if ( field == m_currentField )
+            return;
+
+        wxCHECK_RET( field <= GetLastField(), "Invalid field" );
+
+        m_currentField = static_cast<Field>(field);
+        m_isFirstDigit = true;
+
+        HighlightCurrentField();
+    }
+
+    // Go to the next (Dir_Up) or previous (Dir_Down) field, wrapping if
+    // necessary.
+    void CycleCurrentField(Direction dir)
+    {
+        const unsigned numFields = GetLastField() + 1;
+
+        ChangeCurrentField((m_currentField + numFields + dir) % numFields);
+    }
+
+    // Select the currently actively field.
+    void HighlightCurrentField()
+    {
+        const CharRange range = GetFieldRange(m_currentField);
+
+        m_text->SetSelection(range.from, range.to);
+    }
+
+    // Decrement or increment the value of the current field (wrapping if
+    // necessary).
+    void ChangeCurrentFieldBy1(Direction dir)
+    {
+        switch ( m_currentField )
+        {
+            case Field_Hour:
+                m_time.SetHour((m_time.GetHour() + 24 + dir) % 24);
+                break;
+
+            case Field_Min:
+                m_time.SetMinute((m_time.GetMinute() + 60 + dir) % 60);
+                break;
+
+            case Field_Sec:
+                m_time.SetSecond((m_time.GetSecond() + 60 + dir) % 60);
+                break;
+
+            case Field_AMPM:
+                m_time.SetHour((m_time.GetHour() + 12) % 24);
+                break;
+
+            case Field_Max:
+                wxFAIL_MSG( "Invalid field" );
+        }
+
+        UpdateText();
+    }
+
+    // Set the current field to its minimal or maximal value.
+    void ResetCurrentField(Direction dir)
+    {
+        switch ( m_currentField )
+        {
+            case Field_Hour:
+            case Field_AMPM:
+                // In 12-hour mode setting the hour to the minimal value
+                // also changes the suffix to AM and, correspondingly,
+                // setting it to the maximal one changes the suffix to PM.
+                // And, for consistency with the native MSW behaviour, we
+                // also do the same thing when changing AM/PM field itself,
+                // so change hours in any case.
+                m_time.SetHour(dir == Dir_Down ? 0 : 23);
+                break;
+
+            case Field_Min:
+                m_time.SetMinute(dir == Dir_Down ? 0 : 59);
+                break;
+
+            case Field_Sec:
+                m_time.SetSecond(dir == Dir_Down ? 0 : 59);
+                break;
+
+            case Field_Max:
+                wxFAIL_MSG( "Invalid field" );
+        }
+
+        UpdateText();
+    }
+
+    // Append the given digit (from 0 to 9) to the current value of the current
+    // field.
+    void AppendDigitToCurrentField(int n)
+    {
+        bool moveToNextField = false;
+
+        if ( !m_isFirstDigit )
+        {
+            // The first digit simply replaces the existing field contents,
+            // but the second one should be combined with the previous one,
+            // otherwise entering 2-digit numbers would be impossible.
+            int currentValue wxDUMMY_INITIALIZE(0),
+                maxValue wxDUMMY_INITIALIZE(0);
+
+            switch ( m_currentField )
+            {
+                case Field_Hour:
+                    currentValue = m_time.GetHour();
+                    maxValue = 23;
+                    break;
+
+                case Field_Min:
+                    currentValue = m_time.GetMinute();
+                    maxValue = 59;
+                    break;
+
+                case Field_Sec:
+                    currentValue = m_time.GetSecond();
+                    maxValue = 59;
+                    break;
+
+                case Field_AMPM:
+                case Field_Max:
+                    wxFAIL_MSG( "Invalid field" );
+            }
+
+            // Check if the new value is acceptable. If not, we just handle
+            // this digit as if it were the first one.
+            int newValue = currentValue*10 + n;
+            if ( newValue < maxValue )
+            {
+                n = newValue;
+
+                // If we're not on the seconds field, advance to the next one.
+                // This makes it more convenient to enter times as you can just
+                // press all digits one after one without touching the cursor
+                // arrow keys at all.
+                //
+                // Notice that MSW native control doesn't do this but it seems
+                // so useful that we intentionally diverge from it here.
+                moveToNextField = true;
+
+                // We entered both digits so the next one will be "first" again.
+                m_isFirstDigit = true;
+            }
+        }
+        else // First digit entered.
+        {
+            // The next one won't be first any more.
+            m_isFirstDigit = false;
+        }
+
+        switch ( m_currentField )
+        {
+            case Field_Hour:
+                m_time.SetHour(n);
+                break;
+
+            case Field_Min:
+                m_time.SetMinute(n);
+                break;
+
+            case Field_Sec:
+                m_time.SetSecond(n);
+                break;
+
+            case Field_AMPM:
+            case Field_Max:
+                wxFAIL_MSG( "Invalid field" );
+        }
+
+        if ( moveToNextField && m_currentField < Field_Sec )
+            CycleCurrentField(Dir_Up);
+
+        UpdateText();
+    }
+
+    // Update the text value to correspond to the current time. By default also
+    // generate an event but this can be avoided by calling the "WithoutEvent"
+    // variant.
+    void UpdateText()
+    {
+        UpdateTextWithoutEvent();
+
+        wxWindow* const ctrl = m_text->GetParent();
+
+        wxDateEvent event(ctrl, m_time, wxEVT_TIME_CHANGED);
+        ctrl->HandleWindowEvent(event);
+    }
+
+    void UpdateTextWithoutEvent()
+    {
+        m_text->SetValue(m_time.Format(m_useAMPM ? "%I:%M:%S %p" : "%H:%M:%S"));
+
+        HighlightCurrentField();
+    }
+
+
+    // The current field of the text control: this is the one affected by
+    // pressing arrow keys or spin button.
+    Field m_currentField;
+
+    // Flag indicating whether we use AM/PM indicator or not.
+    bool m_useAMPM;
+
+    // Flag indicating whether the next digit pressed by user will be the first
+    // digit of the current field or the second one. This is necessary because
+    // the first digit replaces the current field contents while the second one
+    // is appended to it (if possible, e.g. pressing '7' in a field already
+    // containing '8' will still replace it as "78" would be invalid).
+    bool m_isFirstDigit;
+
+    wxDECLARE_NO_COPY_CLASS(wxTimePickerGenericImpl);
+};
+
+// ============================================================================
+// wxTimePickerCtrlGeneric implementation
+// ============================================================================
+
+// ----------------------------------------------------------------------------
+// wxTimePickerCtrlGeneric creation
+// ----------------------------------------------------------------------------
+
+void wxTimePickerCtrlGeneric::Init()
+{
+    m_impl = NULL;
+}
+
+bool
+wxTimePickerCtrlGeneric::Create(wxWindow *parent,
+                                wxWindowID id,
+                                const wxDateTime& date,
+                                const wxPoint& pos,
+                                const wxSize& size,
+                                long style,
+                                const wxValidator& validator,
+                                const wxString& name)
+{
+    // The text control we use already has a border, so we don't need one
+    // ourselves.
+    style &= ~wxBORDER_MASK;
+    style |= wxBORDER_NONE;
+
+    if ( !Base::Create(parent, id, pos, size, style, validator, name) )
+        return false;
+
+    m_impl = new wxTimePickerGenericImpl(this);
+    m_impl->SetValue(date);
+
+    InvalidateBestSize();
+    SetInitialSize(size);
+
+    return true;
+}
+
+wxTimePickerCtrlGeneric::~wxTimePickerCtrlGeneric()
+{
+    delete m_impl;
+}
+
+wxWindowList wxTimePickerCtrlGeneric::GetCompositeWindowParts() const
+{
+    wxWindowList parts;
+    if ( m_impl )
+    {
+        parts.push_back(m_impl->m_text);
+        parts.push_back(m_impl->m_btn);
+    }
+    return parts;
+}
+
+// ----------------------------------------------------------------------------
+// wxTimePickerCtrlGeneric value
+// ----------------------------------------------------------------------------
+
+void wxTimePickerCtrlGeneric::SetValue(const wxDateTime& date)
+{
+    wxCHECK_RET( m_impl, "Must create first" );
+
+    m_impl->SetValue(date);
+}
+
+wxDateTime wxTimePickerCtrlGeneric::GetValue() const
+{
+    wxCHECK_MSG( m_impl, wxDateTime(), "Must create first" );
+
+    return m_impl->m_time;
+}
+
+// ----------------------------------------------------------------------------
+// wxTimePickerCtrlGeneric geometry
+// ----------------------------------------------------------------------------
+
+void wxTimePickerCtrlGeneric::DoMoveWindow(int x, int y, int width, int height)
+{
+    Base::DoMoveWindow(x, y, width, height);
+
+    if ( !m_impl )
+        return;
+
+    const int widthBtn = m_impl->m_btn->GetSize().x;
+    const int widthText = width - widthBtn - HMARGIN_TEXT_SPIN;
+
+    m_impl->m_text->SetSize(0, 0, widthText, height);
+    m_impl->m_btn->SetSize(widthText + HMARGIN_TEXT_SPIN, 0, widthBtn, height);
+}
+
+wxSize wxTimePickerCtrlGeneric::DoGetBestSize() const
+{
+    if ( !m_impl )
+        return Base::DoGetBestSize();
+
+    wxSize size = m_impl->m_text->GetBestSize();
+    size.x += m_impl->m_btn->GetBestSize().x + HMARGIN_TEXT_SPIN;
+
+    return size;
+}
+
+#endif // !wxHAS_NATIVE_TIMEPICKERCTRL || wxUSE_TIMEPICKCTRL_GENERIC
+
+#endif // wxUSE_TIMEPICKCTRL