]>
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 
  47 *TimeCtrl* provides a multi-cell control that allows manipulation of a time 
  48 value.  It supports 12 or 24 hour format, and you can use wxDateTime or mxDateTime 
  49 to get/set values from the control. 
  51 Left/right/tab keys to switch cells within a TimeCtrl, and the up/down arrows act 
  52 like a spin control.  TimeCtrl also allows for an actual spin button to be attached 
  53 to the control, so that it acts like the up/down arrow keys. 
  55 The ! or c key sets the value of the control to the current time. 
  57   Here's the API for TimeCtrl:: 
  59     from wx.lib.masked import TimeCtrl 
  64          pos = wx.DefaultPosition, 
  65          size = wx.DefaultSize, 
  66          style = wxTE_PROCESS_TAB, 
  67          validator = wx.DefaultValidator, 
  71          displaySeconds = True, 
  81     If no initial value is set, the default will be midnight; if an illegal string 
  82     is specified, a ValueError will result.  (You can always later set the initial time 
  83     with SetValue() after instantiation of the control.) 
  86     The size of the control will be automatically adjusted for 12/24 hour format 
  87     if wx.DefaultSize is specified.  NOTE: due to a problem with wx.DateTime, if the 
  88     locale does not use 'AM/PM' for its values, the default format will automatically 
  89     change to 24 hour format, and an AttributeError will be thrown if a non-24 format 
  93     By default, TimeCtrl will process TAB events, by allowing tab to the 
  94     different cells within the control. 
  97     By default, TimeCtrl just uses the default (empty) validator, as all 
  98     of its validation for entry control is handled internally.  However, a validator 
  99     can be supplied to provide data transfer capability to the control. 
 102     This parameter can be used instead of the fmt24hr and displaySeconds 
 103     parameters, respectively; it provides a shorthand way to specify the time 
 104     format you want.  Accepted values are 'HHMMSS', 'HHMM', '24HHMMSS', and 
 105     '24HHMM'.  If the format is specified, the other two arguments will be ignored. 
 108     If True, control will display time in 24 hour time format; if False, it will 
 109     use 12 hour AM/PM format.  SetValue() will adjust values accordingly for the 
 110     control, based on the format specified.  (This value is ignored if the *format* 
 111     parameter is specified.) 
 114     If True, control will include a seconds field; if False, it will 
 115     just show hours and minutes. (This value is ignored if the *format* 
 116     parameter is specified.) 
 119     If specified, this button's events will be bound to the behavior of the 
 120     TimeCtrl, working like up/down cursor key events.  (See BindSpinButton.) 
 123     Defines the lower bound for "valid" selections in the control. 
 124     By default, TimeCtrl doesn't have bounds.  You must set both upper and lower 
 125     bounds to make the control pay attention to them, (as only one bound makes no sense 
 126     with times.) "Valid" times will fall between the min and max "pie wedge" of the 
 129     Defines the upper bound for "valid" selections in the control. 
 130     "Valid" times will fall between the min and max "pie wedge" of the 
 131     clock. (This can be a "big piece", ie. min = 11pm, max= 10pm 
 132     means *all but the hour from 10:00pm to 11pm are valid times.*) 
 134     If True, the control will not permit entry of values that fall outside the 
 138     Sets the background color used to indicate out-of-bounds values for the control 
 139     when the control is not limited.  This is set to "Yellow" by default. 
 143 EVT_TIMEUPDATE(win, id, func) 
 144     func is fired whenever the value of the control changes. 
 147 SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta) 
 148     Sets the value of the control to a particular time, given a valid 
 149     value; raises ValueError on invalid value. 
 151 *NOTE:* This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime 
 152     was successfully imported by the class module. 
 154 GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False) 
 155     Retrieves the value of the time from the control.  By default this is 
 156     returned as a string, unless one of the other arguments is set; args are 
 157     searched in the order listed; only one value will be returned. 
 159 GetWxDateTime(value=None) 
 160     When called without arguments, retrieves the value of the control, and applies 
 161     it to the wxDateTimeFromHMS() constructor, and returns the resulting value. 
 162     The date portion will always be set to Jan 1, 1970. This form is the same 
 163     as GetValue(as_wxDateTime=True).  GetWxDateTime can also be called with any of the 
 164     other valid time formats settable with SetValue, to regularize it to a single 
 165     wxDateTime form.  The function will raise ValueError on an unconvertable argument. 
 168     Retrieves the value of the control and applies it to the DateTime.Time() 
 169     constructor,and returns the resulting value.  (The date portion will always be 
 170     set to Jan 1, 1970.) (Same as GetValue(as_wxDateTime=True); provided for backward 
 171     compatibility with previous release.) 
 174 BindSpinButton(SpinBtton) 
 175     Binds an externally created spin button to the control, so that up/down spin 
 176     events change the active cell or selection in the control (in addition to the 
 177     up/down cursor keys.)  (This is primarily to allow you to create a "standard" 
 178     interface to time controls, as seen in Windows.) 
 182     Sets the expected minimum value, or lower bound, of the control. 
 183     (The lower bound will only be enforced if the control is 
 184     configured to limit its values to the set bounds.) 
 185     If a value of *None* is provided, then the control will have 
 186     explicit lower bound.  If the value specified is greater than 
 187     the current lower bound, then the function returns False and the 
 188     lower bound will not change from its current setting.  On success, 
 189     the function returns True.  Even if set, if there is no corresponding 
 190     upper bound, the control will behave as if it is unbounded. 
 192     If successful and the current value is outside the 
 193     new bounds, if the control is limited the value will be 
 194     automatically adjusted to the nearest bound; if not limited, 
 195     the background of the control will be colored with the current 
 198 GetMin(as_string=False) 
 199     Gets the current lower bound value for the control, returning 
 200     None, if not set, or a wxDateTime, unless the as_string parameter 
 201     is set to True, at which point it will return the string 
 202     representation of the lower bound. 
 206     Sets the expected maximum value, or upper bound, of the control. 
 207     (The upper bound will only be enforced if the control is 
 208     configured to limit its values to the set bounds.) 
 209     If a value of *None* is provided, then the control will 
 210     have no explicit upper bound.  If the value specified is less 
 211     than the current lower bound, then the function returns False and 
 212     the maximum will not change from its current setting. On success, 
 213     the function returns True.  Even if set, if there is no corresponding 
 214     lower bound, the control will behave as if it is unbounded. 
 216     If successful and the current value is outside the 
 217     new bounds, if the control is limited the value will be 
 218     automatically adjusted to the nearest bound; if not limited, 
 219     the background of the control will be colored with the current 
 222 GetMax(as_string = False) 
 223     Gets the current upper bound value for the control, returning 
 224     None, if not set, or a wxDateTime, unless the as_string parameter 
 225     is set to True, at which point it will return the string 
 226     representation of the lower bound. 
 230 SetBounds(min=None,max=None) 
 231     This function is a convenience function for setting the min and max 
 232     values at the same time.  The function only applies the maximum bound 
 233     if setting the minimum bound is successful, and returns True 
 234     only if both operations succeed.  *Note: leaving out an argument 
 235     will remove the corresponding bound, and result in the behavior of 
 236     an unbounded control.* 
 238 GetBounds(as_string = False) 
 239     This function returns a two-tuple (min,max), indicating the 
 240     current bounds of the control.  Each value can be None if 
 241     that bound is not set.  The values will otherwise be wxDateTimes 
 242     unless the as_string argument is set to True, at which point they 
 243     will be returned as string representations of the bounds. 
 246 IsInBounds(value=None) 
 247     Returns *True* if no value is specified and the current value 
 248     of the control falls within the current bounds.  This function can also 
 249     be called with a value to see if that value would fall within the current 
 250     bounds of the given control.  It will raise ValueError if the value 
 251     specified is not a wxDateTime, mxDateTime (if available) or parsable string. 
 255     Returns *True* if specified value is a legal time value and 
 256     falls within the current bounds of the given control. 
 260     If called with a value of True, this function will cause the control 
 261     to limit the value to fall within the bounds currently specified. 
 262     (Provided both bounds have been set.) 
 263     If the control's value currently exceeds the bounds, it will then 
 264     be set to the nearest bound. 
 265     If called with a value of False, this function will disable value 
 266     limiting, but coloring of out-of-bounds values will still take 
 267     place if bounds have been set for the control. 
 269     Returns *True* if the control is currently limiting the 
 270     value to fall within the current bounds. 
 281 from wx
