]> git.saurik.com Git - wxWidgets.git/blame - src/generic/timectrlg.cpp
Fix for scroll position being changed when partial layout is done
[wxWidgets.git] / src / generic / timectrlg.cpp
CommitLineData
569c7d8c
VZ
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
53enum
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
63class wxTimePickerGenericImpl : public wxEvtHandler
64{
65public:
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
e941b45e
VZ
75 m_btn = new wxSpinButton(ctrl, wxID_ANY,
76 wxDefaultPosition, wxDefaultSize,
77 wxSP_VERTICAL | wxSP_WRAP);
569c7d8c
VZ
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
b05875c9
VZ
134 // Ensure that the date part doesn't correspond to a DST change date as
135 // time is discontinuous then resulting in many problems, e.g. it's
136 // impossible to even enter 2:00:00 at the beginning of summer time
137 // date as this time doesn't exist. By using Jan 1, on which nobody
138 // changes DST, we avoid all such problems.
139 wxDateTime::Tm tm = m_time.GetTm();
140 tm.mday =
141 tm.yday = 1;
142 tm.mon = wxDateTime::Jan;
143 m_time.Set(tm);
144
569c7d8c
VZ
145 UpdateTextWithoutEvent();
146 }
147
148
149 // The text part of the control.
150 wxTextCtrl* m_text;
151
152 // The spin button used to change the text fields.
153 wxSpinButton* m_btn;
154
155 // The current time (date part is ignored).
156 wxDateTime m_time;
157
158private:
159 // The logical fields of the text control (AM/PM one may not be present).
160 enum Field
161 {
162 Field_Hour,
163 Field_Min,
164 Field_Sec,
165 Field_AMPM,
166 Field_Max
167 };
168
169 // Direction of change of time fields.
170 enum Direction
171 {
172 // Notice that the enum elements values matter.
173 Dir_Down = -1,
174 Dir_Up = +1
175 };
176
177 // A range of character positions ("from" is inclusive, "to" -- exclusive).
178 struct CharRange
179 {
180 int from,
181 to;
182 };
183
184 // Event handlers for various events in our controls.
185 void OnTextSetFocus(wxFocusEvent& event)
186 {
187 HighlightCurrentField();
188
189 event.Skip();
190 }
191
192 // Keyboard interface here is modelled over MSW native control and may need
193 // adjustments for other platforms.
194 void OnTextKeyDown(wxKeyEvent& event)
195 {
196 const int key = event.GetKeyCode();
197
198 switch ( key )
199 {
200 case WXK_DOWN:
201 ChangeCurrentFieldBy1(Dir_Down);
202 break;
203
204 case WXK_UP:
205 ChangeCurrentFieldBy1(Dir_Up);
206 break;
207
208 case WXK_LEFT:
209 CycleCurrentField(Dir_Down);
210 break;
211
212 case WXK_RIGHT:
213 CycleCurrentField(Dir_Up);
214 break;
215
216 case WXK_HOME:
217 ResetCurrentField(Dir_Down);
218 break;
219
220 case WXK_END:
221 ResetCurrentField(Dir_Up);
222 break;
223
224 case '0':
225 case '1':
226 case '2':
227 case '3':
228 case '4':
229 case '5':
230 case '6':
231 case '7':
232 case '8':
233 case '9':
234 // The digits work in all keys except AM/PM.
235 if ( m_currentField != Field_AMPM )
236 {
237 AppendDigitToCurrentField(key - '0');
238 }
239 break;
240
241 case 'A':
242 case 'P':
243 // These keys only work to toggle AM/PM field.
244 if ( m_currentField == Field_AMPM )
245 {
246 unsigned hour = m_time.GetHour();
247 if ( key == 'A' )
248 {
249 if ( hour >= 12 )
250 hour -= 12;
251 }
252 else // PM
253 {
254 if ( hour < 12 )
255 hour += 12;
256 }
257
258 if ( hour != m_time.GetHour() )
259 {
260 m_time.SetHour(hour);
261 UpdateText();
262 }
263 }
264 break;
265
266 // Do not skip the other events, just consume them to prevent the
267 // user from editing the text directly.
268 }
269 }
270
271 void OnTextClick(wxMouseEvent& event)
272 {
273 Field field wxDUMMY_INITIALIZE(Field_Max);
274 long pos;
275 switch ( m_text->HitTest(event.GetPosition(), &pos) )
276 {
277 case wxTE_HT_UNKNOWN:
278 // Don't do anything, it's better than doing something wrong.
279 return;
280
281 case wxTE_HT_BEFORE:
282 // Select the first field.
283 field = Field_Hour;
284 break;
285
286 case wxTE_HT_ON_TEXT:
287 // Find the field containing this position.
288 for ( field = Field_Hour; field <= GetLastField(); )
289 {
290 const CharRange range = GetFieldRange(field);
291
292 // Normally the "to" end is exclusive but we want to give
293 // focus to some field when the user clicks between them so
294 // count it as part of the preceding field here.
295 if ( range.from <= pos && pos <= range.to )
296 break;
297
298 field = static_cast<Field>(field + 1);
299 }
300 break;
301
302 case wxTE_HT_BELOW:
303 // This shouldn't happen for single line control.
304 wxFAIL_MSG( "Unreachable" );
305 // fall through
306
307 case wxTE_HT_BEYOND:
308 // Select the last field.
309 field = GetLastField();
310 break;
311 }
312
313 ChangeCurrentField(field);
569c7d8c
VZ
314 }
315
316 void OnArrowUp(wxSpinEvent& WXUNUSED(event))
317 {
318 ChangeCurrentFieldBy1(Dir_Up);
569c7d8c
VZ
319 }
320
321 void OnArrowDown(wxSpinEvent& WXUNUSED(event))
322 {
323 ChangeCurrentFieldBy1(Dir_Down);
569c7d8c
VZ
324 }
325
326
327 // Get the range of the given field in character positions ("from" is
328 // inclusive, "to" exclusive).
329 static CharRange GetFieldRange(Field field)
330 {
331 // Currently we can just hard code the ranges as they are the same for
332 // both supported formats, if we want to support arbitrary formats in
333 // the future, we'd need to determine them dynamically by examining the
334 // format here.
335 static const CharRange ranges[] =
336 {
337 { 0, 2 },
338 { 3, 5 },
339 { 6, 8 },
340 { 9, 11},
341 };
342
343 wxCOMPILE_TIME_ASSERT( WXSIZEOF(ranges) == Field_Max,
344 FieldRangesMismatch );
345
346 return ranges[field];
347 }
348
349 // Get the last field used depending on m_useAMPM.
350 Field GetLastField() const
351 {
352 return m_useAMPM ? Field_AMPM : Field_Sec;
353 }
354
355 // Change the current field. For convenience, accept int field here as this
356 // allows us to use arithmetic operations in the caller.
357 void ChangeCurrentField(int field)
358 {
359 if ( field == m_currentField )
360 return;
361
362 wxCHECK_RET( field <= GetLastField(), "Invalid field" );
363
364 m_currentField = static_cast<Field>(field);
365 m_isFirstDigit = true;
366
367 HighlightCurrentField();
368 }
369
370 // Go to the next (Dir_Up) or previous (Dir_Down) field, wrapping if
371 // necessary.
372 void CycleCurrentField(Direction dir)
373 {
374 const unsigned numFields = GetLastField() + 1;
375
376 ChangeCurrentField((m_currentField + numFields + dir) % numFields);
377 }
378
379 // Select the currently actively field.
380 void HighlightCurrentField()
381 {
f58f412b
VZ
382 m_text->SetFocus();
383
569c7d8c
VZ
384 const CharRange range = GetFieldRange(m_currentField);
385
386 m_text->SetSelection(range.from, range.to);
387 }
388
389 // Decrement or increment the value of the current field (wrapping if
390 // necessary).
391 void ChangeCurrentFieldBy1(Direction dir)
392 {
393 switch ( m_currentField )
394 {
395 case Field_Hour:
396 m_time.SetHour((m_time.GetHour() + 24 + dir) % 24);
397 break;
398
399 case Field_Min:
400 m_time.SetMinute((m_time.GetMinute() + 60 + dir) % 60);
401 break;
402
403 case Field_Sec:
404 m_time.SetSecond((m_time.GetSecond() + 60 + dir) % 60);
405 break;
406
407 case Field_AMPM:
408 m_time.SetHour((m_time.GetHour() + 12) % 24);
409 break;
410
411 case Field_Max:
412 wxFAIL_MSG( "Invalid field" );
413 }
414
415 UpdateText();
416 }
417
418 // Set the current field to its minimal or maximal value.
419 void ResetCurrentField(Direction dir)
420 {
421 switch ( m_currentField )
422 {
423 case Field_Hour:
424 case Field_AMPM:
425 // In 12-hour mode setting the hour to the minimal value
426 // also changes the suffix to AM and, correspondingly,
427 // setting it to the maximal one changes the suffix to PM.
428 // And, for consistency with the native MSW behaviour, we
429 // also do the same thing when changing AM/PM field itself,
430 // so change hours in any case.
431 m_time.SetHour(dir == Dir_Down ? 0 : 23);
432 break;
433
434 case Field_Min:
435 m_time.SetMinute(dir == Dir_Down ? 0 : 59);
436 break;
437
438 case Field_Sec:
439 m_time.SetSecond(dir == Dir_Down ? 0 : 59);
440 break;
441
442 case Field_Max:
443 wxFAIL_MSG( "Invalid field" );
444 }
445
446 UpdateText();
447 }
448
449 // Append the given digit (from 0 to 9) to the current value of the current
450 // field.
451 void AppendDigitToCurrentField(int n)
452 {
453 bool moveToNextField = false;
454
455 if ( !m_isFirstDigit )
456 {
457 // The first digit simply replaces the existing field contents,
458 // but the second one should be combined with the previous one,
459 // otherwise entering 2-digit numbers would be impossible.
460 int currentValue wxDUMMY_INITIALIZE(0),
461 maxValue wxDUMMY_INITIALIZE(0);
462
463 switch ( m_currentField )
464 {
465 case Field_Hour:
466 currentValue = m_time.GetHour();
467 maxValue = 23;
468 break;
469
470 case Field_Min:
471 currentValue = m_time.GetMinute();
472 maxValue = 59;
473 break;
474
475 case Field_Sec:
476 currentValue = m_time.GetSecond();
477 maxValue = 59;
478 break;
479
480 case Field_AMPM:
481 case Field_Max:
482 wxFAIL_MSG( "Invalid field" );
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
582void wxTimePickerCtrlGeneric::Init()
583{
584 m_impl = NULL;
585}
586
587bool
588wxTimePickerCtrlGeneric::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
614wxTimePickerCtrlGeneric::~wxTimePickerCtrlGeneric()
615{
616 delete m_impl;
617}
618
619wxWindowList 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
634void wxTimePickerCtrlGeneric::SetValue(const wxDateTime& date)
635{
636 wxCHECK_RET( m_impl, "Must create first" );
637
638 m_impl->SetValue(date);
639}
640
641wxDateTime 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
652void 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
666wxSize 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