mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-08-28 21:41:28 +00:00
668 lines
20 KiB
C++
668 lines
20 KiB
C++
///////////////////////////////////////////////////////////////////////////////
|
|
// 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 <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, 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>(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>(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
|