]> git.saurik.com Git - wxWidgets.git/blob - src/generic/timectrlg.cpp
71867c2f35727aea3abf9dce66b0e537f78f4422
[wxWidgets.git] / src / generic / timectrlg.cpp
1 ///////////////////////////////////////////////////////////////////////////////
2 // Name: src/generic/timectrl.cpp
3 // Purpose: Generic implementation of wxTimePickerCtrl.
4 // Author: Paul Breen, Vadim Zeitlin
5 // Created: 2011-09-22
6 // Copyright: (c) 2011 Vadim Zeitlin <vadim@wxwidgets.org>
7 // Licence: wxWindows licence
8 ///////////////////////////////////////////////////////////////////////////////
9
10 // ============================================================================
11 // declarations
12 // ============================================================================
13
14 // ----------------------------------------------------------------------------
15 // headers
16 // ----------------------------------------------------------------------------
17
18 // for compilers that support precompilation, includes "wx.h".
19 #include "wx/wxprec.h"
20
21 #ifdef __BORLANDC__
22 #pragma hdrstop
23 #endif
24
25 #if wxUSE_TIMEPICKCTRL
26
27 #ifndef WX_PRECOMP
28 #include "wx/textctrl.h"
29 #endif // WX_PRECOMP
30
31 #include "wx/timectrl.h"
32
33 // This class is only compiled if there is no native version or if we
34 // explicitly want to use both the native and generic one (this is useful for
35 // testing but not much otherwise and so by default we don't use the generic
36 // implementation if a native one is available).
37 #if !defined(wxHAS_NATIVE_TIMEPICKERCTRL) || wxUSE_TIMEPICKCTRL_GENERIC
38
39 #include "wx/generic/timectrl.h"
40
41 #include "wx/dateevt.h"
42 #include "wx/spinbutt.h"
43
44 #ifndef wxHAS_NATIVE_TIMEPICKERCTRL
45 IMPLEMENT_DYNAMIC_CLASS(wxTimePickerCtrl, wxControl)
46 #endif
47
48 // ----------------------------------------------------------------------------
49 // Constants
50 // ----------------------------------------------------------------------------
51
52 enum
53 {
54 // Horizontal margin between the text and spin control.
55 HMARGIN_TEXT_SPIN = 2
56 };
57
58 // ----------------------------------------------------------------------------
59 // wxTimePickerGenericImpl: used to implement wxTimePickerCtrlGeneric
60 // ----------------------------------------------------------------------------
61
62 class wxTimePickerGenericImpl : public wxEvtHandler
63 {
64 public:
65 wxTimePickerGenericImpl(wxTimePickerCtrlGeneric* ctrl)
66 {
67 m_text = new wxTextCtrl(ctrl, wxID_ANY, wxString());
68
69 // As this text can't be edited, don't use the standard cursor for it
70 // to avoid misleading the user. Ideally we'd also hide the caret but
71 // this is not currently supported by wxTextCtrl.
72 m_text->SetCursor(wxCURSOR_ARROW);
73
74 m_btn = new wxSpinButton(ctrl, wxID_ANY,
75 wxDefaultPosition, wxDefaultSize,
76 wxSP_VERTICAL | wxSP_WRAP);
77
78 m_currentField = Field_Hour;
79 m_isFirstDigit = true;
80
81 // We don't support arbitrary formats currently as this requires
82 // significantly more work both here and also in wxLocale::GetInfo().
83 //
84 // For now just use either "%H:%M:%S" or "%I:%M:%S %p". It would be
85 // nice to add support to "%k" and "%l" (hours with leading blanks
86 // instead of zeros) too as this is the most common unsupported case in
87 // practice.
88 m_useAMPM = wxLocale::GetInfo(wxLOCALE_TIME_FMT).Contains("%p");
89
90 m_text->Connect
91 (
92 wxEVT_SET_FOCUS,
93 wxFocusEventHandler(wxTimePickerGenericImpl::OnTextSetFocus),
94 NULL,
95 this
96 );
97 m_text->Connect
98 (
99 wxEVT_KEY_DOWN,
100 wxKeyEventHandler(wxTimePickerGenericImpl::OnTextKeyDown),
101 NULL,
102 this
103 );
104 m_text->Connect
105 (
106 wxEVT_LEFT_DOWN,
107 wxMouseEventHandler(wxTimePickerGenericImpl::OnTextClick),
108 NULL,
109 this
110 );
111
112 m_btn->Connect
113 (
114 wxEVT_SPIN_UP,
115 wxSpinEventHandler(wxTimePickerGenericImpl::OnArrowUp),
116 NULL,
117 this
118 );
119 m_btn->Connect
120 (
121 wxEVT_SPIN_DOWN,
122 wxSpinEventHandler(wxTimePickerGenericImpl::OnArrowDown),
123 NULL,
124 this
125 );
126 }
127
128 // Set the new value.
129 void SetValue(const wxDateTime& time)
130 {
131 m_time = time.IsValid() ? time : wxDateTime::Now();
132
133 // Ensure that the date part doesn't correspond to a DST change date as
134 // time is discontinuous then resulting in many problems, e.g. it's
135 // impossible to even enter 2:00:00 at the beginning of summer time
136 // date as this time doesn't exist. By using Jan 1, on which nobody
137 // changes DST, we avoid all such problems.
138 wxDateTime::Tm tm = m_time.GetTm();
139 tm.mday =
140 tm.yday = 1;
141 tm.mon = wxDateTime::Jan;
142 m_time.Set(tm);
143
144 UpdateTextWithoutEvent();
145 }
146
147
148 // The text part of the control.
149 wxTextCtrl* m_text;
150
151 // The spin button used to change the text fields.
152 wxSpinButton* m_btn;
153
154 // The current time (date part is ignored).
155 wxDateTime m_time;
156
157 private:
158 // The logical fields of the text control (AM/PM one may not be present).
159 enum Field
160 {
161 Field_Hour,
162 Field_Min,
163 Field_Sec,
164 Field_AMPM,
165 Field_Max
166 };
167
168 // Direction of change of time fields.
169 enum Direction
170 {
171 // Notice that the enum elements values matter.
172 Dir_Down = -1,
173 Dir_Up = +1
174 };
175
176 // A range of character positions ("from" is inclusive, "to" -- exclusive).
177 struct CharRange
178 {
179 int from,
180 to;
181 };
182
183 // Event handlers for various events in our controls.
184 void OnTextSetFocus(wxFocusEvent& event)
185 {
186 HighlightCurrentField();
187
188 event.Skip();
189 }
190
191 // Keyboard interface here is modelled over MSW native control and may need
192 // adjustments for other platforms.
193 void OnTextKeyDown(wxKeyEvent& event)
194 {
195 const int key = event.GetKeyCode();
196
197 switch ( key )
198 {
199 case WXK_DOWN:
200 ChangeCurrentFieldBy1(Dir_Down);
201 break;
202
203 case WXK_UP:
204 ChangeCurrentFieldBy1(Dir_Up);
205 break;
206
207 case WXK_LEFT:
208 CycleCurrentField(Dir_Down);
209 break;
210
211 case WXK_RIGHT:
212 CycleCurrentField(Dir_Up);
213 break;
214
215 case WXK_HOME:
216 ResetCurrentField(Dir_Down);
217 break;
218
219 case WXK_END:
220 ResetCurrentField(Dir_Up);
221 break;
222
223 case '0':
224 case '1':
225 case '2':
226 case '3':
227 case '4':
228 case '5':
229 case '6':
230 case '7':
231 case '8':
232 case '9':
233 // The digits work in all keys except AM/PM.
234 if ( m_currentField != Field_AMPM )
235 {
236 AppendDigitToCurrentField(key - '0');
237 }
238 break;
239
240 case 'A':
241 case 'P':
242 // These keys only work to toggle AM/PM field.
243 if ( m_currentField == Field_AMPM )
244 {
245 unsigned hour = m_time.GetHour();
246 if ( key == 'A' )
247 {
248 if ( hour >= 12 )
249 hour -= 12;
250 }
251 else // PM
252 {
253 if ( hour < 12 )
254 hour += 12;
255 }
256
257 if ( hour != m_time.GetHour() )
258 {
259 m_time.SetHour(hour);
260 UpdateText();
261 }
262 }
263 break;
264
265 // Do not skip the other events, just consume them to prevent the
266 // user from editing the text directly.
267 }
268 }
269
270 void OnTextClick(wxMouseEvent& event)
271 {
272 Field field = Field_Max; // Initialize just to suppress warnings.
273 long pos;
274 switch ( m_text->HitTest(event.GetPosition(), &pos) )
275 {
276 case wxTE_HT_UNKNOWN:
277 // Don't do anything, it's better than doing something wrong.
278 return;
279
280 case wxTE_HT_BEFORE:
281 // Select the first field.
282 field = Field_Hour;
283 break;
284
285 case wxTE_HT_ON_TEXT:
286 // Find the field containing this position.
287 for ( field = Field_Hour; field <= GetLastField(); )
288 {
289 const CharRange range = GetFieldRange(field);
290
291 // Normally the "to" end is exclusive but we want to give
292 // focus to some field when the user clicks between them so
293 // count it as part of the preceding field here.
294 if ( range.from <= pos && pos <= range.to )
295 break;
296
297 field = static_cast<Field>(field + 1);
298 }
299 break;
300
301 case wxTE_HT_BELOW:
302 // This shouldn't happen for single line control.
303 wxFAIL_MSG( "Unreachable" );
304 // fall through
305
306 case wxTE_HT_BEYOND:
307 // Select the last field.
308 field = GetLastField();
309 break;
310 }
311
312 ChangeCurrentField(field);
313 }
314
315 void OnArrowUp(wxSpinEvent& WXUNUSED(event))
316 {
317 ChangeCurrentFieldBy1(Dir_Up);
318 }
319
320 void OnArrowDown(wxSpinEvent& WXUNUSED(event))
321 {
322 ChangeCurrentFieldBy1(Dir_Down);
323 }
324
325
326 // Get the range of the given field in character positions ("from" is
327 // inclusive, "to" exclusive).
328 static CharRange GetFieldRange(Field field)
329 {
330 // Currently we can just hard code the ranges as they are the same for
331 // both supported formats, if we want to support arbitrary formats in
332 // the future, we'd need to determine them dynamically by examining the
333 // format here.
334 static const CharRange ranges[] =
335 {
336 { 0, 2 },
337 { 3, 5 },
338 { 6, 8 },
339 { 9, 11},
340 };
341
342 wxCOMPILE_TIME_ASSERT( WXSIZEOF(ranges) == Field_Max,
343 FieldRangesMismatch );
344
345 return ranges[field];
346 }
347
348 // Get the last field used depending on m_useAMPM.
349 Field GetLastField() const
350 {
351 return m_useAMPM ? Field_AMPM : Field_Sec;
352 }
353
354 // Change the current field. For convenience, accept int field here as this
355 // allows us to use arithmetic operations in the caller.
356 void ChangeCurrentField(int field)
357 {
358 if ( field == m_currentField )
359 return;
360
361 wxCHECK_RET( field <= GetLastField(), "Invalid field" );
362
363 m_currentField = static_cast<Field>(field);
364 m_isFirstDigit = true;
365
366 HighlightCurrentField();
367 }
368
369 // Go to the next (Dir_Up) or previous (Dir_Down) field, wrapping if
370 // necessary.
371 void CycleCurrentField(Direction dir)
372 {
373 const unsigned numFields = GetLastField() + 1;
374
375 ChangeCurrentField((m_currentField + numFields + dir) % numFields);
376 }
377
378 // Select the currently actively field.
379 void HighlightCurrentField()
380 {
381 m_text->SetFocus();
382
383 const CharRange range = GetFieldRange(m_currentField);
384
385 m_text->SetSelection(range.from, range.to);
386 }
387
388 // Decrement or increment the value of the current field (wrapping if
389 // necessary).
390 void ChangeCurrentFieldBy1(Direction dir)
391 {
392 switch ( m_currentField )
393 {
394 case Field_Hour:
395 m_time.SetHour((m_time.GetHour() + 24 + dir) % 24);
396 break;
397
398 case Field_Min:
399 m_time.SetMinute((m_time.GetMinute() + 60 + dir) % 60);
400 break;
401
402 case Field_Sec:
403 m_time.SetSecond((m_time.GetSecond() + 60 + dir) % 60);
404 break;
405
406 case Field_AMPM:
407 m_time.SetHour((m_time.GetHour() + 12) % 24);
408 break;
409
410 case Field_Max:
411 wxFAIL_MSG( "Invalid field" );
412 }
413
414 UpdateText();
415 }
416
417 // Set the current field to its minimal or maximal value.
418 void ResetCurrentField(Direction dir)
419 {
420 switch ( m_currentField )
421 {
422 case Field_Hour:
423 case Field_AMPM:
424 // In 12-hour mode setting the hour to the minimal value
425 // also changes the suffix to AM and, correspondingly,
426 // setting it to the maximal one changes the suffix to PM.
427 // And, for consistency with the native MSW behaviour, we
428 // also do the same thing when changing AM/PM field itself,
429 // so change hours in any case.
430 m_time.SetHour(dir == Dir_Down ? 0 : 23);
431 break;
432
433 case Field_Min:
434 m_time.SetMinute(dir == Dir_Down ? 0 : 59);
435 break;
436
437 case Field_Sec:
438 m_time.SetSecond(dir == Dir_Down ? 0 : 59);
439 break;
440
441 case Field_Max:
442 wxFAIL_MSG( "Invalid field" );
443 }
444
445 UpdateText();
446 }
447
448 // Append the given digit (from 0 to 9) to the current value of the current
449 // field.
450 void AppendDigitToCurrentField(int n)
451 {
452 bool moveToNextField = false;
453
454 if ( !m_isFirstDigit )
455 {
456 // The first digit simply replaces the existing field contents,
457 // but the second one should be combined with the previous one,
458 // otherwise entering 2-digit numbers would be impossible.
459 int currentValue = 0,
460 maxValue = 0;
461
462 switch ( m_currentField )
463 {
464 case Field_Hour:
465 currentValue = m_time.GetHour();
466 maxValue = 23;
467 break;
468
469 case Field_Min:
470 currentValue = m_time.GetMinute();
471 maxValue = 59;
472 break;
473
474 case Field_Sec:
475 currentValue = m_time.GetSecond();
476 maxValue = 59;
477 break;
478
479 case Field_AMPM:
480 case Field_Max:
481 wxFAIL_MSG( "Invalid field" );
482 return;
483 }
484
485 // Check if the new value is acceptable. If not, we just handle
486 // this digit as if it were the first one.
487 int newValue = currentValue*10 + n;
488 if ( newValue < maxValue )
489 {
490 n = newValue;
491
492 // If we're not on the seconds field, advance to the next one.
493 // This makes it more convenient to enter times as you can just
494 // press all digits one after one without touching the cursor
495 // arrow keys at all.
496 //
497 // Notice that MSW native control doesn't do this but it seems
498 // so useful that we intentionally diverge from it here.
499 moveToNextField = true;
500
501 // We entered both digits so the next one will be "first" again.
502 m_isFirstDigit = true;
503 }
504 }
505 else // First digit entered.
506 {
507 // The next one won't be first any more.
508 m_isFirstDigit = false;
509 }
510
511 switch ( m_currentField )
512 {
513 case Field_Hour:
514 m_time.SetHour(n);
515 break;
516
517 case Field_Min:
518 m_time.SetMinute(n);
519 break;
520
521 case Field_Sec:
522 m_time.SetSecond(n);
523 break;
524
525 case Field_AMPM:
526 case Field_Max:
527 wxFAIL_MSG( "Invalid field" );
528 }
529
530 if ( moveToNextField && m_currentField < Field_Sec )
531 CycleCurrentField(Dir_Up);
532
533 UpdateText();
534 }
535
536 // Update the text value to correspond to the current time. By default also
537 // generate an event but this can be avoided by calling the "WithoutEvent"
538 // variant.
539 void UpdateText()
540 {
541 UpdateTextWithoutEvent();
542
543 wxWindow* const ctrl = m_text->GetParent();
544
545 wxDateEvent event(ctrl, m_time, wxEVT_TIME_CHANGED);
546 ctrl->HandleWindowEvent(event);
547 }
548
549 void UpdateTextWithoutEvent()
550 {
551 m_text->SetValue(m_time.Format(m_useAMPM ? "%I:%M:%S %p" : "%H:%M:%S"));
552
553 HighlightCurrentField();
554 }
555
556
557 // The current field of the text control: this is the one affected by
558 // pressing arrow keys or spin button.
559 Field m_currentField;
560
561 // Flag indicating whether we use AM/PM indicator or not.
562 bool m_useAMPM;
563
564 // Flag indicating whether the next digit pressed by user will be the first
565 // digit of the current field or the second one. This is necessary because
566 // the first digit replaces the current field contents while the second one
567 // is appended to it (if possible, e.g. pressing '7' in a field already
568 // containing '8' will still replace it as "78" would be invalid).
569 bool m_isFirstDigit;
570
571 wxDECLARE_NO_COPY_CLASS(wxTimePickerGenericImpl);
572 };
573
574 // ============================================================================
575 // wxTimePickerCtrlGeneric implementation
576 // ============================================================================
577
578 // ----------------------------------------------------------------------------
579 // wxTimePickerCtrlGeneric creation
580 // ----------------------------------------------------------------------------
581
582 void wxTimePickerCtrlGeneric::Init()
583 {
584 m_impl = NULL;
585 }
586
587 bool
588 wxTimePickerCtrlGeneric::Create(wxWindow *parent,
589 wxWindowID id,
590 const wxDateTime& date,
591 const wxPoint& pos,
592 const wxSize& size,
593 long style,
594 const wxValidator& validator,
595 const wxString& name)
596 {
597 // The text control we use already has a border, so we don't need one
598 // ourselves.
599 style &= ~wxBORDER_MASK;
600 style |= wxBORDER_NONE;
601
602 if ( !Base::Create(parent, id, pos, size, style, validator, name) )
603 return false;
604
605 m_impl = new wxTimePickerGenericImpl(this);
606 m_impl->SetValue(date);
607
608 InvalidateBestSize();
609 SetInitialSize(size);
610
611 return true;
612 }
613
614 wxTimePickerCtrlGeneric::~wxTimePickerCtrlGeneric()
615 {
616 delete m_impl;
617 }
618
619 wxWindowList wxTimePickerCtrlGeneric::GetCompositeWindowParts() const
620 {
621 wxWindowList parts;
622 if ( m_impl )
623 {
624 parts.push_back(m_impl->m_text);
625 parts.push_back(m_impl->m_btn);
626 }
627 return parts;
628 }
629
630 // ----------------------------------------------------------------------------
631 // wxTimePickerCtrlGeneric value
632 // ----------------------------------------------------------------------------
633
634 void wxTimePickerCtrlGeneric::SetValue(const wxDateTime& date)
635 {
636 wxCHECK_RET( m_impl, "Must create first" );
637
638 m_impl->SetValue(date);
639 }
640
641 wxDateTime wxTimePickerCtrlGeneric::GetValue() const
642 {
643 wxCHECK_MSG( m_impl, wxDateTime(), "Must create first" );
644
645 return m_impl->m_time;
646 }
647
648 // ----------------------------------------------------------------------------
649 // wxTimePickerCtrlGeneric geometry
650 // ----------------------------------------------------------------------------
651
652 void wxTimePickerCtrlGeneric::DoMoveWindow(int x, int y, int width, int height)
653 {
654 Base::DoMoveWindow(x, y, width, height);
655
656 if ( !m_impl )
657 return;
658
659 const int widthBtn = m_impl->m_btn->GetSize().x;
660 const int widthText = width - widthBtn - HMARGIN_TEXT_SPIN;
661
662 m_impl->m_text->SetSize(0, 0, widthText, height);
663 m_impl->m_btn->SetSize(widthText + HMARGIN_TEXT_SPIN, 0, widthBtn, height);
664 }
665
666 wxSize wxTimePickerCtrlGeneric::DoGetBestSize() const
667 {
668 if ( !m_impl )
669 return Base::DoGetBestSize();
670
671 wxSize size = m_impl->m_text->GetBestSize();
672 size.x += m_impl->m_btn->GetBestSize().x + HMARGIN_TEXT_SPIN;
673
674 return size;
675 }
676
677 #endif // !wxHAS_NATIVE_TIMEPICKERCTRL || wxUSE_TIMEPICKCTRL_GENERIC
678
679 #endif // wxUSE_TIMEPICKCTRL