+///////////////////////////////////////////////////////////////////////////////
+// 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