X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/1319b2684fe48d168282de7b6ce9a113a206d87e..569c7d8ccb0c3f350b50f0f8435b6c6ac3d78cc8:/src/generic/timectrl.cpp?ds=inline diff --git a/src/generic/timectrl.cpp b/src/generic/timectrl.cpp new file mode 100644 index 0000000000..38218a6a62 --- /dev/null +++ b/src/generic/timectrl.cpp @@ -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 +// 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 + 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); + 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