1 #----------------------------------------------------------------------------
5 # Copyright: (c) 2002 by Will Sadkin, 2002
7 # License: wxWindows license
8 #----------------------------------------------------------------------------
10 # This was written way it is because of the lack of masked edit controls
11 # in wxWindows/wxPython. I would also have preferred to derive this
12 # control from a wxSpinCtrl rather than wxTextCtrl, but the wxTextCtrl
13 # component of that control is inaccessible through the interface exposed in
16 # wxTimeCtrl does not use validators, because it does careful manipulation
17 # of the cursor in the text window on each keystroke, and validation is
18 # cursor-position specific, so the control intercepts the key codes before the
19 # validator would fire.
21 # wxTimeCtrl now also supports .SetValue() with either strings or wxDateTime
22 # values, as well as range limits, with the option of either enforcing them
23 # or simply coloring the text of the control if the limits are exceeded.
25 # Note: this class now makes heavy use of wxDateTime for parsing and
26 # regularization, but it always does so with ephemeral instances of
27 # wxDateTime, as the C++/Python validity of these instances seems to not
28 # persist. Because "today" can be a day for which an hour can "not exist"
29 # or be counted twice (1 day each per year, for DST adjustments), the date
30 # portion of all wxDateTimes used/returned have their date portion set to
31 # Jan 1, 1970 (the "epoch.")
36 <B>wxTimeCtrl</B> provides a multi-cell control that allows manipulation of a time
37 value. It supports 12 or 24 hour format, and you can use wxDateTime or mxDateTime
38 to get/set values from the control.
40 Left/right/tab keys to switch cells within a wxTimeCtrl, and the up/down arrows act
41 like a spin control. wxTimeCtrl also allows for an actual spin button to be attached
42 to the control, so that it acts like the up/down arrow keys.
44 The <B>!</B> or <B>c</B> key sets the value of the control to the current time.
46 Here's the API for wxTimeCtrl:
50 <B>value</B> = '12:00:00 AM',
51 pos = wxDefaultPosition,
53 <B>style</B> = wxTE_PROCESS_TAB,
54 <B>validator</B> = wxDefaultValidator,
56 <B>fmt24hr</B> = False,
57 <B>spinButton</B> = None,
60 <B>limited</B> = None,
61 <B>oob_color</B> = "Yellow"
66 <DD>If no initial value is set, the default will be midnight; if an illegal string
67 is specified, a ValueError will result. (You can always later set the initial time
68 with SetValue() after instantiation of the control.)
70 <DD>The size of the control will be automatically adjusted for 12/24 hour format
71 if wxDefaultSize is specified.
73 <DD>By default, wxTimeCtrl will process TAB events, by allowing tab to the
74 different cells within the control.
76 <DD>By default, wxTimeCtrl just uses the default (empty) validator, as all
77 of its validation for entry control is handled internally. However, a validator
78 can be supplied to provide data transfer capability to the control.
81 <DD>If True, control will display time in 24 hour time format; if False, it will
82 use 12 hour AM/PM format. SetValue() will adjust values accordingly for the
83 control, based on the format specified.
86 <DD>If specified, this button's events will be bound to the behavior of the
87 wxTimeCtrl, working like up/down cursor key events. (See BindSpinButton.)
90 <DD>Defines the lower bound for "valid" selections in the control.
91 By default, wxTimeCtrl doesn't have bounds. You must set both upper and lower
92 bounds to make the control pay attention to them, (as only one bound makes no sense
93 with times.) "Valid" times will fall between the min and max "pie wedge" of the
96 <DD>Defines the upper bound for "valid" selections in the control.
97 "Valid" times will fall between the min and max "pie wedge" of the
98 clock. (This can be a "big piece", ie. <b>min = 11pm, max= 10pm</b>
99 means <I>all but the hour from 10:00pm to 11pm are valid times.</I>)
101 <DD>If True, the control will not permit entry of values that fall outside the
105 <DD>Sets the background color used to indicate out-of-bounds values for the control
106 when the control is not limited. This is set to "Yellow" by default.
112 <DT><B>EVT_TIMEUPDATE(win, id, func)</B>
113 <DD>func is fired whenever the value of the control changes.
116 <DT><B>SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta)</B>
117 <DD>Sets the value of the control to a particular time, given a valid
118 value; raises ValueError on invalid value.
119 <EM>NOTE:</EM> This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime
120 was successfully imported by the class module.
122 <DT><B>GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False)</B>
123 <DD>Retrieves the value of the time from the control. By default this is
124 returned as a string, unless one of the other arguments is set; args are
125 searched in the order listed; only one value will be returned.
127 <DT><B>GetWxDateTime(value=None)</B>
128 <DD>When called without arguments, retrieves the value of the control, and applies
129 it to the wxDateTimeFromHMS() constructor, and returns the resulting value.
130 The date portion will always be set to Jan 1, 1970. This form is the same
131 as GetValue(as_wxDateTime=True). GetWxDateTime can also be called with any of the
132 other valid time formats settable with SetValue, to regularize it to a single
133 wxDateTime form. The function will raise ValueError on an unconvertable argument.
135 <DT><B>GetMxDateTime()</B>
136 <DD>Retrieves the value of the control and applies it to the DateTime.Time()
137 constructor,and returns the resulting value. (The date portion will always be
138 set to Jan 1, 1970.) (Same as GetValue(as_wxDateTime=True); provided for backward
139 compatibility with previous release.)
142 <DT><B>BindSpinButton(wxSpinBtton)</B>
143 <DD>Binds an externally created spin button to the control, so that up/down spin
144 events change the active cell or selection in the control (in addition to the
145 up/down cursor keys.) (This is primarily to allow you to create a "standard"
146 interface to time controls, as seen in Windows.)
149 <DT><B>SetMin(min=None)</B>
150 <DD>Sets the expected minimum value, or lower bound, of the control.
151 (The lower bound will only be enforced if the control is
152 configured to limit its values to the set bounds.)
153 If a value of <I>None</I> is provided, then the control will have
154 explicit lower bound. If the value specified is greater than
155 the current lower bound, then the function returns False and the
156 lower bound will not change from its current setting. On success,
157 the function returns True. Even if set, if there is no corresponding
158 upper bound, the control will behave as if it is unbounded.
159 <DT><DD>If successful and the current value is outside the
160 new bounds, if the control is limited the value will be
161 automatically adjusted to the nearest bound; if not limited,
162 the background of the control will be colored with the current
165 <DT><B>GetMin(as_string=False)</B>
166 <DD>Gets the current lower bound value for the control, returning
167 None, if not set, or a wxDateTime, unless the as_string parameter
168 is set to True, at which point it will return the string
169 representation of the lower bound.
172 <DT><B>SetMax(max=None)</B>
173 <DD>Sets the expected maximum value, or upper bound, of the control.
174 (The upper bound will only be enforced if the control is
175 configured to limit its values to the set bounds.)
176 If a value of <I>None</I> is provided, then the control will
177 have no explicit upper bound. If the value specified is less
178 than the current lower bound, then the function returns False and
179 the maximum will not change from its current setting. On success,
180 the function returns True. Even if set, if there is no corresponding
181 lower bound, the control will behave as if it is unbounded.
182 <DT><DD>If successful and the current value is outside the
183 new bounds, if the control is limited the value will be
184 automatically adjusted to the nearest bound; if not limited,
185 the background of the control will be colored with the current
188 <DT><B>GetMax(as_string = False)</B>
189 <DD>Gets the current upper bound value for the control, returning
190 None, if not set, or a wxDateTime, unless the as_string parameter
191 is set to True, at which point it will return the string
192 representation of the lower bound.
196 <DT><B>SetBounds(min=None,max=None)</B>
197 <DD>This function is a convenience function for setting the min and max
198 values at the same time. The function only applies the maximum bound
199 if setting the minimum bound is successful, and returns True
200 only if both operations succeed. <B><I>Note: leaving out an argument
201 will remove the corresponding bound, and result in the behavior of
202 an unbounded control.</I></B>
204 <DT><B>GetBounds(as_string = False)</B>
205 <DD>This function returns a two-tuple (min,max), indicating the
206 current bounds of the control. Each value can be None if
207 that bound is not set. The values will otherwise be wxDateTimes
208 unless the as_string argument is set to True, at which point they
209 will be returned as string representations of the bounds.
212 <DT><B>IsInBounds(value=None)</B>
213 <DD>Returns <I>True</I> if no value is specified and the current value
214 of the control falls within the current bounds. This function can also
215 be called with a value to see if that value would fall within the current
216 bounds of the given control. It will raise ValueError if the value
217 specified is not a wxDateTime, mxDateTime (if available) or parsable string.
220 <DT><B>IsValid(value)</B>
221 <DD>Returns <I>True</I>if specified value is a legal time value and
222 falls within the current bounds of the given control.
225 <DT><B>SetLimited(bool)</B>
226 <DD>If called with a value of True, this function will cause the control
227 to limit the value to fall within the bounds currently specified.
228 (Provided both bounds have been set.)
229 If the control's value currently exceeds the bounds, it will then
230 be set to the nearest bound.
231 If called with a value of False, this function will disable value
232 limiting, but coloring of out-of-bounds values will still take
233 place if bounds have been set for the control.
234 <DT><B>IsLimited()</B>
235 <DD>Returns <I>True</I> if the control is currently limiting the
236 value to fall within the current bounds.
243 from wxPython
.wx
import *
244 from wxPython
.tools
.dbg
import Logger
245 from wxPython
.lib
.maskededit
import wxMaskedTextCtrl
, Field
251 from mx
import DateTime
256 # This class of event fires whenever the value of the time changes in the control:
257 wxEVT_TIMEVAL_UPDATED
= wxNewId()
258 def EVT_TIMEUPDATE(win
, id, func
):
259 """Used to trap events indicating that the current time has been changed."""
260 win
.Connect(id, -1, wxEVT_TIMEVAL_UPDATED
, func
)
262 class TimeUpdatedEvent(wxPyCommandEvent
):
263 def __init__(self
, id, value
='12:00:00 AM'):
264 wxPyCommandEvent
.__init
__(self
, wxEVT_TIMEVAL_UPDATED
, id)
267 """Retrieve the value of the time control at the time this event was generated"""
271 class wxTimeCtrl(wxMaskedTextCtrl
):
273 valid_ctrl_params
= {
274 'display_seconds' : True, # by default, shows seconds
275 'min': None, # by default, no bounds set
277 'limited': False, # by default, no limiting even if bounds set
278 'useFixedWidthFont': True, # by default, use a fixed-width font
279 'oob_color': "Yellow" # by default, the default wxMaskedTextCtrl "invalid" color
283 self
, parent
, id=-1, value
= '12:00:00 AM',
284 pos
= wxDefaultPosition
, size
= wxDefaultSize
,
287 style
= wxTE_PROCESS_TAB
,
288 validator
= wxDefaultValidator
,
292 # set defaults for control:
293 dbg('setting defaults:')
294 for key
, param_value
in wxTimeCtrl
.valid_ctrl_params
.items():
295 # This is done this way to make setattr behave consistently with
296 # "private attribute" name mangling
297 setattr(self
, "_wxTimeCtrl__" + key
, copy
.copy(param_value
))
299 # create locals from current defaults, so we can override if
300 # specified in kwargs, and handle uniformly:
303 limited
= self
.__limited
304 self
.__posCurrent
= 0
307 # (handle positional args (from original release) differently from rest of kwargs:)
308 self
.__fmt
24hr
= fmt24hr
310 maskededit_kwargs
= {}
312 # assign keyword args as appropriate:
313 for key
, param_value
in kwargs
.items():
314 if key
not in wxTimeCtrl
.valid_ctrl_params
.keys():
315 raise AttributeError('invalid keyword argument "%s"' % key
)
317 if key
== "display_seconds":
318 self
.__display
_seconds
= param_value
320 elif key
== "min": min = param_value
321 elif key
== "max": max = param_value
322 elif key
== "limited": limited
= param_value
324 elif key
== "useFixedWidthFont":
325 maskededit_kwargs
[key
] = param_value
326 elif key
== "oob_color":
327 maskededit_kwargs
['invalidBackgroundColor'] = param_value
330 if self
.__display
_seconds
: maskededit_kwargs
['autoformat'] = 'MILTIMEHHMMSS'
331 else: maskededit_kwargs
['autoformat'] = 'MILTIMEHHMM'
333 # Set hour field to zero-pad, right-insert, require explicit field change,
334 # select entire field on entry, and require a resultant valid entry
335 # to allow character entry:
336 hourfield
= Field(formatcodes
='0r<SV', validRegex
='0\d|1\d|2[0123]', validRequired
=True)
338 if self
.__display
_seconds
: maskededit_kwargs
['autoformat'] = 'TIMEHHMMSS'
339 else: maskededit_kwargs
['autoformat'] = 'TIMEHHMM'
341 # Set hour field to allow spaces (at start), right-insert,
342 # require explicit field change, select entire field on entry,
343 # and require a resultant valid entry to allow character entry:
344 hourfield
= Field(formatcodes
='_0<rSV', validRegex
='0[1-9]| [1-9]|1[012]', validRequired
=True)
345 ampmfield
= Field(formatcodes
='S', emptyInvalid
= True, validRequired
= True)
347 # Field 1 is always a zero-padded right-insert minute field,
348 # similarly configured as above:
349 minutefield
= Field(formatcodes
='0r<SV', validRegex
='[0-5]\d', validRequired
=True)
351 fields
= [ hourfield
, minutefield
]
352 if self
.__display
_seconds
:
353 fields
.append(copy
.copy(minutefield
)) # second field has same constraints as field 1
355 if not self
.__fmt
24hr
:
356 fields
.append(ampmfield
)
358 # set fields argument:
359 maskededit_kwargs
['fields'] = fields
361 # This allows range validation if set
362 maskededit_kwargs
['validFunc'] = self
.IsInBounds
364 # This allows range limits to affect insertion into control or not
365 # dynamically without affecting individual field constraint validation
366 maskededit_kwargs
['retainFieldValidation'] = True
368 # allow control over font selection:
369 maskededit_kwargs
['useFixedWidthFont'] = self
.__useFixedWidthFont
371 # allow for explicit size specification:
372 if size
!= wxDefaultSize
:
373 # override (and remove) "autofit" autoformat code in standard time formats:
374 maskededit_kwargs
['formatcodes'] = 'T!'
376 # Now we can initialize the base control:
377 wxMaskedTextCtrl
.__init
__(
381 validator
= validator
,
383 setupEventHandling
= False,
387 # This makes ':' act like tab (after we fix each ':' key event to remove "shift")
388 self
._SetKeyHandler
(':', self
._OnChangeField
)
391 # This makes the up/down keys act like spin button controls:
392 self
._SetKeycodeHandler
(WXK_UP
, self
.__OnSpinUp
)
393 self
._SetKeycodeHandler
(WXK_DOWN
, self
.__OnSpinDown
)
396 # This allows ! and c/C to set the control to the current time:
397 self
._SetKeyHandler
('!', self
.__OnSetToNow
)
398 self
._SetKeyHandler
('c', self
.__OnSetToNow
)
399 self
._SetKeyHandler
('C', self
.__OnSetToNow
)
402 # Set up event handling ourselves, so we can insert special
403 # processing on the ":' key to remove the "shift" attribute
404 # *before* the default handlers have been installed, so
405 # that : takes you forward, not back, and so we can issue
406 # EVT_TIMEUPDATE events on changes:
408 EVT_SET_FOCUS( self
, self
._OnFocus
) ## defeat automatic full selection
409 EVT_KILL_FOCUS( self
, self
._OnKillFocus
) ## run internal validator
410 EVT_LEFT_UP(self
, self
.__LimitSelection
) ## limit selections to single field
411 EVT_LEFT_DCLICK(self
, self
._OnDoubleClick
) ## select field under cursor on dclick
412 EVT_KEY_DOWN( self
, self
._OnKeyDown
) ## capture control events not normally seen, eg ctrl-tab.
413 EVT_CHAR( self
, self
.__OnChar
) ## remove "shift" attribute from colon key event,
414 ## then call wxMaskedTextCtrl._OnChar with
415 ## the possibly modified event.
416 EVT_TEXT( self
, self
.GetId(), self
.__OnTextChange
) ## color control appropriately and EVT_TIMEUPDATE events
419 # Validate initial value and set if appropriate
421 self
.SetBounds(min, max)
422 self
.SetLimited(limited
)
425 self
.SetValue('12:00:00 AM')
428 self
.BindSpinButton(spinButton
) # bind spin button up/down events to this control
432 def BindSpinButton(self
, sb
):
434 This function binds an externally created spin button to the control, so that
435 up/down events from the button automatically change the control.
437 dbg('wxTimeCtrl::BindSpinButton')
438 self
.__spinButton
= sb
439 if self
.__spinButton
:
440 # bind event handlers to spin ctrl
441 EVT_SPIN_UP(self
.__spinButton
, self
.__spinButton
.GetId(), self
.__OnSpinUp
)
442 EVT_SPIN_DOWN(self
.__spinButton
, self
.__spinButton
.GetId(), self
.__OnSpinDown
)
446 return "<wxTimeCtrl: %s>" % self
.GetValue()
449 def SetValue(self
, value
):
451 Validating SetValue function for time values:
452 This function will do dynamic type checking on the value argument,
453 and convert wxDateTime, mxDateTime, or 12/24 format time string
454 into the appropriate format string for the control.
456 dbg('wxTimeCtrl::SetValue(%s)' % repr(value
), indent
=1)
458 strtime
= self
._toGUI
(self
.__validateValue
(value
))
460 dbg('validation failed', indent
=0)
463 dbg('strtime:', strtime
)
464 self
._SetValue
(strtime
)
468 as_wxDateTime
= False,
469 as_mxDateTime
= False,
470 as_wxTimeSpan
= False,
471 as_mxDateTimeDelta
= False):
474 if as_wxDateTime
or as_mxDateTime
or as_wxTimeSpan
or as_mxDateTimeDelta
:
475 value
= self
.GetWxDateTime()
479 value
= DateTime
.DateTime(1970, 1, 1, value
.GetHour(), value
.GetMinute(), value
.GetSecond())
481 value
= wxTimeSpan(value
.GetHour(), value
.GetMinute(), value
.GetSecond())
482 elif as_mxDateTimeDelta
:
483 value
= DateTime
.DateTimeDelta(0, value
.GetHour(), value
.GetMinute(), value
.GetSecond())
485 value
= wxMaskedTextCtrl
.GetValue(self
)
489 def SetWxDateTime(self
, wxdt
):
491 Because SetValue can take a wxDateTime, this is now just an alias.
496 def GetWxDateTime(self
, value
=None):
498 This function is the conversion engine for wxTimeCtrl; it takes
499 one of the following types:
505 and converts it to a wxDateTime that always has Jan 1, 1970 as its date
506 portion, so that range comparisons around values can work using
507 wxDateTime's built-in comparison function. If a value is not
508 provided to convert, the string value of the control will be used.
509 If the value is not one of the accepted types, a ValueError will be
514 dbg('wxTimeCtrl::GetWxDateTime(%s)' % repr(value
), indent
=1)
516 dbg('getting control value')
517 value
= self
.GetValue()
518 dbg('value = "%s"' % value
)
520 if type(value
) == types
.UnicodeType
:
521 value
= str(value
) # convert to regular string
523 valid
= True # assume true
524 if type(value
) == types
.StringType
:
526 # Construct constant wxDateTime, then try to parse the string:
527 wxdt
= wxDateTimeFromDMY(1, 0, 1970)
528 dbg('attempting conversion')
529 value
= value
.strip() # (parser doesn't like leading spaces)
530 checkTime
= wxdt
.ParseTime(value
)
531 valid
= checkTime
== len(value
) # entire string parsed?
532 dbg('checkTime == len(value)?', valid
)
535 dbg(indent
=0, suspend
=0)
536 raise ValueError('cannot convert string "%s" to valid time' % value
)
539 if isinstance(value
, wxDateTime
):
540 hour
, minute
, second
= value
.GetHour(), value
.GetMinute(), value
.GetSecond()
541 elif isinstance(value
, wxTimeSpan
):
542 totalseconds
= value
.GetSeconds()
543 hour
= totalseconds
/ 3600
544 minute
= totalseconds
/ 60 - (hour
* 60)
545 second
= totalseconds
- ((hour
* 3600) + (minute
* 60))
547 elif accept_mx
and isinstance(value
, DateTime
.DateTimeType
):
548 hour
, minute
, second
= value
.hour
, value
.minute
, value
.second
549 elif accept_mx
and isinstance(value
, DateTime
.DateTimeDeltaType
):
550 hour
, minute
, second
= value
.hour
, value
.minute
, value
.second
552 # Not a valid function argument
554 error
= 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value
)
556 error
= 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value
)
557 dbg(indent
=0, suspend
=0)
558 raise ValueError(error
)
560 wxdt
= wxDateTimeFromDMY(1, 0, 1970)
562 wxdt
.SetMinute(minute
)
563 wxdt
.SetSecond(second
)
565 dbg('wxdt:', wxdt
, indent
=0, suspend
=0)
569 def SetMxDateTime(self
, mxdt
):
571 Because SetValue can take an mxDateTime, (if DateTime is importable),
572 this is now just an alias.
577 def GetMxDateTime(self
, value
=None):
579 t
= self
.GetValue(as_mxDateTime
=True)
581 # Convert string 1st to wxDateTime, then use components, since
582 # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM:
583 wxdt
= self
.GetWxDateTime(value
)
584 hour
, minute
, second
= wxdt
.GetHour(), wxdt
.GetMinute(), wxdt
.GetSecond()
585 t
= DateTime
.DateTime(1970,1,1) + DateTimeDelta(0, hour
, minute
, second
)
589 def SetMin(self
, min=None):
591 Sets the minimum value of the control. If a value of None
592 is provided, then the control will have no explicit minimum value.
593 If the value specified is greater than the current maximum value,
594 then the function returns 0 and the minimum will not change from
595 its current setting. On success, the function returns 1.
597 If successful and the current value is lower than the new lower
598 bound, if the control is limited, the value will be automatically
599 adjusted to the new minimum value; if not limited, the value in the
600 control will be colored as invalid.
602 dbg('wxTimeCtrl::SetMin(%s)'% repr(min), indent
=1)
605 min = self
.GetWxDateTime(min)
606 self
.__min
= self
._toGUI
(min)
608 dbg('exception occurred', indent
=0)
613 if self
.IsLimited() and not self
.IsInBounds():
614 self
.SetLimited(self
.__limited
) # force limited value:
618 dbg('ret:', ret
, indent
=0)
622 def GetMin(self
, as_string
= False):
624 Gets the minimum value of the control.
625 If None, it will return None. Otherwise it will return
626 the current minimum bound on the control, as a wxDateTime
627 by default, or as a string if as_string argument is True.
630 dbg('wxTimeCtrl::GetMin, as_string?', as_string
, indent
=1)
631 if self
.__min
is None:
639 ret
= self
.GetWxDateTime(self
.__min
)
642 dbg('exception occurred', indent
=0)
643 dbg('ret:', repr(ret
))
644 dbg(indent
=0, suspend
=0)
648 def SetMax(self
, max=None):
650 Sets the maximum value of the control. If a value of None
651 is provided, then the control will have no explicit maximum value.
652 If the value specified is less than the current minimum value, then
653 the function returns False and the maximum will not change from its
654 current setting. On success, the function returns True.
656 If successful and the current value is greater than the new upper
657 bound, if the control is limited the value will be automatically
658 adjusted to this maximum value; if not limited, the value in the
659 control will be colored as invalid.
661 dbg('wxTimeCtrl::SetMax(%s)' % repr(max), indent
=1)
664 max = self
.GetWxDateTime(max)
665 self
.__max
= self
._toGUI
(max)
667 dbg('exception occurred', indent
=0)
671 dbg('max:', repr(self
.__max
))
672 if self
.IsLimited() and not self
.IsInBounds():
673 self
.SetLimited(self
.__limited
) # force limited value:
677 dbg('ret:', ret
, indent
=0)
681 def GetMax(self
, as_string
= False):
683 Gets the minimum value of the control.
684 If None, it will return None. Otherwise it will return
685 the current minimum bound on the control, as a wxDateTime
686 by default, or as a string if as_string argument is True.
689 dbg('wxTimeCtrl::GetMin, as_string?', as_string
, indent
=1)
690 if self
.__max
is None:
698 ret
= self
.GetWxDateTime(self
.__max
)
701 dbg('exception occurred', indent
=0)
703 dbg('ret:', repr(ret
))
704 dbg(indent
=0, suspend
=0)
708 def SetBounds(self
, min=None, max=None):
710 This function is a convenience function for setting the min and max
711 values at the same time. The function only applies the maximum bound
712 if setting the minimum bound is successful, and returns True
713 only if both operations succeed.
714 NOTE: leaving out an argument will remove the corresponding bound.
716 ret
= self
.SetMin(min)
717 return ret
and self
.SetMax(max)
720 def GetBounds(self
, as_string
= False):
722 This function returns a two-tuple (min,max), indicating the
723 current bounds of the control. Each value can be None if
724 that bound is not set.
726 return (self
.GetMin(as_string
), self
.GetMax(as_string
))
729 def SetLimited(self
, limited
):
731 If called with a value of True, this function will cause the control
732 to limit the value to fall within the bounds currently specified.
733 If the control's value currently exceeds the bounds, it will then
734 be limited accordingly.
736 If called with a value of 0, this function will disable value
737 limiting, but coloring of out-of-bounds values will still take
738 place if bounds have been set for the control.
740 dbg('wxTimeCtrl::SetLimited(%d)' % limited
, indent
=1)
741 self
.__limited
= limited
744 self
.SetMaskParameters(validRequired
= False)
749 dbg('requiring valid value')
750 self
.SetMaskParameters(validRequired
= True)
754 if min is None or max is None:
755 dbg('both bounds not set; no further action taken')
756 return # can't limit without 2 bounds
758 elif not self
.IsInBounds():
759 # set value to the nearest bound:
761 value
= self
.GetWxDateTime()
763 dbg('exception occurred', indent
=0)
766 if min <= max: # valid range doesn't span midnight
768 # which makes the "nearest bound" computation trickier...
770 # determine how long the "invalid" pie wedge is, and cut
771 # this interval in half for comparison purposes:
773 # Note: relies on min and max and value date portions
774 # always being the same.
775 interval
= (min + wxTimeSpan(24, 0, 0, 0)) - max
777 half_interval
= wxTimeSpan(
780 interval
.GetSeconds() / 2, # seconds
783 if value
< min: # min is on next day, so use value on
784 # "next day" for "nearest" interval calculation:
785 cmp_value
= value
+ wxTimeSpan(24, 0, 0, 0)
786 else: # "before midnight; ok
789 if (cmp_value
- max) > half_interval
:
790 dbg('forcing value to min (%s)' % min.FormatTime())
793 dbg('forcing value to max (%s)' % max.FormatTime())
797 # therefore max < value < min guaranteed to be true,
798 # so "nearest bound" calculation is much easier:
799 if (value
- max) >= (min - value
):
800 # current value closer to min; pick that edge of pie wedge
801 dbg('forcing value to min (%s)' % min.FormatTime())
804 dbg('forcing value to max (%s)' % max.FormatTime())
813 Returns True if the control is currently limiting the
814 value to fall within any current bounds. Note: can
815 be set even if there are no current bounds.
817 return self
.__limited
820 def IsInBounds(self
, value
=None):
822 Returns True if no value is specified and the current value
823 of the control falls within the current bounds. As the clock
824 is a "circle", both minimum and maximum bounds must be set for
825 a value to ever be considered "out of bounds". This function can
826 also be called with a value to see if that value would fall within
827 the current bounds of the given control.
829 if value
is not None:
831 value
= self
.GetWxDateTime(value
) # try to regularize passed value
833 dbg('ValueError getting wxDateTime for %s' % repr(value
), indent
=0)
836 dbg('wxTimeCtrl::IsInBounds(%s)' % repr(value
), indent
=1)
837 if self
.__min
is None or self
.__max
is None:
843 value
= self
.GetWxDateTime()
845 dbg('exception occurred', indent
=0)
847 dbg('value:', value
.FormatTime())
849 # Get wxDateTime representations of bounds:
853 midnight
= wxDateTimeFromDMY(1, 0, 1970)
854 if min <= max: # they don't span midnight
855 ret
= min <= value
<= max
858 # have to break into 2 tests; to be in bounds
859 # either "min" <= value (<= midnight of *next day*)
860 # or midnight <= value <= "max"
861 ret
= min <= value
or (midnight
<= value
<= max)
862 dbg('in bounds?', ret
, indent
=0)
866 def IsValid( self
, value
):
868 Can be used to determine if a given value would be a legal and
869 in-bounds value for the control.
872 self
.__validateValue
(value
)
878 #-------------------------------------------------------------------------------------------------------------
879 # these are private functions and overrides:
882 def __OnTextChange(self
, event
=None):
883 dbg('wxTimeCtrl::OnTextChange', indent
=1)
885 # Allow wxMaskedtext base control to color as appropriate,
886 # and Skip the EVT_TEXT event (if appropriate.)
887 ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue()
888 ## call is generating two (2) EVT_TEXT events. (!)
889 ## The the only mechanism I can find to mask this problem is to
890 ## keep track of last value seen, and declare a valid EVT_TEXT
891 ## event iff the value has actually changed. The masked edit
892 ## OnTextChange routine does this, and returns True on a valid event,
894 if not wxMaskedTextCtrl
._OnTextChange
(self
, event
):
897 dbg('firing TimeUpdatedEvent...')
898 evt
= TimeUpdatedEvent(self
.GetId(), self
.GetValue())
899 evt
.SetEventObject(self
)
900 self
.GetEventHandler().ProcessEvent(evt
)
904 def SetInsertionPoint(self
, pos
):
906 Records the specified position and associated cell before calling base class' function.
907 This is necessary to handle the optional spin button, because the insertion
908 point is lost when the focus shifts to the spin button.
910 dbg('wxTimeCtrl::SetInsertionPoint', pos
, indent
=1)
911 wxMaskedTextCtrl
.SetInsertionPoint(self
, pos
) # (causes EVT_TEXT event to fire)
912 self
.__posCurrent
= self
.GetInsertionPoint()
916 def SetSelection(self
, sel_start
, sel_to
):
917 dbg('wxTimeCtrl::SetSelection', sel_start
, sel_to
, indent
=1)
919 # Adjust selection range to legal extent if not already
923 if self
.__posCurrent
!= sel_start
: # force selection and insertion point to match
924 self
.SetInsertionPoint(sel_start
)
925 cell_start
, cell_end
= self
._FindField
(sel_start
)._extent
926 if not cell_start
<= sel_to
<= cell_end
:
929 self
.__bSelection
= sel_start
!= sel_to
930 wxMaskedTextCtrl
.SetSelection(self
, sel_start
, sel_to
)
934 def __OnSpin(self
, key
):
936 This is the function that gets called in response to up/down arrow or
937 bound spin button events.
939 self
.__IncrementValue
(key
, self
.__posCurrent
) # changes the value
941 # Ensure adjusted control regains focus and has adjusted portion
944 start
, end
= self
._FindField
(self
.__posCurrent
)._extent
945 self
.SetInsertionPoint(start
)
946 self
.SetSelection(start
, end
)
947 dbg('current position:', self
.__posCurrent
)
950 def __OnSpinUp(self
, event
):
952 Event handler for any bound spin button on EVT_SPIN_UP;
953 causes control to behave as if up arrow was pressed.
955 dbg('wxTimeCtrl::OnSpinUp', indent
=1)
956 self
.__OnSpin
(WXK_UP
)
957 keep_processing
= False
959 return keep_processing
962 def __OnSpinDown(self
, event
):
964 Event handler for any bound spin button on EVT_SPIN_DOWN;
965 causes control to behave as if down arrow was pressed.
967 dbg('wxTimeCtrl::OnSpinDown', indent
=1)
968 self
.__OnSpin
(WXK_DOWN
)
969 keep_processing
= False
971 return keep_processing
974 def __OnChar(self
, event
):
976 Handler to explicitly look for ':' keyevents, and if found,
977 clear the m_shiftDown field, so it will behave as forward tab.
978 It then calls the base control's _OnChar routine with the modified
981 dbg('wxTimeCtrl::OnChar', indent
=1)
982 keycode
= event
.GetKeyCode()
983 dbg('keycode:', keycode
)
984 if keycode
== ord(':'):
985 dbg('colon seen! removing shift attribute')
986 event
.m_shiftDown
= False
987 wxMaskedTextCtrl
._OnChar
(self
, event
) ## handle each keypress
991 def __OnSetToNow(self
, event
):
993 This is the key handler for '!' and 'c'; this allows the user to
994 quickly set the value of the control to the current time.
996 self
.SetValue(wxDateTime_Now().FormatTime())
997 keep_processing
= False
998 return keep_processing
1001 def __LimitSelection(self
, event
):
1003 Event handler for motion events; this handler
1004 changes limits the selection to the new cell boundaries.
1006 dbg('wxTimeCtrl::LimitSelection', indent
=1)
1007 pos
= self
.GetInsertionPoint()
1008 self
.__posCurrent
= pos
1009 sel_start
, sel_to
= self
.GetSelection()
1010 selection
= sel_start
!= sel_to
1012 # only allow selection to end of current cell:
1013 start
, end
= self
._FindField
(sel_start
)._extent
1014 if sel_to
< pos
: sel_to
= start
1015 elif sel_to
> pos
: sel_to
= end
1017 dbg('new pos =', self
.__posCurrent
, 'select to ', sel_to
)
1018 self
.SetInsertionPoint(self
.__posCurrent
)
1019 self
.SetSelection(self
.__posCurrent
, sel_to
)
1020 if event
: event
.Skip()
1024 def __IncrementValue(self
, key
, pos
):
1025 dbg('wxTimeCtrl::IncrementValue', key
, pos
, indent
=1)
1026 text
= self
.GetValue()
1027 field
= self
._FindField
(pos
)
1028 dbg('field: ', field
._index
)
1029 start
, end
= field
._extent
1030 slice = text
[start
:end
]
1031 if key
== WXK_UP
: increment
= 1
1032 else: increment
= -1
1034 if slice in ('A', 'P'):
1035 if slice == 'A': newslice
= 'P'
1036 elif slice == 'P': newslice
= 'A'
1037 newvalue
= text
[:start
] + newslice
+ text
[end
:]
1039 elif field
._index
== 0:
1040 # adjusting this field is trickier, as its value can affect the
1041 # am/pm setting. So, we use wxDateTime to generate a new value for us:
1042 # (Use a fixed date not subject to DST variations:)
1043 converter
= wxDateTimeFromDMY(1, 0, 1970)
1044 dbg('text: "%s"' % text
)
1045 converter
.ParseTime(text
.strip())
1046 currenthour
= converter
.GetHour()
1047 dbg('current hour:', currenthour
)
1048 newhour
= (currenthour
+ increment
) % 24
1049 dbg('newhour:', newhour
)
1050 converter
.SetHour(newhour
)
1051 dbg('converter.GetHour():', converter
.GetHour())
1052 newvalue
= converter
# take advantage of auto-conversion for am/pm in .SetValue()
1054 else: # minute or second field; handled the same way:
1055 newslice
= "%02d" % ((int(slice) + increment
) % 60)
1056 newvalue
= text
[:start
] + newslice
+ text
[end
:]
1059 self
.SetValue(newvalue
)
1061 except ValueError: # must not be in bounds:
1062 if not wxValidator_IsSilent():
1067 def _toGUI( self
, wxdt
):
1069 This function takes a wxdt as an unambiguous representation of a time, and
1070 converts it to a string appropriate for the format of the control.
1073 if self
.__display
_seconds
: strval
= wxdt
.Format('%H:%M:%S')
1074 else: strval
= wxdt
.Format('%H:%M')
1076 if self
.__display
_seconds
: strval
= wxdt
.Format('%I:%M:%S %p')
1077 else: strval
= wxdt
.Format('%I:%M %p')
1082 def __validateValue( self
, value
):
1084 This function converts the value to a wxDateTime if not already one,
1085 does bounds checking and raises ValueError if argument is
1086 not a valid value for the control as currently specified.
1087 It is used by both the SetValue() and the IsValid() methods.
1089 dbg('wxTimeCtrl::__validateValue(%s)' % repr(value
), indent
=1)
1092 raise ValueError('%s not a valid time value' % repr(value
))
1094 valid
= True # assume true
1096 value
= self
.GetWxDateTime(value
) # regularize form; can generate ValueError if problem doing so
1098 dbg('exception occurred', indent
=0)
1101 if self
.IsLimited() and not self
.IsInBounds(value
):
1104 'value %s is not within the bounds of the control' % str(value
) )
1108 #----------------------------------------------------------------------------
1109 # Test jig for wxTimeCtrl:
1111 if __name__
== '__main__':
1114 class TestPanel(wxPanel
):
1115 def __init__(self
, parent
, id,
1116 pos
= wxPyDefaultPosition
, size
= wxPyDefaultSize
,
1117 fmt24hr
= 0, test_mx
= 0,
1118 style
= wxTAB_TRAVERSAL
):
1120 wxPanel
.__init
__(self
, parent
, id, pos
, size
, style
)
1122 self
.test_mx
= test_mx
1124 self
.tc
= wxTimeCtrl(self
, 10, fmt24hr
= fmt24hr
)
1125 sb
= wxSpinButton( self
, 20, wxDefaultPosition
, wxSize(-1,20), 0 )
1126 self
.tc
.BindSpinButton(sb
)
1128 sizer
= wxBoxSizer( wxHORIZONTAL
)
1129 sizer
.AddWindow( self
.tc
, 0, wxALIGN_CENTRE|wxLEFT|wxTOP|wxBOTTOM
, 5 )
1130 sizer
.AddWindow( sb
, 0, wxALIGN_CENTRE|wxRIGHT|wxTOP|wxBOTTOM
, 5 )
1132 self
.SetAutoLayout( True )
1133 self
.SetSizer( sizer
)
1135 sizer
.SetSizeHints( self
)
1137 EVT_TIMEUPDATE(self
, self
.tc
.GetId(), self
.OnTimeChange
)
1139 def OnTimeChange(self
, event
):
1140 dbg('OnTimeChange: value = ', event
.GetValue())
1141 wxdt
= self
.tc
.GetWxDateTime()
1142 dbg('wxdt =', wxdt
.GetHour(), wxdt
.GetMinute(), wxdt
.GetSecond())
1144 mxdt
= self
.tc
.GetMxDateTime()
1145 dbg('mxdt =', mxdt
.hour
, mxdt
.minute
, mxdt
.second
)
1151 fmt24hr
= '24' in sys
.argv
1152 test_mx
= 'mx' in sys
.argv
1154 frame
= wxFrame(NULL
, -1, "wxTimeCtrl Test", wxPoint(20,20), wxSize(100,100) )
1155 panel
= TestPanel(frame
, -1, wxPoint(-1,-1), fmt24hr
=fmt24hr
, test_mx
= test_mx
)
1158 traceback
.print_exc()
1166 traceback
.print_exc()