]>
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) 
 368         if wxdt.Format('%p') != 'AM': 
 369             TimeCtrl.valid_ctrl_params['format'] = '24HHMMSS' 
 370             self.__fmt24hr = True 
 371             fmt24hr = True  # force/change default positional argument 
 372                             # (will countermand explicit set to False too.) 
 374         for key, param_value in TimeCtrl.valid_ctrl_params.items(): 
 375             # This is done this way to make setattr behave consistently with 
 376             # "private attribute" name mangling 
 377             setattr(self, "_TimeCtrl__" + key, copy.copy(param_value)) 
 379         # create locals from current defaults, so we can override if 
 380         # specified in kwargs, and handle uniformly: 
 383         limited = self.__limited 
 384         self.__posCurrent = 0 
 385         # handle deprecated keword argument name: 
 386         if kwargs.has_key('display_seconds'): 
 387             kwargs['displaySeconds'] = kwargs['display_seconds'] 
 388             del kwargs['display_seconds'] 
 389         if not kwargs.has_key('displaySeconds'): 
 390             kwargs['displaySeconds'] = True 
 392         # (handle positional arg (from original release) differently from rest of kwargs:) 
 393         if not kwargs.has_key('format'): 
 395                 if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: 
 396                     kwargs['format'] = '24HHMMSS' 
 397                     del kwargs['displaySeconds'] 
 399                     kwargs['format'] = '24HHMM' 
 401                 if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: 
 402                     kwargs['format'] = 'HHMMSS' 
 403                     del kwargs['displaySeconds'] 
 405                     kwargs['format'] = 'HHMM' 
 407         if not kwargs.has_key('useFixedWidthFont'): 
 408             # allow control over font selection: 
 409             kwargs['useFixedWidthFont'] = self.__useFixedWidthFont 
 411         maskededit_kwargs = self.SetParameters(**kwargs) 
 413         # allow for explicit size specification: 
 414         if size != wx.DefaultSize: 
 415             # override (and remove) "autofit" autoformat code in standard time formats: 
 416             maskededit_kwargs['formatcodes'] = 'T!' 
 418         # This allows range validation if set 
 419         maskededit_kwargs['validFunc'] = self.IsInBounds 
 421         # This allows range limits to affect insertion into control or not 
 422         # dynamically without affecting individual field constraint validation 
 423         maskededit_kwargs['retainFieldValidation'] = True 
 425         # Now we can initialize the base control: 
 426         BaseMaskedTextCtrl.__init__( 
 430                 validator = validator, 
 432                 setupEventHandling = False, 
 436         # This makes ':' act like tab (after we fix each ':' key event to remove "shift") 
 437         self._SetKeyHandler(':', self._OnChangeField) 
 440         # This makes the up/down keys act like spin button controls: 
 441         self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp) 
 442         self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown) 
 445         # This allows ! and c/C to set the control to the current time: 
 446         self._SetKeyHandler('!', self.__OnSetToNow) 
 447         self._SetKeyHandler('c', self.__OnSetToNow) 
 448         self._SetKeyHandler('C', self.__OnSetToNow) 
 451         # Set up event handling ourselves, so we can insert special 
 452         # processing on the ":' key to remove the "shift" attribute 
 453         # *before* the default handlers have been installed, so 
 454         # that : takes you forward, not back, and so we can issue 
 455         # EVT_TIMEUPDATE events on changes: 
 457         self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )         ## defeat automatic full selection 
 458         self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )    ## run internal validator 
 459         self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection)    ## limit selections to single field 
 460         self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick 
 461         self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )        ## capture control events not normally seen, eg ctrl-tab. 
 462         self.Bind(wx.EVT_CHAR, self.__OnChar )              ## remove "shift" attribute from colon key event, 
 463                                                             ## then call BaseMaskedTextCtrl._OnChar with 
 464                                                             ## the possibly modified event. 
 465         self.Bind(wx.EVT_TEXT, self.__OnTextChange, self )  ## color control appropriately and EVT_TIMEUPDATE events 
 468         # Validate initial value and set if appropriate 
 470             self.SetBounds(min, max) 
 471             self.SetLimited(limited) 
 474             self.SetValue('00:00:00') 
 477             self.BindSpinButton(spinButton)     # bind spin button up/down events to this control 
 480     def SetParameters(self, **kwargs): 
 482         Function providing access to the parameters governing TimeCtrl display and bounds. 
 484 ##        dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1) 
 485         maskededit_kwargs = {} 
 488         if kwargs.has_key('display_seconds'): 
 489             kwargs['displaySeconds'] = kwargs['display_seconds'] 
 490             del kwargs['display_seconds'] 
 491         if kwargs.has_key('format') and kwargs.has_key('displaySeconds'): 
 492             del kwargs['displaySeconds']    # always apply format if specified 
 494         # assign keyword args as appropriate: 
 495         for key, param_value in kwargs.items(): 
 496             if key not in TimeCtrl.valid_ctrl_params.keys(): 
 497                 raise AttributeError('invalid keyword argument "%s"' % key) 
 500                 wxdt = wx.DateTimeFromDMY(1, 0, 1970) 
 501                 if wxdt.Format('%p') != 'AM': 
 506                 # handle both local or generic 'maskededit' autoformat codes: 
 507                 if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS': 
 508                     self.__displaySeconds = True 
 509                     self.__fmt24hr = False 
 510                 elif param_value == 'HHMM' or param_value == 'TIMEHHMM': 
 511                     self.__displaySeconds = False 
 512                     self.__fmt24hr = False 
 513                 elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS': 
 514                     self.__displaySeconds = True 
 515                     self.__fmt24hr = True 
 516                 elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM': 
 517                     self.__displaySeconds = False 
 518                     self.__fmt24hr = True 
 520                     raise AttributeError('"%s" is not a valid format' % param_value) 
 522                 if require24hr and not self.__fmt24hr: 
 523                     raise AttributeError('"%s" is an unsupported time format for the current locale' % param_value) 
 527             elif key in ("displaySeconds",  "display_seconds") and not kwargs.has_key('format'): 
 528                 self.__displaySeconds = param_value 
 531             elif key == "min":      min = param_value 
 532             elif key == "max":      max = param_value 
 533             elif key == "limited":  limited = param_value 
 535             elif key == "useFixedWidthFont": 
 536                 maskededit_kwargs[key] = param_value 
 538             elif key == "oob_color": 
 539                 maskededit_kwargs['invalidBackgroundColor'] = param_value 
 543                 if self.__displaySeconds:  maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS' 
 544                 else:                      maskededit_kwargs['autoformat'] = '24HRTIMEHHMM' 
 546                 # Set hour field to zero-pad, right-insert, require explicit field change, 
 547                 # select entire field on entry, and require a resultant valid entry 
 548                 # to allow character entry: 
 549                 hourfield = Field(formatcodes='0r<SV', validRegex='0\d|1\d|2[0123]', validRequired=True) 
 551                 if self.__displaySeconds:  maskededit_kwargs['autoformat'] = 'TIMEHHMMSS' 
 552                 else:                      maskededit_kwargs['autoformat'] = 'TIMEHHMM' 
 554                 # Set hour field to allow spaces (at start), right-insert, 
 555                 # require explicit field change, select entire field on entry, 
 556                 # and require a resultant valid entry to allow character entry: 
 557                 hourfield = Field(formatcodes='_0<rSV', validRegex='0[1-9]| [1-9]|1[012]', validRequired=True) 
 558                 ampmfield = Field(formatcodes='S', emptyInvalid = True, validRequired = True) 
 560             # Field 1 is always a zero-padded right-insert minute field, 
 561             # similarly configured as above: 
 562             minutefield = Field(formatcodes='0r<SV', validRegex='[0-5]\d', validRequired=True) 
 564             fields = [ hourfield, minutefield ] 
 565             if self.__displaySeconds: 
 566                 fields.append(copy.copy(minutefield))    # second field has same constraints as field 1 
 568             if not self.__fmt24hr: 
 569                 fields.append(ampmfield) 
 571             # set fields argument: 
 572             maskededit_kwargs['fields'] = fields 
 574             # This allows range validation if set 
 575             maskededit_kwargs['validFunc'] = self.IsInBounds 
 577             # This allows range limits to affect insertion into control or not 
 578             # dynamically without affecting individual field constraint validation 
 579             maskededit_kwargs['retainFieldValidation'] = True 
 581         if hasattr(self, 'controlInitialized') and self.controlInitialized: 
 582             self.SetCtrlParameters(**maskededit_kwargs)   # set appropriate parameters 
 584             # Validate initial value and set if appropriate 
 586                 self.SetBounds(min, max) 
 587                 self.SetLimited(limited) 
 590                 self.SetValue('00:00:00') 
 592             return {}   # no arguments to return 
 595             return maskededit_kwargs 
 598     def BindSpinButton(self, sb): 
 600         This function binds an externally created spin button to the control, so that 
 601         up/down events from the button automatically change the control. 
 603 ##        dbg('TimeCtrl::BindSpinButton') 
 604         self.__spinButton = sb 
 605         if self.__spinButton: 
 606             # bind event handlers to spin ctrl 
 607             self.__spinButton.Bind(wx.EVT_SPIN_UP, self.__OnSpinUp, self.__spinButton) 
 608             self.__spinButton.Bind(wx.EVT_SPIN_DOWN, self.__OnSpinDown, self.__spinButton) 
 612         return "<TimeCtrl: %s>" % self.GetValue() 
 615     def SetValue(self, value): 
 617         Validating SetValue function for time values: 
 618         This function will do dynamic type checking on the value argument, 
 619         and convert wxDateTime, mxDateTime, or 12/24 format time string 
 620         into the appropriate format string for the control. 
 622 ##        dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1) 
 624             strtime = self._toGUI(self.__validateValue(value)) 
 626 ##            dbg('validation failed', indent=0) 
 629 ##        dbg('strtime:', strtime) 
 630         self._SetValue(strtime) 
 634                  as_wxDateTime = False, 
 635                  as_mxDateTime = False, 
 636                  as_wxTimeSpan = False, 
 637                  as_mxDateTimeDelta = False): 
 639         This function returns the value of the display as a string by default, but 
 640         supports return as a wx.DateTime, mx.DateTime, wx.TimeSpan, or mx.DateTimeDelta, 
 641         if requested. (Evaluated in the order above-- first one wins!) 
 645         if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta: 
 646             value = self.GetWxDateTime() 
 650                 value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond()) 
 652                 value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond()) 
 653             elif as_mxDateTimeDelta: 
 654                 value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond()) 
 656             value = BaseMaskedTextCtrl.GetValue(self) 
 660     def SetWxDateTime(self, wxdt): 
 662         Because SetValue can take a wx.DateTime, this is now just an alias. 
 667     def GetWxDateTime(self, value=None): 
 669         This function is the conversion engine for TimeCtrl; it takes 
 670         one of the following types: 
 676         and converts it to a wx.DateTime that always has Jan 1, 1970 as its date 
 677         portion, so that range comparisons around values can work using 
 678         wx.DateTime's built-in comparison function.  If a value is not 
 679         provided to convert, the string value of the control will be used. 
 680         If the value is not one of the accepted types, a ValueError will be 
 685 ##        dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1) 
 687 ##            dbg('getting control value') 
 688             value = self.GetValue() 
 689 ##            dbg('value = "%s"' % value) 
 691         if type(value) == types.UnicodeType: 
 692             value = str(value)  # convert to regular string 
 694         valid = True    # assume true 
 695         if type(value) == types.StringType: 
 697             # Construct constant wxDateTime, then try to parse the string: 
 698             wxdt = wx.DateTimeFromDMY(1, 0, 1970) 
 699 ##            dbg('attempting conversion') 
 700             value = value.strip()    # (parser doesn't like leading spaces) 
 701             checkTime    = wxdt.ParseTime(value) 
 702             valid = checkTime == len(value)     # entire string parsed? 
 703 ##            dbg('checkTime == len(value)?', valid) 
 706                 # deal with bug/deficiency in wx.DateTime: 
 707                 if wxdt.Format('%p') not in ('AM', 'PM') and checkTime in (5,8): 
 708                     # couldn't parse the AM/PM field 
 709                     raise ValueError('cannot convert string "%s" to valid time for the current locale; please use 24hr time instead' % value) 
 711 ##                dbg(indent=0, suspend=0) 
 712                     raise ValueError('cannot convert string "%s" to valid time' % value) 
 715             if isinstance(value, wx.DateTime): 
 716                 hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond() 
 717             elif isinstance(value, wx.TimeSpan): 
 718                 totalseconds = value.GetSeconds() 
 719                 hour = totalseconds / 3600 
 720                 minute = totalseconds / 60 - (hour * 60) 
 721                 second = totalseconds - ((hour * 3600) + (minute * 60)) 
 723             elif accept_mx and isinstance(value, DateTime.DateTimeType): 
 724                 hour, minute, second = value.hour, value.minute, value.second 
 725             elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType): 
 726                 hour, minute, second = value.hour, value.minute, value.second 
 728                 # Not a valid function argument 
 730                     error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value) 
 732                     error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value) 
 733 ##                dbg(indent=0, suspend=0) 
 734                 raise ValueError(error) 
 736             wxdt = wx.DateTimeFromDMY(1, 0, 1970) 
 738             wxdt.SetMinute(minute) 
 739             wxdt.SetSecond(second) 
 741 ##        dbg('wxdt:', wxdt, indent=0, suspend=0) 
 745     def SetMxDateTime(self, mxdt): 
 747         Because SetValue can take an mx.DateTime, (if DateTime is importable), 
 748         this is now just an alias. 
 753     def GetMxDateTime(self, value=None): 
 755         Returns the value of the control as an mx.DateTime, with the date 
 756         portion set to January 1, 1970. 
 759             t = self.GetValue(as_mxDateTime=True) 
 761             # Convert string 1st to wxDateTime, then use components, since 
 762             # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM: 
 763             wxdt = self.GetWxDateTime(value) 
 764             hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond() 
 765             t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second) 
 769     def SetMin(self, min=None): 
 771         Sets the minimum value of the control.  If a value of None 
 772         is provided, then the control will have no explicit minimum value. 
 773         If the value specified is greater than the current maximum value, 
 774         then the function returns 0 and the minimum will not change from 
 775         its current setting.  On success, the function returns 1. 
 777         If successful and the current value is lower than the new lower 
 778         bound, if the control is limited, the value will be automatically 
 779         adjusted to the new minimum value; if not limited, the value in the 
 780         control will be colored as invalid. 
 782 ##        dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1) 
 785                 min = self.GetWxDateTime(min) 
 786                 self.__min = self._toGUI(min) 
 788 ##                dbg('exception occurred', indent=0) 
 793         if self.IsLimited() and not self.IsInBounds(): 
 794             self.SetLimited(self.__limited) # force limited value: 
 798 ##        dbg('ret:', ret, indent=0) 
 802     def GetMin(self, as_string = False): 
 804         Gets the minimum value of the control. 
 805         If None, it will return None.  Otherwise it will return 
 806         the current minimum bound on the control, as a wxDateTime 
 807         by default, or as a string if as_string argument is True. 
 810 ##        dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) 
 811         if self.__min is None: 
 812 ##            dbg('(min == None)') 
 819                 ret = self.GetWxDateTime(self.__min) 
 822 ##                dbg('exception occurred', indent=0) 
 824 ##            dbg('ret:', repr(ret)) 
 825 ##        dbg(indent=0, suspend=0) 
 829     def SetMax(self, max=None): 
 831         Sets the maximum value of the control. If a value of None 
 832         is provided, then the control will have no explicit maximum value. 
 833         If the value specified is less than the current minimum value, then 
 834         the function returns False and the maximum will not change from its 
 835         current setting. On success, the function returns True. 
 837         If successful and the current value is greater than the new upper 
 838         bound, if the control is limited the value will be automatically 
 839         adjusted to this maximum value; if not limited, the value in the 
 840         control will be colored as invalid. 
 842 ##        dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1) 
 845                 max = self.GetWxDateTime(max) 
 846                 self.__max = self._toGUI(max) 
 848 ##                dbg('exception occurred', indent=0) 
 852 ##        dbg('max:', repr(self.__max)) 
 853         if self.IsLimited() and not self.IsInBounds(): 
 854             self.SetLimited(self.__limited) # force limited value: 
 858 ##        dbg('ret:', ret, indent=0) 
 862     def GetMax(self, as_string = False): 
 864         Gets the minimum value of the control. 
 865         If None, it will return None.  Otherwise it will return 
 866         the current minimum bound on the control, as a wxDateTime 
 867         by default, or as a string if as_string argument is True. 
 870 ##        dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) 
 871         if self.__max is None: 
 872 ##            dbg('(max == None)') 
 879                 ret = self.GetWxDateTime(self.__max) 
 882 ##                dbg('exception occurred', indent=0) 
 884 ##            dbg('ret:', repr(ret)) 
 885 ##        dbg(indent=0, suspend=0) 
 889     def SetBounds(self, min=None, max=None): 
 891         This function is a convenience function for setting the min and max 
 892         values at the same time.  The function only applies the maximum bound 
 893         if setting the minimum bound is successful, and returns True 
 894         only if both operations succeed. 
 895         **NOTE:** leaving out an argument will remove the corresponding bound. 
 897         ret = self.SetMin(min) 
 898         return ret and self.SetMax(max) 
 901     def GetBounds(self, as_string = False): 
 903         This function returns a two-tuple (min,max), indicating the 
 904         current bounds of the control.  Each value can be None if 
 905         that bound is not set. 
 907         return (self.GetMin(as_string), self.GetMax(as_string)) 
 910     def SetLimited(self, limited): 
 912         If called with a value of True, this function will cause the control 
 913         to limit the value to fall within the bounds currently specified. 
 914         If the control's value currently exceeds the bounds, it will then 
 915         be limited accordingly. 
 917         If called with a value of 0, this function will disable value 
 918         limiting, but coloring of out-of-bounds values will still take 
 919         place if bounds have been set for the control. 
 921 ##        dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1) 
 922         self.__limited = limited 
 925             self.SetMaskParameters(validRequired = False) 
 930 ##        dbg('requiring valid value') 
 931         self.SetMaskParameters(validRequired = True) 
 935         if min is None or max is None: 
 936 ##            dbg('both bounds not set; no further action taken') 
 937             return  # can't limit without 2 bounds 
 939         elif not self.IsInBounds(): 
 940             # set value to the nearest bound: 
 942                 value = self.GetWxDateTime() 
 944 ##                dbg('exception occurred', indent=0) 
 947             if min <= max:   # valid range doesn't span midnight 
 949                 # which makes the "nearest bound" computation trickier... 
 951                 # determine how long the "invalid" pie wedge is, and cut 
 952                 # this interval in half for comparison purposes: 
 954                 # Note: relies on min and max and value date portions 
 955                 # always being the same. 
 956                 interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max 
 958                 half_interval = wx.TimeSpan( 
 961                                     interval.GetSeconds() / 2,  # seconds 
 964                 if value < min: # min is on next day, so use value on 
 965                     # "next day" for "nearest" interval calculation: 
 966                     cmp_value = value + wx.TimeSpan(24, 0, 0, 0) 
 967                 else:   # "before midnight; ok 
 970                 if (cmp_value - max) > half_interval: 
 971 ##                    dbg('forcing value to min (%s)' % min.FormatTime()) 
 974 ##                    dbg('forcing value to max (%s)' % max.FormatTime()) 
 978                 # therefore  max < value < min guaranteed to be true, 
 979                 # so "nearest bound" calculation is much easier: 
 980                 if (value - max) >= (min - value): 
 981                     # current value closer to min; pick that edge of pie wedge 
 982 ##                    dbg('forcing value to min (%s)' % min.FormatTime()) 
 985 ##                    dbg('forcing value to max (%s)' % max.FormatTime()) 
 994         Returns True if the control is currently limiting the 
 995         value to fall within any current bounds.  *Note:* can 
 996         be set even if there are no current bounds. 
 998         return self.__limited 
1001     def IsInBounds(self, value=None): 
1003         Returns True if no value is specified and the current value 
1004         of the control falls within the current bounds.  As the clock 
1005         is a "circle", both minimum and maximum bounds must be set for 
1006         a value to ever be considered "out of bounds".  This function can 
1007         also be called with a value to see if that value would fall within 
1008         the current bounds of the given control. 
1010         if value is not None: 
1012                 value = self.GetWxDateTime(value)   # try to regularize passed value 
1014 ##                dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0) 
1017 ##        dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1) 
1018         if self.__min is None or self.__max is None: 
1024                 value = self.GetWxDateTime() 
1026 ##                dbg('exception occurred', indent=0) 
1029 ##        dbg('value:', value.FormatTime()) 
1031         # Get wxDateTime representations of bounds: 
1035         midnight = wx.DateTimeFromDMY(1, 0, 1970) 
1036         if min <= max:   # they don't span midnight 
1037             ret = min <= value <= max 
1040             # have to break into 2 tests; to be in bounds 
1041             # either "min" <= value (<= midnight of *next day*) 
1042             # or midnight <= value <= "max" 
1043             ret = min <= value or (midnight <= value <= max) 
1044 ##        dbg('in bounds?', ret, indent=0) 
1048     def IsValid( self, value ): 
1050         Can be used to determine if a given value would be a legal and 
1051         in-bounds value for the control. 
1054             self.__validateValue(value) 
1059     def SetFormat(self, format): 
1060         self.SetParameters(format=format) 
1062     def GetFormat(self): 
1063         if self.__displaySeconds: 
1064             if self.__fmt24hr: return '24HHMMSS' 
1065             else:              return 'HHMMSS' 
1067             if self.__fmt24hr: return '24HHMM' 
1070 #------------------------------------------------------------------------------------------------------------- 
1071 # these are private functions and overrides: 
1074     def __OnTextChange(self, event=None): 
1075 ##        dbg('TimeCtrl::OnTextChange', indent=1) 
1077         # Allow Maskedtext base control to color as appropriate, 
1078         # and Skip the EVT_TEXT event (if appropriate.) 
1079         ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue() 
1080         ## call is generating two (2) EVT_TEXT events. (!) 
1081         ## The the only mechanism I can find to mask this problem is to 
1082         ## keep track of last value seen, and declare a valid EVT_TEXT 
1083         ## event iff the value has actually changed.  The masked edit 
1084         ## OnTextChange routine does this, and returns True on a valid event, 
1086         if not BaseMaskedTextCtrl._OnTextChange(self, event): 
1089 ##        dbg('firing TimeUpdatedEvent...') 
1090         evt = TimeUpdatedEvent(self.GetId(), self.GetValue()) 
1091         evt.SetEventObject(self) 
1092         self.GetEventHandler().ProcessEvent(evt) 
1096     def SetInsertionPoint(self, pos): 
1098         This override records the specified position and associated cell before 
1099         calling base class' function.  This is necessary to handle the optional 
1100         spin button, because the insertion point is lost when the focus shifts 
1103 ##        dbg('TimeCtrl::SetInsertionPoint', pos, indent=1) 
1104         BaseMaskedTextCtrl.SetInsertionPoint(self, pos)                 # (causes EVT_TEXT event to fire) 
1105         self.__posCurrent = self.GetInsertionPoint() 
1109     def SetSelection(self, sel_start, sel_to): 
1110 ##        dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1) 
1112         # Adjust selection range to legal extent if not already 
1116         if self.__posCurrent != sel_start:                      # force selection and insertion point to match 
1117             self.SetInsertionPoint(sel_start) 
1118         cell_start, cell_end = self._FindField(sel_start)._extent 
1119         if not cell_start <= sel_to <= cell_end: 
1122         self.__bSelection = sel_start != sel_to 
1123         BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to) 
1127     def __OnSpin(self, key): 
1129         This is the function that gets called in response to up/down arrow or 
1130         bound spin button events. 
1132         self.__IncrementValue(key, self.__posCurrent)   # changes the value 
1134         # Ensure adjusted control regains focus and has adjusted portion 
1137         start, end = self._FindField(self.__posCurrent)._extent 
1138         self.SetInsertionPoint(start) 
1139         self.SetSelection(start, end) 
1140 ##        dbg('current position:', self.__posCurrent) 
1143     def __OnSpinUp(self, event): 
1145         Event handler for any bound spin button on EVT_SPIN_UP; 
1146         causes control to behave as if up arrow was pressed. 
1148 ##        dbg('TimeCtrl::OnSpinUp', indent=1) 
1149         self.__OnSpin(wx.WXK_UP) 
1150         keep_processing = False 
1152         return keep_processing 
1155     def __OnSpinDown(self, event): 
1157         Event handler for any bound spin button on EVT_SPIN_DOWN; 
1158         causes control to behave as if down arrow was pressed. 
1160 ##        dbg('TimeCtrl::OnSpinDown', indent=1) 
1161         self.__OnSpin(wx.WXK_DOWN) 
1162         keep_processing = False 
1164         return keep_processing 
1167     def __OnChar(self, event): 
1169         Handler to explicitly look for ':' keyevents, and if found, 
1170         clear the m_shiftDown field, so it will behave as forward tab. 
1171         It then calls the base control's _OnChar routine with the modified 
1174 ##        dbg('TimeCtrl::OnChar', indent=1) 
1175         keycode = event.GetKeyCode() 
1176 ##        dbg('keycode:', keycode) 
1177         if keycode == ord(':'): 
1178 ##            dbg('colon seen! removing shift attribute') 
1179             event.m_shiftDown = False 
1180         BaseMaskedTextCtrl._OnChar(self, event )              ## handle each keypress 
1184     def __OnSetToNow(self, event): 
1186         This is the key handler for '!' and 'c'; this allows the user to 
1187         quickly set the value of the control to the current time. 
1189         self.SetValue(wx.DateTime_Now().FormatTime()) 
1190         keep_processing = False 
1191         return keep_processing 
1194     def __LimitSelection(self, event): 
1196         Event handler for motion events; this handler 
1197         changes limits the selection to the new cell boundaries. 
1199 ##        dbg('TimeCtrl::LimitSelection', indent=1) 
1200         pos = self.GetInsertionPoint() 
1201         self.__posCurrent = pos 
1202         sel_start, sel_to = self.GetSelection() 
1203         selection = sel_start != sel_to 
1205             # only allow selection to end of current cell: 
1206             start, end = self._FindField(sel_start)._extent 
1207             if sel_to < pos:   sel_to = start 
1208             elif sel_to > pos: sel_to = end 
1210 ##        dbg('new pos =', self.__posCurrent, 'select to ', sel_to) 
1211         self.SetInsertionPoint(self.__posCurrent) 
1212         self.SetSelection(self.__posCurrent, sel_to) 
1213         if event: event.Skip() 
1217     def __IncrementValue(self, key, pos): 
1218 ##        dbg('TimeCtrl::IncrementValue', key, pos, indent=1) 
1219         text = self.GetValue() 
1220         field = self._FindField(pos) 
1221 ##        dbg('field: ', field._index) 
1222         start, end = field._extent 
1223         slice = text[start:end] 
1224         if key == wx.WXK_UP: increment = 1 
1225         else:             increment = -1 
1227         if slice in ('A', 'P'): 
1228             if slice == 'A': newslice = 'P' 
1229             elif slice == 'P': newslice = 'A' 
1230             newvalue = text[:start] + newslice + text[end:] 
1232         elif field._index == 0: 
1233             # adjusting this field is trickier, as its value can affect the 
1234             # am/pm setting.  So, we use wxDateTime to generate a new value for us: 
1235             # (Use a fixed date not subject to DST variations:) 
1236             converter = wx.DateTimeFromDMY(1, 0, 1970) 
1237 ##            dbg('text: "%s"' % text) 
1238             converter.ParseTime(text.strip()) 
1239             currenthour = converter.GetHour() 
1240 ##            dbg('current hour:', currenthour) 
1241             newhour = (currenthour + increment) % 24 
1242 ##            dbg('newhour:', newhour) 
1243             converter.SetHour(newhour) 
1244 ##            dbg('converter.GetHour():', converter.GetHour()) 
1245             newvalue = converter     # take advantage of auto-conversion for am/pm in .SetValue() 
1247         else:   # minute or second field; handled the same way: 
1248             newslice = "%02d" % ((int(slice) + increment) % 60) 
1249             newvalue = text[:start] + newslice + text[end:] 
1252             self.SetValue(newvalue) 
1254         except ValueError:  # must not be in bounds: 
1255             if not wx.Validator_IsSilent(): 
1260     def _toGUI( self, wxdt ): 
1262         This function takes a wxdt as an unambiguous representation of a time, and 
1263         converts it to a string appropriate for the format of the control. 
1266             if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S') 
1267             else:                     strval = wxdt.Format('%H:%M') 
1269             if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p') 
1270             else:                     strval = wxdt.Format('%I:%M %p') 
1275     def __validateValue( self, value ): 
1277         This function converts the value to a wxDateTime if not already one, 
1278         does bounds checking and raises ValueError if argument is 
1279         not a valid value for the control as currently specified. 
1280         It is used by both the SetValue() and the IsValid() methods. 
1282 ##        dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1) 
1285             raise ValueError('%s not a valid time value' % repr(value)) 
1287         valid = True    # assume true 
1289             value = self.GetWxDateTime(value)   # regularize form; can generate ValueError if problem doing so 
1291 ##            dbg('exception occurred', indent=0) 
1294         if self.IsLimited() and not self.IsInBounds(value): 
1297                 'value %s is not within the bounds of the control' % str(value) ) 
1301 #---------------------------------------------------------------------------- 
1302 # Test jig for TimeCtrl: 
1304 if __name__ == '__main__': 
1307     class TestPanel(wx.Panel): 
1308         def __init__(self, parent, id, 
1309                      pos = wx.DefaultPosition, size = wx.DefaultSize, 
1310                      fmt24hr = 0, test_mx = 0, 
1311                      style = wx.TAB_TRAVERSAL ): 
1313             wx.Panel.__init__(self, parent, id, pos, size, style) 
1315             self.test_mx = test_mx 
1317             self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr) 
1318             sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 ) 
1319             self.tc.BindSpinButton(sb) 
1321             sizer = wx.BoxSizer( wx.HORIZONTAL ) 
1322             sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 ) 
1323             sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 ) 
1325             self.SetAutoLayout( True ) 
1326             self.SetSizer( sizer ) 
1328             sizer.SetSizeHints( self ) 
1330             self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc) 
1332         def OnTimeChange(self, event): 
1333 ##            dbg('OnTimeChange: value = ', event.GetValue()) 
1334             wxdt = self.tc.GetWxDateTime() 
1335 ##            dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()) 
1337                 mxdt = self.tc.GetMxDateTime() 
1338 ##                dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second) 
1341     class MyApp(wx.App): 
1344             fmt24hr = '24' in sys.argv 
1345             test_mx = 'mx' in sys.argv 
1347                 frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) ) 
1348                 panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx) 
1351                 traceback.print_exc() 
1359         traceback.print_exc() 
1363 ## ==================== 
1365 ##  1. Converted docstrings to reST format, added doc for ePyDoc. 
1366 ##  2. Renamed helper functions, vars etc. not intended to be visible in public 
1367 ##     interface to code. 
1370 ##   1. Changed parameter name display_seconds to displaySeconds, to follow 
1371 ##      other masked edit conventions. 
1372 ##   2. Added format parameter, to remove need to use both fmt24hr and displaySeconds. 
1373 ##   3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of 
1374 ##      nonsensical parameter methods from the control, so it will work 
1375 ##      properly with Boa.