]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/timectrl.py
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 # TimeCtrl 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 # TimeCtrl 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.")
32 #----------------------------------------------------------------------------
33 # 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net)
35 # o Updated for V2.5 compatability
36 # o wx.SpinCtl has some issues that cause the control to
37 # lock up. Noted in other places using it too, it's not this module
40 # 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
42 # o wxMaskedTextCtrl -> MaskedTextCtrl
43 # o wxTimeCtrl -> TimeCtrl
48 <B>TimeCtrl</B> provides a multi-cell control that allows manipulation of a time
49 value. It supports 12 or 24 hour format, and you can use wxDateTime or mxDateTime
50 to get/set values from the control.
52 Left/right/tab keys to switch cells within a TimeCtrl, and the up/down arrows act
53 like a spin control. TimeCtrl also allows for an actual spin button to be attached
54 to the control, so that it acts like the up/down arrow keys.
56 The <B>!</B> or <B>c</B> key sets the value of the control to the current time.
58 Here's the API for TimeCtrl:
62 <B>value</B> = '12:00:00 AM',
63 pos = wxDefaultPosition,
65 <B>style</B> = wxTE_PROCESS_TAB,
66 <B>validator</B> = wxDefaultValidator,
68 <B>fmt24hr</B> = False,
69 <B>spinButton</B> = None,
72 <B>limited</B> = None,
73 <B>oob_color</B> = "Yellow"
78 <DD>If no initial value is set, the default will be midnight; if an illegal string
79 is specified, a ValueError will result. (You can always later set the initial time
80 with SetValue() after instantiation of the control.)
82 <DD>The size of the control will be automatically adjusted for 12/24 hour format
83 if wxDefaultSize is specified.
85 <DD>By default, TimeCtrl will process TAB events, by allowing tab to the
86 different cells within the control.
88 <DD>By default, TimeCtrl just uses the default (empty) validator, as all
89 of its validation for entry control is handled internally. However, a validator
90 can be supplied to provide data transfer capability to the control.
93 <DD>If True, control will display time in 24 hour time format; if False, it will
94 use 12 hour AM/PM format. SetValue() will adjust values accordingly for the
95 control, based on the format specified.
98 <DD>If specified, this button's events will be bound to the behavior of the
99 TimeCtrl, working like up/down cursor key events. (See BindSpinButton.)
102 <DD>Defines the lower bound for "valid" selections in the control.
103 By default, TimeCtrl doesn't have bounds. You must set both upper and lower
104 bounds to make the control pay attention to them, (as only one bound makes no sense
105 with times.) "Valid" times will fall between the min and max "pie wedge" of the
108 <DD>Defines the upper bound for "valid" selections in the control.
109 "Valid" times will fall between the min and max "pie wedge" of the
110 clock. (This can be a "big piece", ie. <b>min = 11pm, max= 10pm</b>
111 means <I>all but the hour from 10:00pm to 11pm are valid times.</I>)
113 <DD>If True, the control will not permit entry of values that fall outside the
117 <DD>Sets the background color used to indicate out-of-bounds values for the control
118 when the control is not limited. This is set to "Yellow" by default.
124 <DT><B>EVT_TIMEUPDATE(win, id, func)</B>
125 <DD>func is fired whenever the value of the control changes.
128 <DT><B>SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta)</B>
129 <DD>Sets the value of the control to a particular time, given a valid
130 value; raises ValueError on invalid value.
131 <EM>NOTE:</EM> This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime
132 was successfully imported by the class module.
134 <DT><B>GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False)</B>
135 <DD>Retrieves the value of the time from the control. By default this is
136 returned as a string, unless one of the other arguments is set; args are
137 searched in the order listed; only one value will be returned.
139 <DT><B>GetWxDateTime(value=None)</B>
140 <DD>When called without arguments, retrieves the value of the control, and applies
141 it to the wxDateTimeFromHMS() constructor, and returns the resulting value.
142 The date portion will always be set to Jan 1, 1970. This form is the same
143 as GetValue(as_wxDateTime=True). GetWxDateTime can also be called with any of the
144 other valid time formats settable with SetValue, to regularize it to a single
145 wxDateTime form. The function will raise ValueError on an unconvertable argument.
147 <DT><B>GetMxDateTime()</B>
148 <DD>Retrieves the value of the control and applies it to the DateTime.Time()
149 constructor,and returns the resulting value. (The date portion will always be
150 set to Jan 1, 1970.) (Same as GetValue(as_wxDateTime=True); provided for backward
151 compatibility with previous release.)
154 <DT><B>BindSpinButton(wxSpinBtton)</B>
155 <DD>Binds an externally created spin button to the control, so that up/down spin
156 events change the active cell or selection in the control (in addition to the
157 up/down cursor keys.) (This is primarily to allow you to create a "standard"
158 interface to time controls, as seen in Windows.)
161 <DT><B>SetMin(min=None)</B>
162 <DD>Sets the expected minimum value, or lower bound, of the control.
163 (The lower bound will only be enforced if the control is
164 configured to limit its values to the set bounds.)
165 If a value of <I>None</I> is provided, then the control will have
166 explicit lower bound. If the value specified is greater than
167 the current lower bound, then the function returns False and the
168 lower bound will not change from its current setting. On success,
169 the function returns True. Even if set, if there is no corresponding
170 upper bound, the control will behave as if it is unbounded.
171 <DT><DD>If successful and the current value is outside the
172 new bounds, if the control is limited the value will be
173 automatically adjusted to the nearest bound; if not limited,
174 the background of the control will be colored with the current
177 <DT><B>GetMin(as_string=False)</B>
178 <DD>Gets the current lower bound value for the control, returning
179 None, if not set, or a wxDateTime, unless the as_string parameter
180 is set to True, at which point it will return the string
181 representation of the lower bound.
184 <DT><B>SetMax(max=None)</B>
185 <DD>Sets the expected maximum value, or upper bound, of the control.
186 (The upper bound will only be enforced if the control is
187 configured to limit its values to the set bounds.)
188 If a value of <I>None</I> is provided, then the control will
189 have no explicit upper bound. If the value specified is less
190 than the current lower bound, then the function returns False and
191 the maximum will not change from its current setting. On success,
192 the function returns True. Even if set, if there is no corresponding
193 lower bound, the control will behave as if it is unbounded.
194 <DT><DD>If successful and the current value is outside the
195 new bounds, if the control is limited the value will be
196 automatically adjusted to the nearest bound; if not limited,
197 the background of the control will be colored with the current
200 <DT><B>GetMax(as_string = False)</B>
201 <DD>Gets the current upper bound value for the control, returning
202 None, if not set, or a wxDateTime, unless the as_string parameter
203 is set to True, at which point it will return the string
204 representation of the lower bound.
208 <DT><B>SetBounds(min=None,max=None)</B>
209 <DD>This function is a convenience function for setting the min and max
210 values at the same time. The function only applies the maximum bound
211 if setting the minimum bound is successful, and returns True
212 only if both operations succeed. <B><I>Note: leaving out an argument
213 will remove the corresponding bound, and result in the behavior of
214 an unbounded control.</I></B>
216 <DT><B>GetBounds(as_string = False)</B>
217 <DD>This function returns a two-tuple (min,max), indicating the
218 current bounds of the control. Each value can be None if
219 that bound is not set. The values will otherwise be wxDateTimes
220 unless the as_string argument is set to True, at which point they
221 will be returned as string representations of the bounds.
224 <DT><B>IsInBounds(value=None)</B>
225 <DD>Returns <I>True</I> if no value is specified and the current value
226 of the control falls within the current bounds. This function can also
227 be called with a value to see if that value would fall within the current
228 bounds of the given control. It will raise ValueError if the value
229 specified is not a wxDateTime, mxDateTime (if available) or parsable string.
232 <DT><B>IsValid(value)</B>
233 <DD>Returns <I>True</I>if specified value is a legal time value and
234 falls within the current bounds of the given control.
237 <DT><B>SetLimited(bool)</B>
238 <DD>If called with a value of True, this function will cause the control
239 to limit the value to fall within the bounds currently specified.
240 (Provided both bounds have been set.)
241 If the control's value currently exceeds the bounds, it will then
242 be set to the nearest bound.
243 If called with a value of False, this function will disable value
244 limiting, but coloring of out-of-bounds values will still take
245 place if bounds have been set for the control.
246 <DT><B>IsLimited()</B>
247 <DD>Returns <I>True</I> if the control is currently limiting the
248 value to fall within the current bounds.
260 from wx
.tools
.dbg
import Logger
261 from wx
.lib
.maskededit
import MaskedTextCtrl
, Field
267 from mx
import DateTime
272 # This class of event fires whenever the value of the time changes in the control:
273 wxEVT_TIMEVAL_UPDATED
= wx
.NewEventType()
274 EVT_TIMEUPDATE
= wx
.PyEventBinder(wxEVT_TIMEVAL_UPDATED
, 1)
276 class TimeUpdatedEvent(wx
.PyCommandEvent
):
277 def __init__(self
, id, value
='12:00:00 AM'):
278 wx
.PyCommandEvent
.__init
__(self
, wxEVT_TIMEVAL_UPDATED
, id)
281 """Retrieve the value of the time control at the time this event was generated"""
285 class TimeCtrl(MaskedTextCtrl
):
287 valid_ctrl_params
= {
288 'display_seconds' : True, # by default, shows seconds
289 'min': None, # by default, no bounds set
291 'limited': False, # by default, no limiting even if bounds set
292 'useFixedWidthFont': True, # by default, use a fixed-width font
293 'oob_color': "Yellow" # by default, the default MaskedTextCtrl "invalid" color
297 self
, parent
, id=-1, value
= '12:00:00 AM',
298 pos
= wx
.DefaultPosition
, size
= wx
.DefaultSize
,
301 style
= wx
.TE_PROCESS_TAB
,
302 validator
= wx
.DefaultValidator
,
306 # set defaults for control:
307 dbg('setting defaults:')
308 for key
, param_value
in TimeCtrl
.valid_ctrl_params
.items():
309 # This is done this way to make setattr behave consistently with
310 # "private attribute" name mangling
311 setattr(self
, "_TimeCtrl__" + key
, copy
.copy(param_value
))
313 # create locals from current defaults, so we can override if
314 # specified in kwargs, and handle uniformly:
317 limited
= self
.__limited
318 self
.__posCurrent
= 0
321 # (handle positional args (from original release) differently from rest of kwargs:)
322 self
.__fmt
24hr
= fmt24hr
324 maskededit_kwargs
= {}
326 # assign keyword args as appropriate:
327 for key
, param_value
in kwargs
.items():
328 if key
not in TimeCtrl
.valid_ctrl_params
.keys():
329 raise AttributeError('invalid keyword argument "%s"' % key
)
331 if key
== "display_seconds":
332 self
.__display
_seconds
= param_value
334 elif key
== "min": min = param_value
335 elif key
== "max": max = param_value
336 elif key
== "limited": limited
= param_value
338 elif key
== "useFixedWidthFont":
339 maskededit_kwargs
[key
] = param_value
340 elif key
== "oob_color":
341 maskededit_kwargs
['invalidBackgroundColor'] = param_value
344 if self
.__display
_seconds
: maskededit_kwargs
['autoformat'] = 'MILTIMEHHMMSS'
345 else: maskededit_kwargs
['autoformat'] = 'MILTIMEHHMM'
347 # Set hour field to zero-pad, right-insert, require explicit field change,
348 # select entire field on entry, and require a resultant valid entry
349 # to allow character entry:
350 hourfield
= Field(formatcodes
='0r<SV', validRegex
='0\d|1\d|2[0123]', validRequired
=True)
352 if self
.__display
_seconds
: maskededit_kwargs
['autoformat'] = 'TIMEHHMMSS'
353 else: maskededit_kwargs
['autoformat'] = 'TIMEHHMM'
355 # Set hour field to allow spaces (at start), right-insert,
356 # require explicit field change, select entire field on entry,
357 # and require a resultant valid entry to allow character entry:
358 hourfield
= Field(formatcodes
='_0<rSV', validRegex
='0[1-9]| [1-9]|1[012]', validRequired
=True)
359 ampmfield
= Field(formatcodes
='S', emptyInvalid
= True, validRequired
= True)
361 # Field 1 is always a zero-padded right-insert minute field,
362 # similarly configured as above:
363 minutefield
= Field(formatcodes
='0r<SV', validRegex
='[0-5]\d', validRequired
=True)
365 fields
= [ hourfield
, minutefield
]
366 if self
.__display
_seconds
:
367 fields
.append(copy
.copy(minutefield
)) # second field has same constraints as field 1
369 if not self
.__fmt
24hr
:
370 fields
.append(ampmfield
)
372 # set fields argument:
373 maskededit_kwargs
['fields'] = fields
375 # This allows range validation if set
376 maskededit_kwargs
['validFunc'] = self
.IsInBounds
378 # This allows range limits to affect insertion into control or not
379 # dynamically without affecting individual field constraint validation
380 maskededit_kwargs
['retainFieldValidation'] = True
382 # allow control over font selection:
383 maskededit_kwargs
['useFixedWidthFont'] = self
.__useFixedWidthFont
385 # allow for explicit size specification:
386 if size
!= wx
.DefaultSize
:
387 # override (and remove) "autofit" autoformat code in standard time formats:
388 maskededit_kwargs
['formatcodes'] = 'T!'
390 # Now we can initialize the base control:
391 MaskedTextCtrl
.__init
__(
395 validator
= validator
,
397 setupEventHandling
= False,
401 # This makes ':' act like tab (after we fix each ':' key event to remove "shift")
402 self
._SetKeyHandler
(':', self
._OnChangeField
)
405 # This makes the up/down keys act like spin button controls:
406 self
._SetKeycodeHandler
(wx
.WXK_UP
, self
.__OnSpinUp
)
407 self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
.__OnSpinDown
)
410 # This allows ! and c/C to set the control to the current time:
411 self
._SetKeyHandler
('!', self
.__OnSetToNow
)
412 self
._SetKeyHandler
('c', self
.__OnSetToNow
)
413 self
._SetKeyHandler
('C', self
.__OnSetToNow
)
416 # Set up event handling ourselves, so we can insert special
417 # processing on the ":' key to remove the "shift" attribute
418 # *before* the default handlers have been installed, so
419 # that : takes you forward, not back, and so we can issue
420 # EVT_TIMEUPDATE events on changes:
422 self
.Bind(wx
.EVT_SET_FOCUS
, self
._OnFocus
) ## defeat automatic full selection
423 self
.Bind(wx
.EVT_KILL_FOCUS
, self
._OnKillFocus
) ## run internal validator
424 self
.Bind(wx
.EVT_LEFT_UP
, self
.__LimitSelection
) ## limit selections to single field
425 self
.Bind(wx
.EVT_LEFT_DCLICK
, self
._OnDoubleClick
) ## select field under cursor on dclick
426 self
.Bind(wx
.EVT_KEY_DOWN
, self
._OnKeyDown
) ## capture control events not normally seen, eg ctrl-tab.
427 self
.Bind(wx
.EVT_CHAR
, self
.__OnChar
) ## remove "shift" attribute from colon key event,
428 ## then call MaskedTextCtrl._OnChar with
429 ## the possibly modified event.
430 self
.Bind(wx
.EVT_TEXT
, self
.__OnTextChange
, self
) ## color control appropriately and EVT_TIMEUPDATE events
433 # Validate initial value and set if appropriate
435 self
.SetBounds(min, max)
436 self
.SetLimited(limited
)
439 self
.SetValue('12:00:00 AM')
442 self
.BindSpinButton(spinButton
) # bind spin button up/down events to this control
446 def BindSpinButton(self
, sb
):
448 This function binds an externally created spin button to the control, so that
449 up/down events from the button automatically change the control.
451 dbg('TimeCtrl::BindSpinButton')
452 self
.__spinButton
= sb
453 if self
.__spinButton
:
454 # bind event handlers to spin ctrl
455 self
.__spinButton
.Bind(wx
.EVT_SPIN_UP
, self
.__OnSpinUp
, self
.__spinButton
)
456 self
.__spinButton
.Bind(wx
.EVT_SPIN_DOWN
, self
.__OnSpinDown
, self
.__spinButton
)
460 return "<TimeCtrl: %s>" % self
.GetValue()
463 def SetValue(self
, value
):
465 Validating SetValue function for time values:
466 This function will do dynamic type checking on the value argument,
467 and convert wxDateTime, mxDateTime, or 12/24 format time string
468 into the appropriate format string for the control.
470 dbg('TimeCtrl::SetValue(%s)' % repr(value
), indent
=1)
472 strtime
= self
._toGUI
(self
.__validateValue
(value
))
474 dbg('validation failed', indent
=0)
477 dbg('strtime:', strtime
)
478 self
._SetValue
(strtime
)
482 as_wxDateTime
= False,
483 as_mxDateTime
= False,
484 as_wxTimeSpan
= False,
485 as_mxDateTimeDelta
= False):
488 if as_wxDateTime
or as_mxDateTime
or as_wxTimeSpan
or as_mxDateTimeDelta
:
489 value
= self
.GetWxDateTime()
493 value
= DateTime
.DateTime(1970, 1, 1, value
.GetHour(), value
.GetMinute(), value
.GetSecond())
495 value
= wx
.TimeSpan(value
.GetHour(), value
.GetMinute(), value
.GetSecond())
496 elif as_mxDateTimeDelta
:
497 value
= DateTime
.DateTimeDelta(0, value
.GetHour(), value
.GetMinute(), value
.GetSecond())
499 value
= MaskedTextCtrl
.GetValue(self
)
503 def SetWxDateTime(self
, wxdt
):
505 Because SetValue can take a wxDateTime, this is now just an alias.
510 def GetWxDateTime(self
, value
=None):
512 This function is the conversion engine for TimeCtrl; it takes
513 one of the following types:
519 and converts it to a wxDateTime that always has Jan 1, 1970 as its date
520 portion, so that range comparisons around values can work using
521 wxDateTime's built-in comparison function. If a value is not
522 provided to convert, the string value of the control will be used.
523 If the value is not one of the accepted types, a ValueError will be
528 dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value
), indent
=1)
530 dbg('getting control value')
531 value
= self
.GetValue()
532 dbg('value = "%s"' % value
)
534 if type(value
) == types
.UnicodeType
:
535 value
= str(value
) # convert to regular string
537 valid
= True # assume true
538 if type(value
) == types
.StringType
:
540 # Construct constant wxDateTime, then try to parse the string:
541 wxdt
= wx
.DateTimeFromDMY(1, 0, 1970)
542 dbg('attempting conversion')
543 value
= value
.strip() # (parser doesn't like leading spaces)
544 checkTime
= wxdt
.ParseTime(value
)
545 valid
= checkTime
== len(value
) # entire string parsed?
546 dbg('checkTime == len(value)?', valid
)
549 dbg(indent
=0, suspend
=0)
550 raise ValueError('cannot convert string "%s" to valid time' % value
)
553 if isinstance(value
, wx
.DateTime
):
554 hour
, minute
, second
= value
.GetHour(), value
.GetMinute(), value
.GetSecond()
555 elif isinstance(value
, wx
.TimeSpan
):
556 totalseconds
= value
.GetSeconds()
557 hour
= totalseconds
/ 3600
558 minute
= totalseconds
/ 60 - (hour
* 60)
559 second
= totalseconds
- ((hour
* 3600) + (minute
* 60))
561 elif accept_mx
and isinstance(value
, DateTime
.DateTimeType
):
562 hour
, minute
, second
= value
.hour
, value
.minute
, value
.second
563 elif accept_mx
and isinstance(value
, DateTime
.DateTimeDeltaType
):
564 hour
, minute
, second
= value
.hour
, value
.minute
, value
.second
566 # Not a valid function argument
568 error
= 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value
)
570 error
= 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value
)
571 dbg(indent
=0, suspend
=0)
572 raise ValueError(error
)
574 wxdt
= wx
.DateTimeFromDMY(1, 0, 1970)
576 wxdt
.SetMinute(minute
)
577 wxdt
.SetSecond(second
)
579 dbg('wxdt:', wxdt
, indent
=0, suspend
=0)
583 def SetMxDateTime(self
, mxdt
):
585 Because SetValue can take an mxDateTime, (if DateTime is importable),
586 this is now just an alias.
591 def GetMxDateTime(self
, value
=None):
593 t
= self
.GetValue(as_mxDateTime
=True)
595 # Convert string 1st to wxDateTime, then use components, since
596 # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM:
597 wxdt
= self
.GetWxDateTime(value
)
598 hour
, minute
, second
= wxdt
.GetHour(), wxdt
.GetMinute(), wxdt
.GetSecond()
599 t
= DateTime
.DateTime(1970,1,1) + DateTimeDelta(0, hour
, minute
, second
)
603 def SetMin(self
, min=None):
605 Sets the minimum value of the control. If a value of None
606 is provided, then the control will have no explicit minimum value.
607 If the value specified is greater than the current maximum value,
608 then the function returns 0 and the minimum will not change from
609 its current setting. On success, the function returns 1.
611 If successful and the current value is lower than the new lower
612 bound, if the control is limited, the value will be automatically
613 adjusted to the new minimum value; if not limited, the value in the
614 control will be colored as invalid.
616 dbg('TimeCtrl::SetMin(%s)'% repr(min), indent
=1)
619 min = self
.GetWxDateTime(min)
620 self
.__min
= self
._toGUI
(min)
622 dbg('exception occurred', indent
=0)
627 if self
.IsLimited() and not self
.IsInBounds():
628 self
.SetLimited(self
.__limited
) # force limited value:
632 dbg('ret:', ret
, indent
=0)
636 def GetMin(self
, as_string
= False):
638 Gets the minimum value of the control.
639 If None, it will return None. Otherwise it will return
640 the current minimum bound on the control, as a wxDateTime
641 by default, or as a string if as_string argument is True.
644 dbg('TimeCtrl::GetMin, as_string?', as_string
, indent
=1)
645 if self
.__min
is None:
653 ret
= self
.GetWxDateTime(self
.__min
)
656 dbg('exception occurred', indent
=0)
657 dbg('ret:', repr(ret
))
658 dbg(indent
=0, suspend
=0)
662 def SetMax(self
, max=None):
664 Sets the maximum value of the control. If a value of None
665 is provided, then the control will have no explicit maximum value.
666 If the value specified is less than the current minimum value, then
667 the function returns False and the maximum will not change from its
668 current setting. On success, the function returns True.
670 If successful and the current value is greater than the new upper
671 bound, if the control is limited the value will be automatically
672 adjusted to this maximum value; if not limited, the value in the
673 control will be colored as invalid.
675 dbg('TimeCtrl::SetMax(%s)' % repr(max), indent
=1)
678 max = self
.GetWxDateTime(max)
679 self
.__max
= self
._toGUI
(max)
681 dbg('exception occurred', indent
=0)
685 dbg('max:', repr(self
.__max
))
686 if self
.IsLimited() and not self
.IsInBounds():
687 self
.SetLimited(self
.__limited
) # force limited value:
691 dbg('ret:', ret
, indent
=0)
695 def GetMax(self
, as_string
= False):
697 Gets the minimum value of the control.
698 If None, it will return None. Otherwise it will return
699 the current minimum bound on the control, as a wxDateTime
700 by default, or as a string if as_string argument is True.
703 dbg('TimeCtrl::GetMin, as_string?', as_string
, indent
=1)
704 if self
.__max
is None:
712 ret
= self
.GetWxDateTime(self
.__max
)
715 dbg('exception occurred', indent
=0)
717 dbg('ret:', repr(ret
))
718 dbg(indent
=0, suspend
=0)
722 def SetBounds(self
, min=None, max=None):
724 This function is a convenience function for setting the min and max
725 values at the same time. The function only applies the maximum bound
726 if setting the minimum bound is successful, and returns True
727 only if both operations succeed.
728 NOTE: leaving out an argument will remove the corresponding bound.
730 ret
= self
.SetMin(min)
731 return ret
and self
.SetMax(max)
734 def GetBounds(self
, as_string
= False):
736 This function returns a two-tuple (min,max), indicating the
737 current bounds of the control. Each value can be None if
738 that bound is not set.
740 return (self
.GetMin(as_string
), self
.GetMax(as_string
))
743 def SetLimited(self
, limited
):
745 If called with a value of True, this function will cause the control
746 to limit the value to fall within the bounds currently specified.
747 If the control's value currently exceeds the bounds, it will then
748 be limited accordingly.
750 If called with a value of 0, this function will disable value
751 limiting, but coloring of out-of-bounds values will still take
752 place if bounds have been set for the control.
754 dbg('TimeCtrl::SetLimited(%d)' % limited
, indent
=1)
755 self
.__limited
= limited
758 self
.SetMaskParameters(validRequired
= False)
763 dbg('requiring valid value')
764 self
.SetMaskParameters(validRequired
= True)
768 if min is None or max is None:
769 dbg('both bounds not set; no further action taken')
770 return # can't limit without 2 bounds
772 elif not self
.IsInBounds():
773 # set value to the nearest bound:
775 value
= self
.GetWxDateTime()
777 dbg('exception occurred', indent
=0)
780 if min <= max: # valid range doesn't span midnight
782 # which makes the "nearest bound" computation trickier...
784 # determine how long the "invalid" pie wedge is, and cut
785 # this interval in half for comparison purposes:
787 # Note: relies on min and max and value date portions
788 # always being the same.
789 interval
= (min + wx
.TimeSpan(24, 0, 0, 0)) - max
791 half_interval
= wx
.TimeSpan(
794 interval
.GetSeconds() / 2, # seconds
797 if value
< min: # min is on next day, so use value on
798 # "next day" for "nearest" interval calculation:
799 cmp_value
= value
+ wx
.TimeSpan(24, 0, 0, 0)
800 else: # "before midnight; ok
803 if (cmp_value
- max) > half_interval
:
804 dbg('forcing value to min (%s)' % min.FormatTime())
807 dbg('forcing value to max (%s)' % max.FormatTime())
811 # therefore max < value < min guaranteed to be true,
812 # so "nearest bound" calculation is much easier:
813 if (value
- max) >= (min - value
):
814 # current value closer to min; pick that edge of pie wedge
815 dbg('forcing value to min (%s)' % min.FormatTime())
818 dbg('forcing value to max (%s)' % max.FormatTime())
827 Returns True if the control is currently limiting the
828 value to fall within any current bounds. Note: can
829 be set even if there are no current bounds.
831 return self
.__limited
834 def IsInBounds(self
, value
=None):
836 Returns True if no value is specified and the current value
837 of the control falls within the current bounds. As the clock
838 is a "circle", both minimum and maximum bounds must be set for
839 a value to ever be considered "out of bounds". This function can
840 also be called with a value to see if that value would fall within
841 the current bounds of the given control.
843 if value
is not None:
845 value
= self
.GetWxDateTime(value
) # try to regularize passed value
847 dbg('ValueError getting wxDateTime for %s' % repr(value
), indent
=0)
850 dbg('TimeCtrl::IsInBounds(%s)' % repr(value
), indent
=1)
851 if self
.__min
is None or self
.__max
is None:
857 value
= self
.GetWxDateTime()
859 dbg('exception occurred', indent
=0)
861 dbg('value:', value
.FormatTime())
863 # Get wxDateTime representations of bounds:
867 midnight
= wx
.DateTimeFromDMY(1, 0, 1970)
868 if min <= max: # they don't span midnight
869 ret
= min <= value
<= max
872 # have to break into 2 tests; to be in bounds
873 # either "min" <= value (<= midnight of *next day*)
874 # or midnight <= value <= "max"
875 ret
= min <= value
or (midnight
<= value
<= max)
876 dbg('in bounds?', ret
, indent
=0)
880 def IsValid( self
, value
):
882 Can be used to determine if a given value would be a legal and
883 in-bounds value for the control.
886 self
.__validateValue
(value
)
892 #-------------------------------------------------------------------------------------------------------------
893 # these are private functions and overrides:
896 def __OnTextChange(self
, event
=None):
897 dbg('TimeCtrl::OnTextChange', indent
=1)
899 # Allow wxMaskedtext base control to color as appropriate,
900 # and Skip the EVT_TEXT event (if appropriate.)
901 ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue()
902 ## call is generating two (2) EVT_TEXT events. (!)
903 ## The the only mechanism I can find to mask this problem is to
904 ## keep track of last value seen, and declare a valid EVT_TEXT
905 ## event iff the value has actually changed. The masked edit
906 ## OnTextChange routine does this, and returns True on a valid event,
908 if not MaskedTextCtrl
._OnTextChange
(self
, event
):
911 dbg('firing TimeUpdatedEvent...')
912 evt
= TimeUpdatedEvent(self
.GetId(), self
.GetValue())
913 evt
.SetEventObject(self
)
914 self
.GetEventHandler().ProcessEvent(evt
)
918 def SetInsertionPoint(self
, pos
):
920 Records the specified position and associated cell before calling base class' function.
921 This is necessary to handle the optional spin button, because the insertion
922 point is lost when the focus shifts to the spin button.
924 dbg('TimeCtrl::SetInsertionPoint', pos
, indent
=1)
925 MaskedTextCtrl
.SetInsertionPoint(self
, pos
) # (causes EVT_TEXT event to fire)
926 self
.__posCurrent
= self
.GetInsertionPoint()
930 def SetSelection(self
, sel_start
, sel_to
):
931 dbg('TimeCtrl::SetSelection', sel_start
, sel_to
, indent
=1)
933 # Adjust selection range to legal extent if not already
937 if self
.__posCurrent
!= sel_start
: # force selection and insertion point to match
938 self
.SetInsertionPoint(sel_start
)
939 cell_start
, cell_end
= self
._FindField
(sel_start
)._extent
940 if not cell_start
<= sel_to
<= cell_end
:
943 self
.__bSelection
= sel_start
!= sel_to
944 MaskedTextCtrl
.SetSelection(self
, sel_start
, sel_to
)
948 def __OnSpin(self
, key
):
950 This is the function that gets called in response to up/down arrow or
951 bound spin button events.
953 self
.__IncrementValue
(key
, self
.__posCurrent
) # changes the value
955 # Ensure adjusted control regains focus and has adjusted portion
958 start
, end
= self
._FindField
(self
.__posCurrent
)._extent
959 self
.SetInsertionPoint(start
)
960 self
.SetSelection(start
, end
)
961 dbg('current position:', self
.__posCurrent
)
964 def __OnSpinUp(self
, event
):
966 Event handler for any bound spin button on EVT_SPIN_UP;
967 causes control to behave as if up arrow was pressed.
969 dbg('TimeCtrl::OnSpinUp', indent
=1)
970 self
.__OnSpin
(WXK_UP
)
971 keep_processing
= False
973 return keep_processing
976 def __OnSpinDown(self
, event
):
978 Event handler for any bound spin button on EVT_SPIN_DOWN;
979 causes control to behave as if down arrow was pressed.
981 dbg('TimeCtrl::OnSpinDown', indent
=1)
982 self
.__OnSpin
(WXK_DOWN
)
983 keep_processing
= False
985 return keep_processing
988 def __OnChar(self
, event
):
990 Handler to explicitly look for ':' keyevents, and if found,
991 clear the m_shiftDown field, so it will behave as forward tab.
992 It then calls the base control's _OnChar routine with the modified
995 dbg('TimeCtrl::OnChar', indent
=1)
996 keycode
= event
.GetKeyCode()
997 dbg('keycode:', keycode
)
998 if keycode
== ord(':'):
999 dbg('colon seen! removing shift attribute')
1000 event
.m_shiftDown
= False
1001 MaskedTextCtrl
._OnChar
(self
, event
) ## handle each keypress
1005 def __OnSetToNow(self
, event
):
1007 This is the key handler for '!' and 'c'; this allows the user to
1008 quickly set the value of the control to the current time.
1010 self
.SetValue(wx
.DateTime_Now().FormatTime())
1011 keep_processing
= False
1012 return keep_processing
1015 def __LimitSelection(self
, event
):
1017 Event handler for motion events; this handler
1018 changes limits the selection to the new cell boundaries.
1020 dbg('TimeCtrl::LimitSelection', indent
=1)
1021 pos
= self
.GetInsertionPoint()
1022 self
.__posCurrent
= pos
1023 sel_start
, sel_to
= self
.GetSelection()
1024 selection
= sel_start
!= sel_to
1026 # only allow selection to end of current cell:
1027 start
, end
= self
._FindField
(sel_start
)._extent
1028 if sel_to
< pos
: sel_to
= start
1029 elif sel_to
> pos
: sel_to
= end
1031 dbg('new pos =', self
.__posCurrent
, 'select to ', sel_to
)
1032 self
.SetInsertionPoint(self
.__posCurrent
)
1033 self
.SetSelection(self
.__posCurrent
, sel_to
)
1034 if event
: event
.Skip()
1038 def __IncrementValue(self
, key
, pos
):
1039 dbg('TimeCtrl::IncrementValue', key
, pos
, indent
=1)
1040 text
= self
.GetValue()
1041 field
= self
._FindField
(pos
)
1042 dbg('field: ', field
._index
)
1043 start
, end
= field
._extent
1044 slice = text
[start
:end
]
1045 if key
== wx
.WXK_UP
: increment
= 1
1046 else: increment
= -1
1048 if slice in ('A', 'P'):
1049 if slice == 'A': newslice
= 'P'
1050 elif slice == 'P': newslice
= 'A'
1051 newvalue
= text
[:start
] + newslice
+ text
[end
:]
1053 elif field
._index
== 0:
1054 # adjusting this field is trickier, as its value can affect the
1055 # am/pm setting. So, we use wxDateTime to generate a new value for us:
1056 # (Use a fixed date not subject to DST variations:)
1057 converter
= wx
.DateTimeFromDMY(1, 0, 1970)
1058 dbg('text: "%s"' % text
)
1059 converter
.ParseTime(text
.strip())
1060 currenthour
= converter
.GetHour()
1061 dbg('current hour:', currenthour
)
1062 newhour
= (currenthour
+ increment
) % 24
1063 dbg('newhour:', newhour
)
1064 converter
.SetHour(newhour
)
1065 dbg('converter.GetHour():', converter
.GetHour())
1066 newvalue
= converter
# take advantage of auto-conversion for am/pm in .SetValue()
1068 else: # minute or second field; handled the same way:
1069 newslice
= "%02d" % ((int(slice) + increment
) % 60)
1070 newvalue
= text
[:start
] + newslice
+ text
[end
:]
1073 self
.SetValue(newvalue
)
1075 except ValueError: # must not be in bounds:
1076 if not wx
.Validator_IsSilent():
1081 def _toGUI( self
, wxdt
):
1083 This function takes a wxdt as an unambiguous representation of a time, and
1084 converts it to a string appropriate for the format of the control.
1087 if self
.__display
_seconds
: strval
= wxdt
.Format('%H:%M:%S')
1088 else: strval
= wxdt
.Format('%H:%M')
1090 if self
.__display
_seconds
: strval
= wxdt
.Format('%I:%M:%S %p')
1091 else: strval
= wxdt
.Format('%I:%M %p')
1096 def __validateValue( self
, value
):
1098 This function converts the value to a wxDateTime if not already one,
1099 does bounds checking and raises ValueError if argument is
1100 not a valid value for the control as currently specified.
1101 It is used by both the SetValue() and the IsValid() methods.
1103 dbg('TimeCtrl::__validateValue(%s)' % repr(value
), indent
=1)
1106 raise ValueError('%s not a valid time value' % repr(value
))
1108 valid
= True # assume true
1110 value
= self
.GetWxDateTime(value
) # regularize form; can generate ValueError if problem doing so
1112 dbg('exception occurred', indent
=0)
1115 if self
.IsLimited() and not self
.IsInBounds(value
):
1118 'value %s is not within the bounds of the control' % str(value
) )
1122 #----------------------------------------------------------------------------
1123 # Test jig for TimeCtrl:
1125 if __name__
== '__main__':
1128 class TestPanel(wx
.Panel
):
1129 def __init__(self
, parent
, id,
1130 pos
= wx
.DefaultPosition
, size
= wx
.DefaultSize
,
1131 fmt24hr
= 0, test_mx
= 0,
1132 style
= wx
.TAB_TRAVERSAL
):
1134 wx
.Panel
.__init
__(self
, parent
, id, pos
, size
, style
)
1136 self
.test_mx
= test_mx
1138 self
.tc
= TimeCtrl(self
, 10, fmt24hr
= fmt24hr
)
1139 sb
= wx
.SpinButton( self
, 20, wx
.DefaultPosition
, (-1,20), 0 )
1140 self
.tc
.BindSpinButton(sb
)
1142 sizer
= wx
.BoxSizer( wx
.HORIZONTAL
)
1143 sizer
.Add( self
.tc
, 0, wx
.ALIGN_CENTRE|wx
.LEFT|wx
.TOP|wx
.BOTTOM
, 5 )
1144 sizer
.Add( sb
, 0, wx
.ALIGN_CENTRE|wx
.RIGHT|wx
.TOP|wx
.BOTTOM
, 5 )
1146 self
.SetAutoLayout( True )
1147 self
.SetSizer( sizer
)
1149 sizer
.SetSizeHints( self
)
1151 self
.Bind(EVT_TIMEUPDATE
, self
.OnTimeChange
, self
.tc
)
1153 def OnTimeChange(self
, event
):
1154 dbg('OnTimeChange: value = ', event
.GetValue())
1155 wxdt
= self
.tc
.GetWxDateTime()
1156 dbg('wxdt =', wxdt
.GetHour(), wxdt
.GetMinute(), wxdt
.GetSecond())
1158 mxdt
= self
.tc
.GetMxDateTime()
1159 dbg('mxdt =', mxdt
.hour
, mxdt
.minute
, mxdt
.second
)
1162 class MyApp(wx
.App
):
1165 fmt24hr
= '24' in sys
.argv
1166 test_mx
= 'mx' in sys
.argv
1168 frame
= wx
.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) )
1169 panel
= TestPanel(frame
, -1, (-1,-1), fmt24hr
=fmt24hr
, test_mx
= test_mx
)
1172 traceback
.print_exc()
1180 traceback
.print_exc()