.tools
.dbg 
import Logger
 
 282 from wx
.lib
.masked 
import Field
, BaseMaskedTextCtrl
 
 288     from mx 
import DateTime
 
 293 # This class of event fires whenever the value of the time changes in the control: 
 294 wxEVT_TIMEVAL_UPDATED 
= wx
.NewEventType() 
 295 EVT_TIMEUPDATE 
= wx
.PyEventBinder(wxEVT_TIMEVAL_UPDATED
, 1) 
 297 class TimeUpdatedEvent(wx
.PyCommandEvent
): 
 299     Used to fire an EVT_TIMEUPDATE event whenever the value in a TimeCtrl changes. 
 301     def __init__(self
, id, value 
='12:00:00 AM'): 
 302         wx
.PyCommandEvent
.__init
__(self
, wxEVT_TIMEVAL_UPDATED
, id) 
 305         """Retrieve the value of the time control at the time this event was generated""" 
 308 class TimeCtrlAccessorsMixin
: 
 310     Defines TimeCtrl's list of attributes having their own Get/Set functions, 
 311     ignoring those in the base class that make no sense for a time control. 
 313     exposed_basectrl_params 
= ( 
 318          'emptyBackgroundColour', 
 319          'validBackgroundColour', 
 320          'invalidBackgroundColour', 
 325     for param 
in exposed_basectrl_params
: 
 326         propname 
= param
[0].upper() + param
[1:] 
 327         exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname
, param
)) 
 328         exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
 330         if param.find('Colour
') != -1: 
 331             # add non-british spellings, for backward-compatibility 
 332             propname.replace('Colour
', 'Color
') 
 334             exec('def Set
%s(self
, value
): self
.SetCtrlParameters(%s=value
)' % (propname, param)) 
 335             exec('def Get
%s(self
): return self
.GetCtrlParameter("%s")''' % (propname, param)) 
 338 class TimeCtrl(BaseMaskedTextCtrl): 
 340     Masked control providing several time formats and manipulation of time values. 
 343     valid_ctrl_params = { 
 344         'format' :  'HHMMSS',       # default format code 
 345         'displaySeconds' : True,    # by default, shows seconds 
 346         'min': None,                # by default, no bounds set 
 348         'limited': False,           # by default, no limiting even if bounds set 
 349         'useFixedWidthFont': True,  # by default, use a fixed-width font 
 350         'oob_color': "Yellow"       # by default, the default masked.TextCtrl "invalid" color 
 354                 self, parent, id=-1, value = '00:00:00', 
 355                 pos = wx.DefaultPosition, size = wx.DefaultSize, 
 358                 style = wx.TE_PROCESS_TAB, 
 359                 validator = wx.DefaultValidator, 
 363         # set defaults for control: 
 364 ##        dbg('setting defaults:') 
 366         self.__fmt24hr = False 
 367         wxdt = wx.DateTimeFromDMY(1, 0, 1970) 
 369             if wxdt.Format('%p') != 'AM': 
 370                 TimeCtrl.valid_ctrl_params['format'] = '24HHMMSS' 
 371                 self.__fmt24hr = True 
 372                 fmt24hr = True  # force/change default positional argument 
 373                                 # (will countermand explicit set to False too.) 
 375             TimeCtrl.valid_ctrl_params['format'] = '24HHMMSS' 
 376             self.__fmt24hr = True 
 377             fmt24hr = True  # force/change default positional argument 
 378                             # (will countermand explicit set to False too.) 
 380         for key, param_value in TimeCtrl.valid_ctrl_params.items(): 
 381             # This is done this way to make setattr behave consistently with 
 382             # "private attribute" name mangling 
 383             setattr(self, "_TimeCtrl__" + key, copy.copy(param_value)) 
 385         # create locals from current defaults, so we can override if 
 386         # specified in kwargs, and handle uniformly: 
 389         limited = self.__limited 
 390         self.__posCurrent = 0 
 391         # handle deprecated keword argument name: 
 392         if kwargs.has_key('display_seconds'): 
 393             kwargs['displaySeconds'] = kwargs['display_seconds'] 
 394             del kwargs['display_seconds'] 
 395         if not kwargs.has_key('displaySeconds'): 
 396             kwargs['displaySeconds'] = True 
 398         # (handle positional arg (from original release) differently from rest of kwargs:) 
 399         if not kwargs.has_key('format'): 
 401                 if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: 
 402                     kwargs['format'] = '24HHMMSS' 
 403                     del kwargs['displaySeconds'] 
 405                     kwargs['format'] = '24HHMM' 
 407                 if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: 
 408                     kwargs['format'] = 'HHMMSS' 
 409                     del kwargs['displaySeconds'] 
 411                     kwargs['format'] = 'HHMM' 
 413         if not kwargs.has_key('useFixedWidthFont'): 
 414             # allow control over font selection: 
 415             kwargs['useFixedWidthFont'] = self.__useFixedWidthFont 
 417         maskededit_kwargs = self.SetParameters(**kwargs) 
 419         # allow for explicit size specification: 
 420         if size != wx.DefaultSize: 
 421             # override (and remove) "autofit" autoformat code in standard time formats: 
 422             maskededit_kwargs['formatcodes'] = 'T!' 
 424         # This allows range validation if set 
 425         maskededit_kwargs['validFunc'] = self.IsInBounds 
 427         # This allows range limits to affect insertion into control or not 
 428         # dynamically without affecting individual field constraint validation 
 429         maskededit_kwargs['retainFieldValidation'] = True 
 431         # Now we can initialize the base control: 
 432         BaseMaskedTextCtrl.__init__( 
 436                 validator = validator, 
 438                 setupEventHandling = False, 
 442         # This makes ':' act like tab (after we fix each ':' key event to remove "shift") 
 443         self._SetKeyHandler(':', self._OnChangeField) 
 446         # This makes the up/down keys act like spin button controls: 
 447         self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp) 
 448         self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown) 
 451         # This allows ! and c/C to set the control to the current time: 
 452         self._SetKeyHandler('!', self.__OnSetToNow) 
 453         self._SetKeyHandler('c', self.__OnSetToNow) 
 454         self._SetKeyHandler('C', self.__OnSetToNow) 
 457         # Set up event handling ourselves, so we can insert special 
 458         # processing on the ":' key to remove the "shift" attribute 
 459         # *before* the default handlers have been installed, so 
 460         # that : takes you forward, not back, and so we can issue 
 461         # EVT_TIMEUPDATE events on changes: 
 463         self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )         ## defeat automatic full selection 
 464         self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )    ## run internal validator 
 465         self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection)    ## limit selections to single field 
 466         self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick 
 467         self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )        ## capture control events not normally seen, eg ctrl-tab. 
 468         self.Bind(wx.EVT_CHAR, self.__OnChar )              ## remove "shift" attribute from colon key event, 
 469                                                             ## then call BaseMaskedTextCtrl._OnChar with 
 470                                                             ## the possibly modified event. 
 471         self.Bind(wx.EVT_TEXT, self.__OnTextChange, self )  ## color control appropriately and EVT_TIMEUPDATE events 
 474         # Validate initial value and set if appropriate 
 476             self.SetBounds(min, max) 
 477             self.SetLimited(limited) 
 480             self.SetValue('00:00:00') 
 483             self.BindSpinButton(spinButton)     # bind spin button up/down events to this control 
 486     def SetParameters(self, **kwargs): 
 488         Function providing access to the parameters governing TimeCtrl display and bounds. 
 490 ##        dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1) 
 491         maskededit_kwargs = {} 
 494         if kwargs.has_key('display_seconds'): 
 495             kwargs['displaySeconds'] = kwargs['display_seconds'] 
 496             del kwargs['display_seconds'] 
 497         if kwargs.has_key('format') and kwargs.has_key('displaySeconds'): 
 498             del kwargs['displaySeconds']    # always apply format if specified 
 500         # assign keyword args as appropriate: 
 501         for key, param_value in kwargs.items(): 
 502             if key not in TimeCtrl.valid_ctrl_params.keys(): 
 503                 raise AttributeError('invalid keyword argument "%s"' % key) 
 506                 wxdt = wx.DateTimeFromDMY(1, 0, 1970) 
 508                     if wxdt.Format('%p') != 'AM': 
 515                 # handle both local or generic 'maskededit' autoformat codes: 
 516                 if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS': 
 517                     self.__displaySeconds = True 
 518                     self.__fmt24hr = False 
 519                 elif param_value == 'HHMM' or param_value == 'TIMEHHMM': 
 520                     self.__displaySeconds = False 
 521                     self.__fmt24hr = False 
 522                 elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS': 
 523                     self.__displaySeconds = True 
 524                     self.__fmt24hr = True 
 525                 elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM': 
 526                     self.__displaySeconds = False 
 527                     self.__fmt24hr = True 
 529                     raise AttributeError('"%s" is not a valid format' % param_value) 
 531                 if require24hr and not self.__fmt24hr: 
 532                     raise AttributeError('"%s" is an unsupported time format for the current locale' % param_value) 
 536             elif key in ("displaySeconds",  "display_seconds") and not kwargs.has_key('format'): 
 537                 self.__displaySeconds = param_value 
 540             elif key == "min":      min = param_value 
 541             elif key == "max":      max = param_value 
 542             elif key == "limited":  limited = param_value 
 544             elif key == "useFixedWidthFont": 
 545                 maskededit_kwargs[key] = param_value 
 547             elif key == "oob_color": 
 548                 maskededit_kwargs['invalidBackgroundColor'] = param_value 
 552                 if self.__displaySeconds:  maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS' 
 553                 else:                      maskededit_kwargs['autoformat'] = '24HRTIMEHHMM' 
 555                 # Set hour field to zero-pad, right-insert, require explicit field change, 
 556                 # select entire field on entry, and require a resultant valid entry 
 557                 # to allow character entry: 
 558                 hourfield = Field(formatcodes='0r<SV', validRegex='0\d|1\d|2[0123]', validRequired=True) 
 560                 if self.__displaySeconds:  maskededit_kwargs['autoformat'] = 'TIMEHHMMSS' 
 561                 else:                      maskededit_kwargs['autoformat'] = 'TIMEHHMM' 
 563                 # Set hour field to allow spaces (at start), right-insert, 
 564                 # require explicit field change, select entire field on entry, 
 565                 # and require a resultant valid entry to allow character entry: 
 566                 hourfield = Field(formatcodes='_0<rSV', validRegex='0[1-9]| [1-9]|1[012]', validRequired=True) 
 567                 ampmfield = Field(formatcodes='S', emptyInvalid = True, validRequired = True) 
 569             # Field 1 is always a zero-padded right-insert minute field, 
 570             # similarly configured as above: 
 571             minutefield = Field(formatcodes='0r<SV', validRegex='[0-5]\d', validRequired=True) 
 573             fields = [ hourfield, minutefield ] 
 574             if self.__displaySeconds: 
 575                 fields.append(copy.copy(minutefield))    # second field has same constraints as field 1 
 577             if not self.__fmt24hr: 
 578                 fields.append(ampmfield) 
 580             # set fields argument: 
 581             maskededit_kwargs['fields'] = fields 
 583             # This allows range validation if set 
 584             maskededit_kwargs['validFunc'] = self.IsInBounds 
 586             # This allows range limits to affect insertion into control or not 
 587             # dynamically without affecting individual field constraint validation 
 588             maskededit_kwargs['retainFieldValidation'] = True 
 590         if hasattr(self, 'controlInitialized') and self.controlInitialized: 
 591             self.SetCtrlParameters(**maskededit_kwargs)   # set appropriate parameters 
 593             # Validate initial value and set if appropriate 
 595                 self.SetBounds(min, max) 
 596                 self.SetLimited(limited) 
 599                 self.SetValue('00:00:00') 
 601             return {}   # no arguments to return 
 604             return maskededit_kwargs 
 607     def BindSpinButton(self, sb): 
 609         This function binds an externally created spin button to the control, so that 
 610         up/down events from the button automatically change the control. 
 612 ##        dbg('TimeCtrl::BindSpinButton') 
 613         self.__spinButton = sb 
 614         if self.__spinButton: 
 615             # bind event handlers to spin ctrl 
 616             self.__spinButton.Bind(wx.EVT_SPIN_UP, self.__OnSpinUp, self.__spinButton) 
 617             self.__spinButton.Bind(wx.EVT_SPIN_DOWN, self.__OnSpinDown, self.__spinButton) 
 621         return "<TimeCtrl: %s>" % self.GetValue() 
 624     def SetValue(self, value): 
 626         Validating SetValue function for time values: 
 627         This function will do dynamic type checking on the value argument, 
 628         and convert wxDateTime, mxDateTime, or 12/24 format time string 
 629         into the appropriate format string for the control. 
 631 ##        dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1) 
 633             strtime = self._toGUI(self.__validateValue(value)) 
 635 ##            dbg('validation failed', indent=0) 
 638 ##        dbg('strtime:', strtime) 
 639         self._SetValue(strtime) 
 643                  as_wxDateTime = False, 
 644                  as_mxDateTime = False, 
 645                  as_wxTimeSpan = False, 
 646                  as_mxDateTimeDelta = False): 
 648         This function returns the value of the display as a string by default, but 
 649         supports return as a wx.DateTime, mx.DateTime, wx.TimeSpan, or mx.DateTimeDelta, 
 650         if requested. (Evaluated in the order above-- first one wins!) 
 654         if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta: 
 655             value = self.GetWxDateTime() 
 659                 value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond()) 
 661                 value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond()) 
 662             elif as_mxDateTimeDelta: 
 663                 value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond()) 
 665             value = BaseMaskedTextCtrl.GetValue(self) 
 669     def SetWxDateTime(self, wxdt): 
 671         Because SetValue can take a wx.DateTime, this is now just an alias. 
 676     def GetWxDateTime(self, value=None): 
 678         This function is the conversion engine for TimeCtrl; it takes 
 679         one of the following types: 
 685         and converts it to a wx.DateTime that always has Jan 1, 1970 as its date 
 686         portion, so that range comparisons around values can work using 
 687         wx.DateTime's built-in comparison function.  If a value is not 
 688         provided to convert, the string value of the control will be used. 
 689         If the value is not one of the accepted types, a ValueError will be 
 694 ##        dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1) 
 696 ##            dbg('getting control value') 
 697             value = self.GetValue() 
 698 ##            dbg('value = "%s"' % value) 
 700         if type(value) == types.UnicodeType: 
 701             value = str(value)  # convert to regular string 
 703         valid = True    # assume true 
 704         if type(value) == types.StringType: 
 706             # Construct constant wxDateTime, then try to parse the string: 
 707             wxdt = wx.DateTimeFromDMY(1, 0, 1970) 
 708 ##            dbg('attempting conversion') 
 709             value = value.strip()    # (parser doesn't like leading spaces) 
 710             checkTime    = wxdt.ParseTime(value) 
 711             valid = checkTime == len(value)     # entire string parsed? 
 712 ##            dbg('checkTime == len(value)?', valid) 
 715                 # deal with bug/deficiency in wx.DateTime: 
 717                     if wxdt.Format('%p') not in ('AM', 'PM') and checkTime in (5,8): 
 718                         # couldn't parse the AM/PM field 
 719                         raise ValueError('cannot convert string "%s" to valid time for the current locale; please use 24hr time instead' % value) 
 721     ##                dbg(indent=0, suspend=0) 
 722                         raise ValueError('cannot convert string "%s" to valid time' % value) 
 724                     raise ValueError('cannot convert string "%s" to valid time for the current locale; please use 24hr time instead' % value) 
 727             if isinstance(value, wx.DateTime): 
 728                 hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond() 
 729             elif isinstance(value, wx.TimeSpan): 
 730                 totalseconds = value.GetSeconds() 
 731                 hour = totalseconds / 3600 
 732                 minute = totalseconds / 60 - (hour * 60) 
 733                 second = totalseconds - ((hour * 3600) + (minute * 60)) 
 735             elif accept_mx and isinstance(value, DateTime.DateTimeType): 
 736                 hour, minute, second = value.hour, value.minute, value.second 
 737             elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType): 
 738                 hour, minute, second = value.hour, value.minute, value.second 
 740                 # Not a valid function argument 
 742                     error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value) 
 744                     error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value) 
 745 ##                dbg(indent=0, suspend=0) 
 746                 raise ValueError(error) 
 748             wxdt = wx.DateTimeFromDMY(1, 0, 1970) 
 750             wxdt.SetMinute(minute) 
 751             wxdt.SetSecond(second) 
 753 ##        dbg('wxdt:', wxdt, indent=0, suspend=0) 
 757     def SetMxDateTime(self, mxdt): 
 759         Because SetValue can take an mx.DateTime, (if DateTime is importable), 
 760         this is now just an alias. 
 765     def GetMxDateTime(self, value=None): 
 767         Returns the value of the control as an mx.DateTime, with the date 
 768         portion set to January 1, 1970. 
 771             t = self.GetValue(as_mxDateTime=True) 
 773             # Convert string 1st to wxDateTime, then use components, since 
 774             # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM: 
 775             wxdt = self.GetWxDateTime(value) 
 776             hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond() 
 777             t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second) 
 781     def SetMin(self, min=None): 
 783         Sets the minimum value of the control.  If a value of None 
 784         is provided, then the control will have no explicit minimum value. 
 785         If the value specified is greater than the current maximum value, 
 786         then the function returns 0 and the minimum will not change from 
 787         its current setting.  On success, the function returns 1. 
 789         If successful and the current value is lower than the new lower 
 790         bound, if the control is limited, the value will be automatically 
 791         adjusted to the new minimum value; if not limited, the value in the 
 792         control will be colored as invalid. 
 794 ##        dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1) 
 797                 min = self.GetWxDateTime(min) 
 798                 self.__min = self._toGUI(min) 
 800 ##                dbg('exception occurred', indent=0) 
 805         if self.IsLimited() and not self.IsInBounds(): 
 806             self.SetLimited(self.__limited) # force limited value: 
 810 ##        dbg('ret:', ret, indent=0) 
 814     def GetMin(self, as_string = False): 
 816         Gets the minimum value of the control. 
 817         If None, it will return None.  Otherwise it will return 
 818         the current minimum bound on the control, as a wxDateTime 
 819         by default, or as a string if as_string argument is True. 
 822 ##        dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) 
 823         if self.__min is None: 
 824 ##            dbg('(min == None)') 
 831                 ret = self.GetWxDateTime(self.__min) 
 834 ##                dbg('exception occurred', indent=0) 
 836 ##            dbg('ret:', repr(ret)) 
 837 ##        dbg(indent=0, suspend=0) 
 841     def SetMax(self, max=None): 
 843         Sets the maximum value of the control. If a value of None 
 844         is provided, then the control will have no explicit maximum value. 
 845         If the value specified is less than the current minimum value, then 
 846         the function returns False and the maximum will not change from its 
 847         current setting. On success, the function returns True. 
 849         If successful and the current value is greater than the new upper 
 850         bound, if the control is limited the value will be automatically 
 851         adjusted to this maximum value; if not limited, the value in the 
 852         control will be colored as invalid. 
 854 ##        dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1) 
 857                 max = self.GetWxDateTime(max) 
 858                 self.__max = self._toGUI(max) 
 860 ##                dbg('exception occurred', indent=0) 
 864 ##        dbg('max:', repr(self.__max)) 
 865         if self.IsLimited() and not self.IsInBounds(): 
 866             self.SetLimited(self.__limited) # force limited value: 
 870 ##        dbg('ret:', ret, indent=0) 
 874     def GetMax(self, as_string = False): 
 876         Gets the minimum value of the control. 
 877         If None, it will return None.  Otherwise it will return 
 878         the current minimum bound on the control, as a wxDateTime 
 879         by default, or as a string if as_string argument is True. 
 882 ##        dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) 
 883         if self.__max is None: 
 884 ##            dbg('(max == None)') 
 891                 ret = self.GetWxDateTime(self.__max) 
 894 ##                dbg('exception occurred', indent=0) 
 896 ##            dbg('ret:', repr(ret)) 
 897 ##        dbg(indent=0, suspend=0) 
 901     def SetBounds(self, min=None, max=None): 
 903         This function is a convenience function for setting the min and max 
 904         values at the same time.  The function only applies the maximum bound 
 905         if setting the minimum bound is successful, and returns True 
 906         only if both operations succeed. 
 907         **NOTE:** leaving out an argument will remove the corresponding bound. 
 909         ret = self.SetMin(min) 
 910         return ret and self.SetMax(max) 
 913     def GetBounds(self, as_string = False): 
 915         This function returns a two-tuple (min,max), indicating the 
 916         current bounds of the control.  Each value can be None if 
 917         that bound is not set. 
 919         return (self.GetMin(as_string), self.GetMax(as_string)) 
 922     def SetLimited(self, limited): 
 924         If called with a value of True, this function will cause the control 
 925         to limit the value to fall within the bounds currently specified. 
 926         If the control's value currently exceeds the bounds, it will then 
 927         be limited accordingly. 
 929         If called with a value of 0, this function will disable value 
 930         limiting, but coloring of out-of-bounds values will still take 
 931         place if bounds have been set for the control. 
 933 ##        dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1) 
 934         self.__limited = limited 
 937             self.SetMaskParameters(validRequired = False) 
 942 ##        dbg('requiring valid value') 
 943         self.SetMaskParameters(validRequired = True) 
 947         if min is None or max is None: 
 948 ##            dbg('both bounds not set; no further action taken') 
 949             return  # can't limit without 2 bounds 
 951         elif not self.IsInBounds(): 
 952             # set value to the nearest bound: 
 954                 value = self.GetWxDateTime() 
 956 ##                dbg('exception occurred', indent=0) 
 959             if min <= max:   # valid range doesn't span midnight 
 961                 # which makes the "nearest bound" computation trickier... 
 963                 # determine how long the "invalid" pie wedge is, and cut 
 964                 # this interval in half for comparison purposes: 
 966                 # Note: relies on min and max and value date portions 
 967                 # always being the same. 
 968                 interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max 
 970                 half_interval = wx.TimeSpan( 
 973                                     interval.GetSeconds() / 2,  # seconds 
 976                 if value < min: # min is on next day, so use value on 
 977                     # "next day" for "nearest" interval calculation: 
 978                     cmp_value = value + wx.TimeSpan(24, 0, 0, 0) 
 979                 else:   # "before midnight; ok 
 982                 if (cmp_value - max) > half_interval: 
 983 ##                    dbg('forcing value to min (%s)' % min.FormatTime()) 
 986 ##                    dbg('forcing value to max (%s)' % max.FormatTime()) 
 990                 # therefore  max < value < min guaranteed to be true, 
 991                 # so "nearest bound" calculation is much easier: 
 992                 if (value - max) >= (min - value): 
 993                     # current value closer to min; pick that edge of pie wedge 
 994 ##                    dbg('forcing value to min (%s)' % min.FormatTime()) 
 997 ##                    dbg('forcing value to max (%s)' % max.FormatTime()) 
1004     def IsLimited(self): 
1006         Returns True if the control is currently limiting the 
1007         value to fall within any current bounds.  *Note:* can 
1008         be set even if there are no current bounds. 
1010         return self.__limited 
1013     def IsInBounds(self, value=None): 
1015         Returns True if no value is specified and the current value 
1016         of the control falls within the current bounds.  As the clock 
1017         is a "circle", both minimum and maximum bounds must be set for 
1018         a value to ever be considered "out of bounds".  This function can 
1019         also be called with a value to see if that value would fall within 
1020         the current bounds of the given control. 
1022         if value is not None: 
1024                 value = self.GetWxDateTime(value)   # try to regularize passed value 
1026 ##                dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0) 
1029 ##        dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1) 
1030         if self.__min is None or self.__max is None: 
1036                 value = self.GetWxDateTime() 
1038 ##                dbg('exception occurred', indent=0) 
1041 ##        dbg('value:', value.FormatTime()) 
1043         # Get wxDateTime representations of bounds: 
1047         midnight = wx.DateTimeFromDMY(1, 0, 1970) 
1048         if min <= max:   # they don't span midnight 
1049             ret = min <= value <= max 
1052             # have to break into 2 tests; to be in bounds 
1053             # either "min" <= value (<= midnight of *next day*) 
1054             # or midnight <= value <= "max" 
1055             ret = min <= value or (midnight <= value <= max) 
1056 ##        dbg('in bounds?', ret, indent=0) 
1060     def IsValid( self, value ): 
1062         Can be used to determine if a given value would be a legal and 
1063         in-bounds value for the control. 
1066             self.__validateValue(value) 
1071     def SetFormat(self, format): 
1072         self.SetParameters(format=format) 
1074     def GetFormat(self): 
1075         if self.__displaySeconds: 
1076             if self.__fmt24hr: return '24HHMMSS' 
1077             else:              return 'HHMMSS' 
1079             if self.__fmt24hr: return '24HHMM' 
1082 #------------------------------------------------------------------------------------------------------------- 
1083 # these are private functions and overrides: 
1086     def __OnTextChange(self, event=None): 
1087 ##        dbg('TimeCtrl::OnTextChange', indent=1) 
1089         # Allow Maskedtext base control to color as appropriate, 
1090         # and Skip the EVT_TEXT event (if appropriate.) 
1091         ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue() 
1092         ## call is generating two (2) EVT_TEXT events. (!) 
1093         ## The the only mechanism I can find to mask this problem is to 
1094         ## keep track of last value seen, and declare a valid EVT_TEXT 
1095         ## event iff the value has actually changed.  The masked edit 
1096         ## OnTextChange routine does this, and returns True on a valid event, 
1098         if not BaseMaskedTextCtrl._OnTextChange(self, event): 
1101 ##        dbg('firing TimeUpdatedEvent...') 
1102         evt = TimeUpdatedEvent(self.GetId(), self.GetValue()) 
1103         evt.SetEventObject(self) 
1104         self.GetEventHandler().ProcessEvent(evt) 
1108     def SetInsertionPoint(self, pos): 
1110         This override records the specified position and associated cell before 
1111         calling base class' function.  This is necessary to handle the optional 
1112         spin button, because the insertion point is lost when the focus shifts 
1115 ##        dbg('TimeCtrl::SetInsertionPoint', pos, indent=1) 
1116         BaseMaskedTextCtrl.SetInsertionPoint(self, pos)                 # (causes EVT_TEXT event to fire) 
1117         self.__posCurrent = self.GetInsertionPoint() 
1121     def SetSelection(self, sel_start, sel_to): 
1122 ##        dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1) 
1124         # Adjust selection range to legal extent if not already 
1128         if self.__posCurrent != sel_start:                      # force selection and insertion point to match 
1129             self.SetInsertionPoint(sel_start) 
1130         cell_start, cell_end = self._FindField(sel_start)._extent 
1131         if not cell_start <= sel_to <= cell_end: 
1134         self.__bSelection = sel_start != sel_to 
1135         BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to) 
1139     def __OnSpin(self, key): 
1141         This is the function that gets called in response to up/down arrow or 
1142         bound spin button events. 
1144         self.__IncrementValue(key, self.__posCurrent)   # changes the value 
1146         # Ensure adjusted control regains focus and has adjusted portion 
1149         start, end = self._FindField(self.__posCurrent)._extent 
1150         self.SetInsertionPoint(start) 
1151         self.SetSelection(start, end) 
1152 ##        dbg('current position:', self.__posCurrent) 
1155     def __OnSpinUp(self, event): 
1157         Event handler for any bound spin button on EVT_SPIN_UP; 
1158         causes control to behave as if up arrow was pressed. 
1160 ##        dbg('TimeCtrl::OnSpinUp', indent=1) 
1161         self.__OnSpin(wx.WXK_UP) 
1162         keep_processing = False 
1164         return keep_processing 
1167     def __OnSpinDown(self, event): 
1169         Event handler for any bound spin button on EVT_SPIN_DOWN; 
1170         causes control to behave as if down arrow was pressed. 
1172 ##        dbg('TimeCtrl::OnSpinDown', indent=1) 
1173         self.__OnSpin(wx.WXK_DOWN) 
1174         keep_processing = False 
1176         return keep_processing 
1179     def __OnChar(self, event): 
1181         Handler to explicitly look for ':' keyevents, and if found, 
1182         clear the m_shiftDown field, so it will behave as forward tab. 
1183         It then calls the base control's _OnChar routine with the modified 
1186 ##        dbg('TimeCtrl::OnChar', indent=1) 
1187         keycode = event.GetKeyCode() 
1188 ##        dbg('keycode:', keycode) 
1189         if keycode == ord(':'): 
1190 ##            dbg('colon seen! removing shift attribute') 
1191             event.m_shiftDown = False 
1192         BaseMaskedTextCtrl._OnChar(self, event )              ## handle each keypress 
1196     def __OnSetToNow(self, event): 
1198         This is the key handler for '!' and 'c'; this allows the user to 
1199         quickly set the value of the control to the current time. 
1201         self.SetValue(wx.DateTime_Now().FormatTime()) 
1202         keep_processing = False 
1203         return keep_processing 
1206     def __LimitSelection(self, event): 
1208         Event handler for motion events; this handler 
1209         changes limits the selection to the new cell boundaries. 
1211 ##        dbg('TimeCtrl::LimitSelection', indent=1) 
1212         pos = self.GetInsertionPoint() 
1213         self.__posCurrent = pos 
1214         sel_start, sel_to = self.GetSelection() 
1215         selection = sel_start != sel_to 
1217             # only allow selection to end of current cell: 
1218             start, end = self._FindField(sel_start)._extent 
1219             if sel_to < pos:   sel_to = start 
1220             elif sel_to > pos: sel_to = end 
1222 ##        dbg('new pos =', self.__posCurrent, 'select to ', sel_to) 
1223         self.SetInsertionPoint(self.__posCurrent) 
1224         self.SetSelection(self.__posCurrent, sel_to) 
1225         if event: event.Skip() 
1229     def __IncrementValue(self, key, pos): 
1230 ##        dbg('TimeCtrl::IncrementValue', key, pos, indent=1) 
1231         text = self.GetValue() 
1232         field = self._FindField(pos) 
1233 ##        dbg('field: ', field._index) 
1234         start, end = field._extent 
1235         slice = text[start:end] 
1236         if key == wx.WXK_UP: increment = 1 
1237         else:             increment = -1 
1239         if slice in ('A', 'P'): 
1240             if slice == 'A': newslice = 'P' 
1241             elif slice == 'P': newslice = 'A' 
1242             newvalue = text[:start] + newslice + text[end:] 
1244         elif field._index == 0: 
1245             # adjusting this field is trickier, as its value can affect the 
1246             # am/pm setting.  So, we use wxDateTime to generate a new value for us: 
1247             # (Use a fixed date not subject to DST variations:) 
1248             converter = wx.DateTimeFromDMY(1, 0, 1970) 
1249 ##            dbg('text: "%s"' % text) 
1250             converter.ParseTime(text.strip()) 
1251             currenthour = converter.GetHour() 
1252 ##            dbg('current hour:', currenthour) 
1253             newhour = (currenthour + increment) % 24 
1254 ##            dbg('newhour:', newhour) 
1255             converter.SetHour(newhour) 
1256 ##            dbg('converter.GetHour():', converter.GetHour()) 
1257             newvalue = converter     # take advantage of auto-conversion for am/pm in .SetValue() 
1259         else:   # minute or second field; handled the same way: 
1260             newslice = "%02d" % ((int(slice) + increment) % 60) 
1261             newvalue = text[:start] + newslice + text[end:] 
1264             self.SetValue(newvalue) 
1266         except ValueError:  # must not be in bounds: 
1267             if not wx.Validator_IsSilent(): 
1272     def _toGUI( self, wxdt ): 
1274         This function takes a wxdt as an unambiguous representation of a time, and 
1275         converts it to a string appropriate for the format of the control. 
1278             if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S') 
1279             else:                     strval = wxdt.Format('%H:%M') 
1281             if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p') 
1282             else:                     strval = wxdt.Format('%I:%M %p') 
1287     def __validateValue( self, value ): 
1289         This function converts the value to a wxDateTime if not already one, 
1290         does bounds checking and raises ValueError if argument is 
1291         not a valid value for the control as currently specified. 
1292         It is used by both the SetValue() and the IsValid() methods. 
1294 ##        dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1) 
1297             raise ValueError('%s not a valid time value' % repr(value)) 
1299         valid = True    # assume true 
1301             value = self.GetWxDateTime(value)   # regularize form; can generate ValueError if problem doing so 
1303 ##            dbg('exception occurred', indent=0) 
1306         if self.IsLimited() and not self.IsInBounds(value): 
1309                 'value %s is not within the bounds of the control' % str(value) ) 
1313 #---------------------------------------------------------------------------- 
1314 # Test jig for TimeCtrl: 
1316 if __name__ == '__main__': 
1319     class TestPanel(wx.Panel): 
1320         def __init__(self, parent, id, 
1321                      pos = wx.DefaultPosition, size = wx.DefaultSize, 
1322                      fmt24hr = 0, test_mx = 0, 
1323                      style = wx.TAB_TRAVERSAL ): 
1325             wx.Panel.__init__(self, parent, id, pos, size, style) 
1327             self.test_mx = test_mx 
1329             self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr) 
1330             sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 ) 
1331             self.tc.BindSpinButton(sb) 
1333             sizer = wx.BoxSizer( wx.HORIZONTAL ) 
1334             sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 ) 
1335             sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 ) 
1337             self.SetAutoLayout( True ) 
1338             self.SetSizer( sizer ) 
1340             sizer.SetSizeHints( self ) 
1342             self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc) 
1344         def OnTimeChange(self, event): 
1345 ##            dbg('OnTimeChange: value = ', event.GetValue()) 
1346             wxdt = self.tc.GetWxDateTime() 
1347 ##            dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()) 
1349                 mxdt = self.tc.GetMxDateTime() 
1350 ##                dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second) 
1353     class MyApp(wx.App): 
1356             fmt24hr = '24' in sys.argv 
1357             test_mx = 'mx' in sys.argv 
1359                 frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) ) 
1360                 panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx) 
1363                 traceback.print_exc() 
1371         traceback.print_exc() 
1375 ## ==================== 
1377 ##  1. Converted docstrings to reST format, added doc for ePyDoc. 
1378 ##  2. Renamed helper functions, vars etc. not intended to be visible in public 
1379 ##     interface to code. 
1382 ##   1. Changed parameter name display_seconds to displaySeconds, to follow 
1383 ##      other masked edit conventions. 
1384 ##   2. Added format parameter, to remove need to use both fmt24hr and displaySeconds. 
1385 ##   3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of 
1386 ##      nonsensical parameter methods from the control, so it will work 
1387 ##      properly with Boa.