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