/////////////////////////////////////////////////////////////////////////////// // Name: src/generic/timectrl.cpp // Purpose: Generic implementation of wxTimePickerCtrl. // Author: Paul Breen, Vadim Zeitlin // Created: 2011-09-22 // RCS-ID: $Id: timectrlg.cpp 69991 2011-12-12 14:01:23Z VZ $ // Copyright: (c) 2011 Vadim Zeitlin // 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, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_VERTICAL | wxSP_WRAP); 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 + 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); } void OnArrowUp(wxSpinEvent& WXUNUSED(event)) { ChangeCurrentFieldBy1(Dir_Up); } void OnArrowDown(wxSpinEvent& WXUNUSED(event)) { ChangeCurrentFieldBy1(Dir_Down); } // 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); 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() { m_text->SetFocus(); 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