]> git.saurik.com Git - wxWidgets.git/blob - src/generic/timectrl.cpp
Use wxSP_WRAP for generic wxTimePicker spin button.
[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, 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 // As we don't skip the event, we also prevent the system from setting
305 // focus to this control as it does by default, so do it manually.
306 m_text->SetFocus();
307 }
308
309 void OnArrowUp(wxSpinEvent& WXUNUSED(event))
310 {
311 ChangeCurrentFieldBy1(Dir_Up);
312
313 m_text->SetFocus();
314 }
315
316 void OnArrowDown(wxSpinEvent& WXUNUSED(event))
317 {
318 ChangeCurrentFieldBy1(Dir_Down);
319
320 m_text->SetFocus();
321 }
322
323
324 // Get the range of the given field in character positions ("from" is
325 // inclusive, "to" exclusive).
326 static CharRange GetFieldRange(Field field)
327 {
328 // Currently we can just hard code the ranges as they are the same for
329 // both supported formats, if we want to support arbitrary formats in
330 // the future, we'd need to determine them dynamically by examining the
331 // format here.
332 static const CharRange ranges[] =
333 {
334 { 0, 2 },
335 { 3, 5 },
336 { 6, 8 },
337 { 9, 11},
338 };
339
340 wxCOMPILE_TIME_ASSERT( WXSIZEOF(ranges) == Field_Max,
341 FieldRangesMismatch );
342
343 return ranges[field];
344 }
345
346 // Get the last field used depending on m_useAMPM.
347 Field GetLastField() const
348 {
349 return m_useAMPM ? Field_AMPM : Field_Sec;
350 }
351
352 // Change the current field. For convenience, accept int field here as this
353 // allows us to use arithmetic operations in the caller.
354 void ChangeCurrentField(int field)
355 {
356 if ( field == m_currentField )
357 return;
358
359 wxCHECK_RET( field <= GetLastField(), "Invalid field" );
360
361 m_currentField = static_cast<Field>(field);
362 m_isFirstDigit = true;
363
364 HighlightCurrentField();
365 }
366
367 // Go to the next (Dir_Up) or previous (Dir_Down) field, wrapping if
368 // necessary.
369 void CycleCurrentField(Direction dir)
370 {
371 const unsigned numFields = GetLastField() + 1;
372
373 ChangeCurrentField((m_currentField + numFields + dir) % numFields);
374 }
375
376 // Select the currently actively field.
377 void HighlightCurrentField()
378 {
379 const CharRange range = GetFieldRange(m_currentField);
380
381 m_text->SetSelection(range.from, range.to);
382 }
383
384 // Decrement or increment the value of the current field (wrapping if
385 // necessary).
386 void ChangeCurrentFieldBy1(Direction dir)
387 {
388 switch ( m_currentField )
389 {
390 case Field_Hour:
391 m_time.SetHour((m_time.GetHour() + 24 + dir) % 24);
392 break;
393
394 case Field_Min:
395 m_time.SetMinute((m_time.GetMinute() + 60 + dir) % 60);
396 break;
397
398 case Field_Sec:
399 m_time.SetSecond((m_time.GetSecond() + 60 + dir) % 60);
400 break;
401
402 case Field_AMPM:
403 m_time.SetHour((m_time.GetHour() + 12) % 24);
404 break;
405
406 case Field_Max:
407 wxFAIL_MSG( "Invalid field" );
408 }
409
410 UpdateText();
411 }
412
413 // Set the current field to its minimal or maximal value.
414 void ResetCurrentField(Direction dir)
415 {
416 switch ( m_currentField )
417 {
418 case Field_Hour:
419 case Field_AMPM:
420 // In 12-hour mode setting the hour to the minimal value
421 // also changes the suffix to AM and, correspondingly,
422 // setting it to the maximal one changes the suffix to PM.
423 // And, for consistency with the native MSW behaviour, we
424 // also do the same thing when changing AM/PM field itself,
425 // so change hours in any case.
426 m_time.SetHour(dir == Dir_Down ? 0 : 23);
427 break;
428
429 case Field_Min:
430 m_time.SetMinute(dir == Dir_Down ? 0 : 59);
431 break;
432
433 case Field_Sec:
434 m_time.SetSecond(dir == Dir_Down ? 0 : 59);
435 break;
436
437 case Field_Max:
438 wxFAIL_MSG( "Invalid field" );
439 }
440
441 UpdateText();
442 }
443
444 // Append the given digit (from 0 to 9) to the current value of the current
445 // field.
446 void AppendDigitToCurrentField(int n)
447 {
448 bool moveToNextField = false;
449
450 if ( !m_isFirstDigit )
451 {
452 // The first digit simply replaces the existing field contents,
453 // but the second one should be combined with the previous one,
454 // otherwise entering 2-digit numbers would be impossible.
455 int currentValue wxDUMMY_INITIALIZE(0),
456 maxValue wxDUMMY_INITIALIZE(0);
457
458 switch ( m_currentField )
459 {
460 case Field_Hour:
461 currentValue = m_time.GetHour();
462 maxValue = 23;
463 break;
464
465 case Field_Min:
466 currentValue = m_time.GetMinute();
467 maxValue = 59;
468 break;
469
470 case Field_Sec:
471 currentValue = m_time.GetSecond();
472 maxValue = 59;
473 break;
474
475 case Field_AMPM:
476 case Field_Max:
477 wxFAIL_MSG( "Invalid field" );
478 }
479
480 // Check if the new value is acceptable. If not, we just handle
481 // this digit as if it were the first one.
482 int newValue = currentValue*10 + n;
483 if ( newValue < maxValue )
484 {
485 n = newValue;
486
487 // If we're not on the seconds field, advance to the next one.
488 // This makes it more convenient to enter times as you can just
489 // press all digits one after one without touching the cursor
490 // arrow keys at all.
491 //
492 // Notice that MSW native control doesn't do this but it seems
493 // so useful that we intentionally diverge from it here.
494 moveToNextField = true;
495
496 // We entered both digits so the next one will be "first" again.
497 m_isFirstDigit = true;
498 }
499 }
500 else // First digit entered.
501 {
502 // The next one won't be first any more.
503 m_isFirstDigit = false;
504 }
505
506 switch ( m_currentField )
507 {
508 case Field_Hour:
509 m_time.SetHour(n);
510 break;
511
512 case Field_Min:
513 m_time.SetMinute(n);
514 break;
515
516 case Field_Sec:
517 m_time.SetSecond(n);
518 break;
519
520 case Field_AMPM:
521 case Field_Max:
522 wxFAIL_MSG( "Invalid field" );
523 }
524
525 if ( moveToNextField && m_currentField < Field_Sec )
526 CycleCurrentField(Dir_Up);
527
528 UpdateText();
529 }
530
531 // Update the text value to correspond to the current time. By default also
532 // generate an event but this can be avoided by calling the "WithoutEvent"
533 // variant.
534 void UpdateText()
535 {
536 UpdateTextWithoutEvent();
537
538 wxWindow* const ctrl = m_text->GetParent();
539
540 wxDateEvent event(ctrl, m_time, wxEVT_TIME_CHANGED);
541 ctrl->HandleWindowEvent(event);
542 }
543
544 void UpdateTextWithoutEvent()
545 {
546 m_text->SetValue(m_time.Format(m_useAMPM ? "%I:%M:%S %p" : "%H:%M:%S"));
547
548 HighlightCurrentField();
549 }
550
551
552 // The current field of the text control: this is the one affected by
553 // pressing arrow keys or spin button.
554 Field m_currentField;
555
556 // Flag indicating whether we use AM/PM indicator or not.
557 bool m_useAMPM;
558
559 // Flag indicating whether the next digit pressed by user will be the first
560 // digit of the current field or the second one. This is necessary because
561 // the first digit replaces the current field contents while the second one
562 // is appended to it (if possible, e.g. pressing '7' in a field already
563 // containing '8' will still replace it as "78" would be invalid).
564 bool m_isFirstDigit;
565
566 wxDECLARE_NO_COPY_CLASS(wxTimePickerGenericImpl);
567 };
568
569 // ============================================================================
570 // wxTimePickerCtrlGeneric implementation
571 // ============================================================================
572
573 // ----------------------------------------------------------------------------
574 // wxTimePickerCtrlGeneric creation
575 // ----------------------------------------------------------------------------
576
577 void wxTimePickerCtrlGeneric::Init()
578 {
579 m_impl = NULL;
580 }
581
582 bool
583 wxTimePickerCtrlGeneric::Create(wxWindow *parent,
584 wxWindowID id,
585 const wxDateTime& date,
586 const wxPoint& pos,
587 const wxSize& size,
588 long style,
589 const wxValidator& validator,
590 const wxString& name)
591 {
592 // The text control we use already has a border, so we don't need one
593 // ourselves.
594 style &= ~wxBORDER_MASK;
595 style |= wxBORDER_NONE;
596
597 if ( !Base::Create(parent, id, pos, size, style, validator, name) )
598 return false;
599
600 m_impl = new wxTimePickerGenericImpl(this);
601 m_impl->SetValue(date);
602
603 InvalidateBestSize();
604 SetInitialSize(size);
605
606 return true;
607 }
608
609 wxTimePickerCtrlGeneric::~wxTimePickerCtrlGeneric()
610 {
611 delete m_impl;
612 }
613
614 wxWindowList wxTimePickerCtrlGeneric::GetCompositeWindowParts() const
615 {
616 wxWindowList parts;
617 if ( m_impl )
618 {
619 parts.push_back(m_impl->m_text);
620 parts.push_back(m_impl->m_btn);
621 }
622 return parts;
623 }
624
625 // ----------------------------------------------------------------------------
626 // wxTimePickerCtrlGeneric value
627 // ----------------------------------------------------------------------------
628
629 void wxTimePickerCtrlGeneric::SetValue(const wxDateTime& date)
630 {
631 wxCHECK_RET( m_impl, "Must create first" );
632
633 m_impl->SetValue(date);
634 }
635
636 wxDateTime wxTimePickerCtrlGeneric::GetValue() const
637 {
638 wxCHECK_MSG( m_impl, wxDateTime(), "Must create first" );
639
640 return m_impl->m_time;
641 }
642
643 // ----------------------------------------------------------------------------
644 // wxTimePickerCtrlGeneric geometry
645 // ----------------------------------------------------------------------------
646
647 void wxTimePickerCtrlGeneric::DoMoveWindow(int x, int y, int width, int height)
648 {
649 Base::DoMoveWindow(x, y, width, height);
650
651 if ( !m_impl )
652 return;
653
654 const int widthBtn = m_impl->m_btn->GetSize().x;
655 const int widthText = width - widthBtn - HMARGIN_TEXT_SPIN;
656
657 m_impl->m_text->SetSize(0, 0, widthText, height);
658 m_impl->m_btn->SetSize(widthText + HMARGIN_TEXT_SPIN, 0, widthBtn, height);
659 }
660
661 wxSize wxTimePickerCtrlGeneric::DoGetBestSize() const
662 {
663 if ( !m_impl )
664 return Base::DoGetBestSize();
665
666 wxSize size = m_impl->m_text->GetBestSize();
667 size.x += m_impl->m_btn->GetBestSize().x + HMARGIN_TEXT_SPIN;
668
669 return size;
670 }
671
672 #endif // !wxHAS_NATIVE_TIMEPICKERCTRL || wxUSE_TIMEPICKCTRL_GENERIC
673
674 #endif // wxUSE_TIMEPICKCTRL