]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/masked/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 -> masked.TextCtrl
43 # o wxTimeCtrl -> masked.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> = '00:00:00',
63 pos = wx.DefaultPosition,
64 size = wx.DefaultSize,
65 <B>style</B> = wxTE_PROCESS_TAB,
66 <B>validator</B> = wx.DefaultValidator,
68 <B>format</B> = 'HHMMSS',
69 <B>fmt24hr</B> = False,
70 <B>displaySeconds</B> = True,
71 <B>spinButton</B> = None,
74 <B>limited</B> = None,
75 <B>oob_color</B> = "Yellow"
80 <DD>If no initial value is set, the default will be midnight; if an illegal string
81 is specified, a ValueError will result. (You can always later set the initial time
82 with SetValue() after instantiation of the control.)
84 <DD>The size of the control will be automatically adjusted for 12/24 hour format
85 if wx.DefaultSize is specified. NOTE: due to a problem with wx.DateTime, if the
86 locale does not use 'AM/PM' for its values, the default format will automatically
87 change to 24 hour format, and an AttributeError will be thrown if a non-24 format
90 <DD>By default, TimeCtrl will process TAB events, by allowing tab to the
91 different cells within the control.
93 <DD>By default, TimeCtrl just uses the default (empty) validator, as all
94 of its validation for entry control is handled internally. However, a validator
95 can be supplied to provide data transfer capability to the control.
98 <DD>This parameter can be used instead of the fmt24hr and displaySeconds
99 parameters, respectively; it provides a shorthand way to specify the time
100 format you want. Accepted values are 'HHMMSS', 'HHMM', '24HHMMSS', and
101 '24HHMM'. If the format is specified, the other two arguments will be ignored.
104 <DD>If True, control will display time in 24 hour time format; if False, it will
105 use 12 hour AM/PM format. SetValue() will adjust values accordingly for the
106 control, based on the format specified. (This value is ignored if the <i>format</i>
107 parameter is specified.)
109 <DT><B>displaySeconds</B>
110 <DD>If True, control will include a seconds field; if False, it will
111 just show hours and minutes. (This value is ignored if the <i>format</i>
112 parameter is specified.)
114 <DT><B>spinButton</B>
115 <DD>If specified, this button's events will be bound to the behavior of the
116 TimeCtrl, working like up/down cursor key events. (See BindSpinButton.)
119 <DD>Defines the lower bound for "valid" selections in the control.
120 By default, TimeCtrl doesn't have bounds. You must set both upper and lower
121 bounds to make the control pay attention to them, (as only one bound makes no sense
122 with times.) "Valid" times will fall between the min and max "pie wedge" of the
125 <DD>Defines the upper bound for "valid" selections in the control.
126 "Valid" times will fall between the min and max "pie wedge" of the
127 clock. (This can be a "big piece", ie. <b>min = 11pm, max= 10pm</b>
128 means <I>all but the hour from 10:00pm to 11pm are valid times.</I>)
130 <DD>If True, the control will not permit entry of values that fall outside the
134 <DD>Sets the background color used to indicate out-of-bounds values for the control
135 when the control is not limited. This is set to "Yellow" by default.
141 <DT><B>EVT_TIMEUPDATE(win, id, func)</B>
142 <DD>func is fired whenever the value of the control changes.
145 <DT><B>SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta)</B>
146 <DD>Sets the value of the control to a particular time, given a valid
147 value; raises ValueError on invalid value.
148 <EM>NOTE:</EM> This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime
149 was successfully imported by the class module.
151 <DT><B>GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False)</B>
152 <DD>Retrieves the value of the time from the control. By default this is
153 returned as a string, unless one of the other arguments is set; args are
154 searched in the order listed; only one value will be returned.
156 <DT><B>GetWxDateTime(value=None)</B>
157 <DD>When called without arguments, retrieves the value of the control, and applies
158 it to the wxDateTimeFromHMS() constructor, and returns the resulting value.
159 The date portion will always be set to Jan 1, 1970. This form is the same
160 as GetValue(as_wxDateTime=True). GetWxDateTime can also be called with any of the
161 other valid time formats settable with SetValue, to regularize it to a single
162 wxDateTime form. The function will raise ValueError on an unconvertable argument.
164 <DT><B>GetMxDateTime()</B>
165 <DD>Retrieves the value of the control and applies it to the DateTime.Time()
166 constructor,and returns the resulting value. (The date portion will always be
167 set to Jan 1, 1970.) (Same as GetValue(as_wxDateTime=True); provided for backward
168 compatibility with previous release.)
171 <DT><B>BindSpinButton(SpinBtton)</B>
172 <DD>Binds an externally created spin button to the control, so that up/down spin
173 events change the active cell or selection in the control (in addition to the
174 up/down cursor keys.) (This is primarily to allow you to create a "standard"
175 interface to time controls, as seen in Windows.)
178 <DT><B>SetMin(min=None)</B>
179 <DD>Sets the expected minimum value, or lower bound, of the control.
180 (The lower bound will only be enforced if the control is
181 configured to limit its values to the set bounds.)
182 If a value of <I>None</I> is provided, then the control will have
183 explicit lower bound. If the value specified is greater than
184 the current lower bound, then the function returns False and the
185 lower bound will not change from its current setting. On success,
186 the function returns True. Even if set, if there is no corresponding
187 upper bound, the control will behave as if it is unbounded.
188 <DT><DD>If successful and the current value is outside the
189 new bounds, if the control is limited the value will be
190 automatically adjusted to the nearest bound; if not limited,
191 the background of the control will be colored with the current
194 <DT><B>GetMin(as_string=False)</B>
195 <DD>Gets the current lower bound value for the control, returning
196 None, if not set, or a wxDateTime, unless the as_string parameter
197 is set to True, at which point it will return the string
198 representation of the lower bound.
201 <DT><B>SetMax(max=None)</B>
202 <DD>Sets the expected maximum value, or upper bound, of the control.
203 (The upper bound will only be enforced if the control is
204 configured to limit its values to the set bounds.)
205 If a value of <I>None</I> is provided, then the control will
206 have no explicit upper bound. If the value specified is less
207 than the current lower bound, then the function returns False and
208 the maximum will not change from its current setting. On success,
209 the function returns True. Even if set, if there is no corresponding
210 lower bound, the control will behave as if it is unbounded.
211 <DT><DD>If successful and the current value is outside the
212 new bounds, if the control is limited the value will be
213 automatically adjusted to the nearest bound; if not limited,
214 the background of the control will be colored with the current
217 <DT><B>GetMax(as_string = False)</B>
218 <DD>Gets the current upper bound value for the control, returning
219 None, if not set, or a wxDateTime, unless the as_string parameter
220 is set to True, at which point it will return the string
221 representation of the lower bound.
225 <DT><B>SetBounds(min=None,max=None)</B>
226 <DD>This function is a convenience function for setting the min and max
227 values at the same time. The function only applies the maximum bound
228 if setting the minimum bound is successful, and returns True
229 only if both operations succeed. <B><I>Note: leaving out an argument
230 will remove the corresponding bound, and result in the behavior of
231 an unbounded control.</I></B>
233 <DT><B>GetBounds(as_string = False)</B>
234 <DD>This function returns a two-tuple (min,max), indicating the
235 current bounds of the control. Each value can be None if
236 that bound is not set. The values will otherwise be wxDateTimes
237 unless the as_string argument is set to True, at which point they
238 will be returned as string representations of the bounds.
241 <DT><B>IsInBounds(value=None)</B>
242 <DD>Returns <I>True</I> if no value is specified and the current value
243 of the control falls within the current bounds. This function can also
244 be called with a value to see if that value would fall within the current
245 bounds of the given control. It will raise ValueError if the value
246 specified is not a wxDateTime, mxDateTime (if available) or parsable string.
249 <DT><B>IsValid(value)</B>
250 <DD>Returns <I>True</I>if specified value is a legal time value and
251 falls within the current bounds of the given control.
254 <DT><B>SetLimited(bool)</B>
255 <DD>If called with a value of True, this function will cause the control
256 to limit the value to fall within the bounds currently specified.
257 (Provided both bounds have been set.)
258 If the control's value currently exceeds the bounds, it will then
259 be set to the nearest bound.
260 If called with a value of False, this function will disable value
261 limiting, but coloring of out-of-bounds values will still take
262 place if bounds have been set for the control.
263 <DT><B>IsLimited()</B>
264 <DD>Returns <I>True</I> if the control is currently limiting the
265 value to fall within the current bounds.
277 from wx
.tools
.dbg
import Logger
278 from wx
.lib
.masked
import Field
, BaseMaskedTextCtrl
284 from mx
import DateTime
289 # This class of event fires whenever the value of the time changes in the control:
290 wxEVT_TIMEVAL_UPDATED
= wx
.NewEventType()
291 EVT_TIMEUPDATE
= wx
.PyEventBinder(wxEVT_TIMEVAL_UPDATED
, 1)
293 class TimeUpdatedEvent(wx
.PyCommandEvent
):
294 def __init__(self
, id, value
='12:00:00 AM'):
295 wx
.PyCommandEvent
.__init
__(self
, wxEVT_TIMEVAL_UPDATED
, id)
298 """Retrieve the value of the time control at the time this event was generated"""
301 class TimeCtrlAccessorsMixin
:
302 # Define TimeCtrl's list of attributes having their own
303 # Get/Set functions, ignoring those that make no sense for
304 # an numeric control.
305 exposed_basectrl_params
= (
310 'emptyBackgroundColour',
311 'validBackgroundColour',
312 'invalidBackgroundColour',
317 for param
in exposed_basectrl_params
:
318 propname
= param
[0].upper() + param
[1:]
319 exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname
, param
))
320 exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param))
322 if param.find('Colour
') != -1:
323 # add non-british spellings, for backward-compatibility
324 propname.replace('Colour
', 'Color
')
326 exec('def Set
%s(self
, value
): self
.SetCtrlParameters(%s=value
)' % (propname, param))
327 exec('def Get
%s(self
): return self
.GetCtrlParameter("%s")''' % (propname, param))
330 class TimeCtrl(BaseMaskedTextCtrl):
332 valid_ctrl_params = {
333 'format' : 'HHMMSS', # default format code
334 'displaySeconds' : True, # by default, shows seconds
335 'min': None, # by default, no bounds set
337 'limited': False, # by default, no limiting even if bounds set
338 'useFixedWidthFont': True, # by default, use a fixed-width font
339 'oob_color': "Yellow" # by default, the default masked.TextCtrl "invalid" color
343 self, parent, id=-1, value = '00:00:00',
344 pos = wx.DefaultPosition, size = wx.DefaultSize,
347 style = wx.TE_PROCESS_TAB,
348 validator = wx.DefaultValidator,
352 # set defaults for control:
353 ## dbg('setting defaults:')
355 self.__fmt24hr = False
356 wxdt = wx.DateTimeFromDMY(1, 0, 1970)
357 if wxdt.Format('%p') != 'AM':
358 TimeCtrl.valid_ctrl_params['format'] = '24HHMMSS'
359 self.__fmt24hr = True
360 fmt24hr = True # force/change default positional argument
361 # (will countermand explicit set to False too.)
363 for key, param_value in TimeCtrl.valid_ctrl_params.items():
364 # This is done this way to make setattr behave consistently with
365 # "private attribute" name mangling
366 setattr(self, "_TimeCtrl__" + key, copy.copy(param_value))
368 # create locals from current defaults, so we can override if
369 # specified in kwargs, and handle uniformly:
372 limited = self.__limited
373 self.__posCurrent = 0
374 # handle deprecated keword argument name:
375 if kwargs.has_key('display_seconds'):
376 kwargs['displaySeconds'] = kwargs['display_seconds']
377 del kwargs['display_seconds']
378 if not kwargs.has_key('displaySeconds'):
379 kwargs['displaySeconds'] = True
381 # (handle positional arg (from original release) differently from rest of kwargs:)
382 if not kwargs.has_key('format'):
384 if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']:
385 kwargs['format'] = '24HHMMSS'
386 del kwargs['displaySeconds']
388 kwargs['format'] = '24HHMM'
390 if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']:
391 kwargs['format'] = 'HHMMSS'
392 del kwargs['displaySeconds']
394 kwargs['format'] = 'HHMM'
396 if not kwargs.has_key('useFixedWidthFont'):
397 # allow control over font selection:
398 kwargs['useFixedWidthFont'] = self.__useFixedWidthFont
400 maskededit_kwargs = self.SetParameters(**kwargs)
402 # allow for explicit size specification:
403 if size != wx.DefaultSize:
404 # override (and remove) "autofit" autoformat code in standard time formats:
405 maskededit_kwargs['formatcodes'] = 'T!'
407 # This allows range validation if set
408 maskededit_kwargs['validFunc'] = self.IsInBounds
410 # This allows range limits to affect insertion into control or not
411 # dynamically without affecting individual field constraint validation
412 maskededit_kwargs['retainFieldValidation'] = True
414 # Now we can initialize the base control:
415 BaseMaskedTextCtrl.__init__(
419 validator = validator,
421 setupEventHandling = False,
425 # This makes ':' act like tab (after we fix each ':' key event to remove "shift")
426 self._SetKeyHandler(':', self._OnChangeField)
429 # This makes the up/down keys act like spin button controls:
430 self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp)
431 self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown)
434 # This allows ! and c/C to set the control to the current time:
435 self._SetKeyHandler('!', self.__OnSetToNow)
436 self._SetKeyHandler('c', self.__OnSetToNow)
437 self._SetKeyHandler('C', self.__OnSetToNow)
440 # Set up event handling ourselves, so we can insert special
441 # processing on the ":' key to remove the "shift" attribute
442 # *before* the default handlers have been installed, so
443 # that : takes you forward, not back, and so we can issue
444 # EVT_TIMEUPDATE events on changes:
446 self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection
447 self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator
448 self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection) ## limit selections to single field
449 self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick
450 self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab.
451 self.Bind(wx.EVT_CHAR, self.__OnChar ) ## remove "shift" attribute from colon key event,
452 ## then call BaseMaskedTextCtrl._OnChar with
453 ## the possibly modified event.
454 self.Bind(wx.EVT_TEXT, self.__OnTextChange, self ) ## color control appropriately and EVT_TIMEUPDATE events
457 # Validate initial value and set if appropriate
459 self.SetBounds(min, max)
460 self.SetLimited(limited)
463 self.SetValue('00:00:00')
466 self.BindSpinButton(spinButton) # bind spin button up/down events to this control
469 def SetParameters(self, **kwargs):
470 ## dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1)
471 maskededit_kwargs = {}
474 if kwargs.has_key('display_seconds'):
475 kwargs['displaySeconds'] = kwargs['display_seconds']
476 del kwargs['display_seconds']
477 if kwargs.has_key('format') and kwargs.has_key('displaySeconds'):
478 del kwargs['displaySeconds'] # always apply format if specified
480 # assign keyword args as appropriate:
481 for key, param_value in kwargs.items():
482 if key not in TimeCtrl.valid_ctrl_params.keys():
483 raise AttributeError('invalid keyword argument "%s"' % key)
486 wxdt = wx.DateTimeFromDMY(1, 0, 1970)
487 if wxdt.Format('%p') != 'AM':
492 # handle both local or generic 'maskededit' autoformat codes:
493 if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS':
494 self.__displaySeconds = True
495 self.__fmt24hr = False
496 elif param_value == 'HHMM' or param_value == 'TIMEHHMM':
497 self.__displaySeconds = False
498 self.__fmt24hr = False
499 elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS':
500 self.__displaySeconds = True
501 self.__fmt24hr = True
502 elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM':
503 self.__displaySeconds = False
504 self.__fmt24hr = True
506 raise AttributeError('"%s" is not a valid format' % param_value)
508 if require24hr and not self.__fmt24hr:
509 raise AttributeError('"%s" is an unsupported time format for the current locale' % param_value)
513 elif key in ("displaySeconds", "display_seconds") and not kwargs.has_key('format'):
514 self.__displaySeconds = param_value
517 elif key == "min": min = param_value
518 elif key == "max": max = param_value
519 elif key == "limited": limited = param_value
521 elif key == "useFixedWidthFont":
522 maskededit_kwargs[key] = param_value
524 elif key == "oob_color":
525 maskededit_kwargs['invalidBackgroundColor'] = param_value
529 if self.__displaySeconds: maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS'
530 else: maskededit_kwargs['autoformat'] = '24HRTIMEHHMM'
532 # Set hour field to zero-pad, right-insert, require explicit field change,
533 # select entire field on entry, and require a resultant valid entry
534 # to allow character entry:
535 hourfield = Field(formatcodes='0r<SV', validRegex='0\d|1\d|2[0123]', validRequired=True)
537 if self.__displaySeconds: maskededit_kwargs['autoformat'] = 'TIMEHHMMSS'
538 else: maskededit_kwargs['autoformat'] = 'TIMEHHMM'
540 # Set hour field to allow spaces (at start), right-insert,
541 # require explicit field change, select entire field on entry,
542 # and require a resultant valid entry to allow character entry:
543 hourfield = Field(formatcodes='_0<rSV', validRegex='0[1-9]| [1-9]|1[012]', validRequired=True)
544 ampmfield = Field(formatcodes='S', emptyInvalid = True, validRequired = True)
546 # Field 1 is always a zero-padded right-insert minute field,
547 # similarly configured as above:
548 minutefield = Field(formatcodes='0r<SV', validRegex='[0-5]\d', validRequired=True)
550 fields = [ hourfield, minutefield ]
551 if self.__displaySeconds:
552 fields.append(copy.copy(minutefield)) # second field has same constraints as field 1
554 if not self.__fmt24hr:
555 fields.append(ampmfield)
557 # set fields argument:
558 maskededit_kwargs['fields'] = fields
560 # This allows range validation if set
561 maskededit_kwargs['validFunc'] = self.IsInBounds
563 # This allows range limits to affect insertion into control or not
564 # dynamically without affecting individual field constraint validation
565 maskededit_kwargs['retainFieldValidation'] = True
567 if hasattr(self, 'controlInitialized') and self.controlInitialized:
568 self.SetCtrlParameters(**maskededit_kwargs) # set appropriate parameters
570 # Validate initial value and set if appropriate
572 self.SetBounds(min, max)
573 self.SetLimited(limited)
576 self.SetValue('00:00:00')
578 return {} # no arguments to return
581 return maskededit_kwargs
584 def BindSpinButton(self, sb):
586 This function binds an externally created spin button to the control, so that
587 up/down events from the button automatically change the control.
589 ## dbg('TimeCtrl::BindSpinButton')
590 self.__spinButton = sb
591 if self.__spinButton:
592 # bind event handlers to spin ctrl
593 self.__spinButton.Bind(wx.EVT_SPIN_UP, self.__OnSpinUp, self.__spinButton)
594 self.__spinButton.Bind(wx.EVT_SPIN_DOWN, self.__OnSpinDown, self.__spinButton)
598 return "<TimeCtrl: %s>" % self.GetValue()
601 def SetValue(self, value):
603 Validating SetValue function for time values:
604 This function will do dynamic type checking on the value argument,
605 and convert wxDateTime, mxDateTime, or 12/24 format time string
606 into the appropriate format string for the control.
608 ## dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1)
610 strtime = self._toGUI(self.__validateValue(value))
612 ## dbg('validation failed', indent=0)
615 ## dbg('strtime:', strtime)
616 self._SetValue(strtime)
620 as_wxDateTime = False,
621 as_mxDateTime = False,
622 as_wxTimeSpan = False,
623 as_mxDateTimeDelta = False):
626 if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta:
627 value = self.GetWxDateTime()
631 value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond())
633 value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond())
634 elif as_mxDateTimeDelta:
635 value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond())
637 value = BaseMaskedTextCtrl.GetValue(self)
641 def SetWxDateTime(self, wxdt):
643 Because SetValue can take a wxDateTime, this is now just an alias.
648 def GetWxDateTime(self, value=None):
650 This function is the conversion engine for TimeCtrl; it takes
651 one of the following types:
657 and converts it to a wxDateTime that always has Jan 1, 1970 as its date
658 portion, so that range comparisons around values can work using
659 wxDateTime's built-in comparison function. If a value is not
660 provided to convert, the string value of the control will be used.
661 If the value is not one of the accepted types, a ValueError will be
666 ## dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1)
668 ## dbg('getting control value')
669 value = self.GetValue()
670 ## dbg('value = "%s"' % value)
672 if type(value) == types.UnicodeType:
673 value = str(value) # convert to regular string
675 valid = True # assume true
676 if type(value) == types.StringType:
678 # Construct constant wxDateTime, then try to parse the string:
679 wxdt = wx.DateTimeFromDMY(1, 0, 1970)
680 ## dbg('attempting conversion')
681 value = value.strip() # (parser doesn't like leading spaces)
682 checkTime = wxdt.ParseTime(value)
683 valid = checkTime == len(value) # entire string parsed?
684 ## dbg('checkTime == len(value)?', valid)
687 # deal with bug/deficiency in wx.DateTime:
688 if wxdt.Format('%p') not in ('AM', 'PM') and checkTime in (5,8):
689 # couldn't parse the AM/PM field
690 raise ValueError('cannot convert string "%s" to valid time for the current locale; please use 24hr time instead' % value)
692 ## dbg(indent=0, suspend=0)
693 raise ValueError('cannot convert string "%s" to valid time' % value)
696 if isinstance(value, wx.DateTime):
697 hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond()
698 elif isinstance(value, wx.TimeSpan):
699 totalseconds = value.GetSeconds()
700 hour = totalseconds / 3600
701 minute = totalseconds / 60 - (hour * 60)
702 second = totalseconds - ((hour * 3600) + (minute * 60))
704 elif accept_mx and isinstance(value, DateTime.DateTimeType):
705 hour, minute, second = value.hour, value.minute, value.second
706 elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType):
707 hour, minute, second = value.hour, value.minute, value.second
709 # Not a valid function argument
711 error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value)
713 error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value)
714 ## dbg(indent=0, suspend=0)
715 raise ValueError(error)
717 wxdt = wx.DateTimeFromDMY(1, 0, 1970)
719 wxdt.SetMinute(minute)
720 wxdt.SetSecond(second)
722 ## dbg('wxdt:', wxdt, indent=0, suspend=0)
726 def SetMxDateTime(self, mxdt):
728 Because SetValue can take an mxDateTime, (if DateTime is importable),
729 this is now just an alias.
734 def GetMxDateTime(self, value=None):
736 t = self.GetValue(as_mxDateTime=True)
738 # Convert string 1st to wxDateTime, then use components, since
739 # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM:
740 wxdt = self.GetWxDateTime(value)
741 hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()
742 t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second)
746 def SetMin(self, min=None):
748 Sets the minimum value of the control. If a value of None
749 is provided, then the control will have no explicit minimum value.
750 If the value specified is greater than the current maximum value,
751 then the function returns 0 and the minimum will not change from
752 its current setting. On success, the function returns 1.
754 If successful and the current value is lower than the new lower
755 bound, if the control is limited, the value will be automatically
756 adjusted to the new minimum value; if not limited, the value in the
757 control will be colored as invalid.
759 ## dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1)
762 min = self.GetWxDateTime(min)
763 self.__min = self._toGUI(min)
765 ## dbg('exception occurred', indent=0)
770 if self.IsLimited() and not self.IsInBounds():
771 self.SetLimited(self.__limited) # force limited value:
775 ## dbg('ret:', ret, indent=0)
779 def GetMin(self, as_string = False):
781 Gets the minimum value of the control.
782 If None, it will return None. Otherwise it will return
783 the current minimum bound on the control, as a wxDateTime
784 by default, or as a string if as_string argument is True.
787 ## dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1)
788 if self.__min is None:
789 ## dbg('(min == None)')
796 ret = self.GetWxDateTime(self.__min)
799 ## dbg('exception occurred', indent=0)
801 ## dbg('ret:', repr(ret))
802 ## dbg(indent=0, suspend=0)
806 def SetMax(self, max=None):
808 Sets the maximum value of the control. If a value of None
809 is provided, then the control will have no explicit maximum value.
810 If the value specified is less than the current minimum value, then
811 the function returns False and the maximum will not change from its
812 current setting. On success, the function returns True.
814 If successful and the current value is greater than the new upper
815 bound, if the control is limited the value will be automatically
816 adjusted to this maximum value; if not limited, the value in the
817 control will be colored as invalid.
819 ## dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1)
822 max = self.GetWxDateTime(max)
823 self.__max = self._toGUI(max)
825 ## dbg('exception occurred', indent=0)
829 ## dbg('max:', repr(self.__max))
830 if self.IsLimited() and not self.IsInBounds():
831 self.SetLimited(self.__limited) # force limited value:
835 ## dbg('ret:', ret, indent=0)
839 def GetMax(self, as_string = False):
841 Gets the minimum value of the control.
842 If None, it will return None. Otherwise it will return
843 the current minimum bound on the control, as a wxDateTime
844 by default, or as a string if as_string argument is True.
847 ## dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1)
848 if self.__max is None:
849 ## dbg('(max == None)')
856 ret = self.GetWxDateTime(self.__max)
859 ## dbg('exception occurred', indent=0)
861 ## dbg('ret:', repr(ret))
862 ## dbg(indent=0, suspend=0)
866 def SetBounds(self, min=None, max=None):
868 This function is a convenience function for setting the min and max
869 values at the same time. The function only applies the maximum bound
870 if setting the minimum bound is successful, and returns True
871 only if both operations succeed.
872 NOTE: leaving out an argument will remove the corresponding bound.
874 ret = self.SetMin(min)
875 return ret and self.SetMax(max)
878 def GetBounds(self, as_string = False):
880 This function returns a two-tuple (min,max), indicating the
881 current bounds of the control. Each value can be None if
882 that bound is not set.
884 return (self.GetMin(as_string), self.GetMax(as_string))
887 def SetLimited(self, limited):
889 If called with a value of True, this function will cause the control
890 to limit the value to fall within the bounds currently specified.
891 If the control's value currently exceeds the bounds, it will then
892 be limited accordingly.
894 If called with a value of 0, this function will disable value
895 limiting, but coloring of out-of-bounds values will still take
896 place if bounds have been set for the control.
898 ## dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1)
899 self.__limited = limited
902 self.SetMaskParameters(validRequired = False)
907 ## dbg('requiring valid value')
908 self.SetMaskParameters(validRequired = True)
912 if min is None or max is None:
913 ## dbg('both bounds not set; no further action taken')
914 return # can't limit without 2 bounds
916 elif not self.IsInBounds():
917 # set value to the nearest bound:
919 value = self.GetWxDateTime()
921 ## dbg('exception occurred', indent=0)
924 if min <= max: # valid range doesn't span midnight
926 # which makes the "nearest bound" computation trickier...
928 # determine how long the "invalid" pie wedge is, and cut
929 # this interval in half for comparison purposes:
931 # Note: relies on min and max and value date portions
932 # always being the same.
933 interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max
935 half_interval = wx.TimeSpan(
938 interval.GetSeconds() / 2, # seconds
941 if value < min: # min is on next day, so use value on
942 # "next day" for "nearest" interval calculation:
943 cmp_value = value + wx.TimeSpan(24, 0, 0, 0)
944 else: # "before midnight; ok
947 if (cmp_value - max) > half_interval:
948 ## dbg('forcing value to min (%s)' % min.FormatTime())
951 ## dbg('forcing value to max (%s)' % max.FormatTime())
955 # therefore max < value < min guaranteed to be true,
956 # so "nearest bound" calculation is much easier:
957 if (value - max) >= (min - value):
958 # current value closer to min; pick that edge of pie wedge
959 ## dbg('forcing value to min (%s)' % min.FormatTime())
962 ## dbg('forcing value to max (%s)' % max.FormatTime())
971 Returns True if the control is currently limiting the
972 value to fall within any current bounds. Note: can
973 be set even if there are no current bounds.
975 return self.__limited
978 def IsInBounds(self, value=None):
980 Returns True if no value is specified and the current value
981 of the control falls within the current bounds. As the clock
982 is a "circle", both minimum and maximum bounds must be set for
983 a value to ever be considered "out of bounds". This function can
984 also be called with a value to see if that value would fall within
985 the current bounds of the given control.
987 if value is not None:
989 value = self.GetWxDateTime(value) # try to regularize passed value
991 ## dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0)
994 ## dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1)
995 if self.__min is None or self.__max is None:
1001 value = self.GetWxDateTime()
1003 ## dbg('exception occurred', indent=0)
1006 ## dbg('value:', value.FormatTime())
1008 # Get wxDateTime representations of bounds:
1012 midnight = wx.DateTimeFromDMY(1, 0, 1970)
1013 if min <= max: # they don't span midnight
1014 ret = min <= value <= max
1017 # have to break into 2 tests; to be in bounds
1018 # either "min" <= value (<= midnight of *next day*)
1019 # or midnight <= value <= "max"
1020 ret = min <= value or (midnight <= value <= max)
1021 ## dbg('in bounds?', ret, indent=0)
1025 def IsValid( self, value ):
1027 Can be used to determine if a given value would be a legal and
1028 in-bounds value for the control.
1031 self.__validateValue(value)
1036 def SetFormat(self, format):
1037 self.SetParameters(format=format)
1039 def GetFormat(self):
1040 if self.__displaySeconds:
1041 if self.__fmt24hr: return '24HHMMSS'
1042 else: return 'HHMMSS'
1044 if self.__fmt24hr: return '24HHMM'
1047 #-------------------------------------------------------------------------------------------------------------
1048 # these are private functions and overrides:
1051 def __OnTextChange(self, event=None):
1052 ## dbg('TimeCtrl::OnTextChange', indent=1)
1054 # Allow Maskedtext base control to color as appropriate,
1055 # and Skip the EVT_TEXT event (if appropriate.)
1056 ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue()
1057 ## call is generating two (2) EVT_TEXT events. (!)
1058 ## The the only mechanism I can find to mask this problem is to
1059 ## keep track of last value seen, and declare a valid EVT_TEXT
1060 ## event iff the value has actually changed. The masked edit
1061 ## OnTextChange routine does this, and returns True on a valid event,
1063 if not BaseMaskedTextCtrl._OnTextChange(self, event):
1066 ## dbg('firing TimeUpdatedEvent...')
1067 evt = TimeUpdatedEvent(self.GetId(), self.GetValue())
1068 evt.SetEventObject(self)
1069 self.GetEventHandler().ProcessEvent(evt)
1073 def SetInsertionPoint(self, pos):
1075 Records the specified position and associated cell before calling base class' function.
1076 This is necessary to handle the optional spin button, because the insertion
1077 point is lost when the focus shifts to the spin button.
1079 ## dbg('TimeCtrl::SetInsertionPoint', pos, indent=1)
1080 BaseMaskedTextCtrl.SetInsertionPoint(self, pos) # (causes EVT_TEXT event to fire)
1081 self.__posCurrent = self.GetInsertionPoint()
1085 def SetSelection(self, sel_start, sel_to):
1086 ## dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1)
1088 # Adjust selection range to legal extent if not already
1092 if self.__posCurrent != sel_start: # force selection and insertion point to match
1093 self.SetInsertionPoint(sel_start)
1094 cell_start, cell_end = self._FindField(sel_start)._extent
1095 if not cell_start <= sel_to <= cell_end:
1098 self.__bSelection = sel_start != sel_to
1099 BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to)
1103 def __OnSpin(self, key):
1105 This is the function that gets called in response to up/down arrow or
1106 bound spin button events.
1108 self.__IncrementValue(key, self.__posCurrent) # changes the value
1110 # Ensure adjusted control regains focus and has adjusted portion
1113 start, end = self._FindField(self.__posCurrent)._extent
1114 self.SetInsertionPoint(start)
1115 self.SetSelection(start, end)
1116 ## dbg('current position:', self.__posCurrent)
1119 def __OnSpinUp(self, event):
1121 Event handler for any bound spin button on EVT_SPIN_UP;
1122 causes control to behave as if up arrow was pressed.
1124 ## dbg('TimeCtrl::OnSpinUp', indent=1)
1125 self.__OnSpin(wx.WXK_UP)
1126 keep_processing = False
1128 return keep_processing
1131 def __OnSpinDown(self, event):
1133 Event handler for any bound spin button on EVT_SPIN_DOWN;
1134 causes control to behave as if down arrow was pressed.
1136 ## dbg('TimeCtrl::OnSpinDown', indent=1)
1137 self.__OnSpin(wx.WXK_DOWN)
1138 keep_processing = False
1140 return keep_processing
1143 def __OnChar(self, event):
1145 Handler to explicitly look for ':' keyevents, and if found,
1146 clear the m_shiftDown field, so it will behave as forward tab.
1147 It then calls the base control's _OnChar routine with the modified
1150 ## dbg('TimeCtrl::OnChar', indent=1)
1151 keycode = event.GetKeyCode()
1152 ## dbg('keycode:', keycode)
1153 if keycode == ord(':'):
1154 ## dbg('colon seen! removing shift attribute')
1155 event.m_shiftDown = False
1156 BaseMaskedTextCtrl._OnChar(self, event ) ## handle each keypress
1160 def __OnSetToNow(self, event):
1162 This is the key handler for '!' and 'c'; this allows the user to
1163 quickly set the value of the control to the current time.
1165 self.SetValue(wx.DateTime_Now().FormatTime())
1166 keep_processing = False
1167 return keep_processing
1170 def __LimitSelection(self, event):
1172 Event handler for motion events; this handler
1173 changes limits the selection to the new cell boundaries.
1175 ## dbg('TimeCtrl::LimitSelection', indent=1)
1176 pos = self.GetInsertionPoint()
1177 self.__posCurrent = pos
1178 sel_start, sel_to = self.GetSelection()
1179 selection = sel_start != sel_to
1181 # only allow selection to end of current cell:
1182 start, end = self._FindField(sel_start)._extent
1183 if sel_to < pos: sel_to = start
1184 elif sel_to > pos: sel_to = end
1186 ## dbg('new pos =', self.__posCurrent, 'select to ', sel_to)
1187 self.SetInsertionPoint(self.__posCurrent)
1188 self.SetSelection(self.__posCurrent, sel_to)
1189 if event: event.Skip()
1193 def __IncrementValue(self, key, pos):
1194 ## dbg('TimeCtrl::IncrementValue', key, pos, indent=1)
1195 text = self.GetValue()
1196 field = self._FindField(pos)
1197 ## dbg('field: ', field._index)
1198 start, end = field._extent
1199 slice = text[start:end]
1200 if key == wx.WXK_UP: increment = 1
1201 else: increment = -1
1203 if slice in ('A', 'P'):
1204 if slice == 'A': newslice = 'P'
1205 elif slice == 'P': newslice = 'A'
1206 newvalue = text[:start] + newslice + text[end:]
1208 elif field._index == 0:
1209 # adjusting this field is trickier, as its value can affect the
1210 # am/pm setting. So, we use wxDateTime to generate a new value for us:
1211 # (Use a fixed date not subject to DST variations:)
1212 converter = wx.DateTimeFromDMY(1, 0, 1970)
1213 ## dbg('text: "%s"' % text)
1214 converter.ParseTime(text.strip())
1215 currenthour = converter.GetHour()
1216 ## dbg('current hour:', currenthour)
1217 newhour = (currenthour + increment) % 24
1218 ## dbg('newhour:', newhour)
1219 converter.SetHour(newhour)
1220 ## dbg('converter.GetHour():', converter.GetHour())
1221 newvalue = converter # take advantage of auto-conversion for am/pm in .SetValue()
1223 else: # minute or second field; handled the same way:
1224 newslice = "%02d" % ((int(slice) + increment) % 60)
1225 newvalue = text[:start] + newslice + text[end:]
1228 self.SetValue(newvalue)
1230 except ValueError: # must not be in bounds:
1231 if not wx.Validator_IsSilent():
1236 def _toGUI( self, wxdt ):
1238 This function takes a wxdt as an unambiguous representation of a time, and
1239 converts it to a string appropriate for the format of the control.
1242 if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S')
1243 else: strval = wxdt.Format('%H:%M')
1245 if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p')
1246 else: strval = wxdt.Format('%I:%M %p')
1251 def __validateValue( self, value ):
1253 This function converts the value to a wxDateTime if not already one,
1254 does bounds checking and raises ValueError if argument is
1255 not a valid value for the control as currently specified.
1256 It is used by both the SetValue() and the IsValid() methods.
1258 ## dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1)
1261 raise ValueError('%s not a valid time value' % repr(value))
1263 valid = True # assume true
1265 value = self.GetWxDateTime(value) # regularize form; can generate ValueError if problem doing so
1267 ## dbg('exception occurred', indent=0)
1270 if self.IsLimited() and not self.IsInBounds(value):
1273 'value %s is not within the bounds of the control' % str(value) )
1277 #----------------------------------------------------------------------------
1278 # Test jig for TimeCtrl:
1280 if __name__ == '__main__':
1283 class TestPanel(wx.Panel):
1284 def __init__(self, parent, id,
1285 pos = wx.DefaultPosition, size = wx.DefaultSize,
1286 fmt24hr = 0, test_mx = 0,
1287 style = wx.TAB_TRAVERSAL ):
1289 wx.Panel.__init__(self, parent, id, pos, size, style)
1291 self.test_mx = test_mx
1293 self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr)
1294 sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 )
1295 self.tc.BindSpinButton(sb)
1297 sizer = wx.BoxSizer( wx.HORIZONTAL )
1298 sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 )
1299 sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 )
1301 self.SetAutoLayout( True )
1302 self.SetSizer( sizer )
1304 sizer.SetSizeHints( self )
1306 self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc)
1308 def OnTimeChange(self, event):
1309 ## dbg('OnTimeChange: value = ', event.GetValue())
1310 wxdt = self.tc.GetWxDateTime()
1311 ## dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond())
1313 mxdt = self.tc.GetMxDateTime()
1314 ## dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second)
1317 class MyApp(wx.App):
1320 fmt24hr = '24' in sys.argv
1321 test_mx = 'mx' in sys.argv
1323 frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) )
1324 panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx)
1327 traceback.print_exc()
1335 traceback.print_exc()
1338 ## 1. Changed parameter name display_seconds to displaySeconds, to follow
1339 ## other masked edit conventions.
1340 ## 2. Added format parameter, to remove need to use both fmt24hr and displaySeconds.
1341 ## 3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of
1342 ## nonsensical parameter methods from the control, so it will work
1343 ## properly with Boa.