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