]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/masked/timectrl.py
   1 #---------------------------------------------------------------------------- 
   5 # Copyright:    (c) 2002 by Will Sadkin, 2002 
   7 # License:      wxWindows license 
   8 #---------------------------------------------------------------------------- 
  10 #   This was written way it is because of the lack of masked edit controls 
  11 #   in wxWindows/wxPython.  I would also have preferred to derive this 
  12 #   control from a wxSpinCtrl rather than wxTextCtrl, but the wxTextCtrl 
  13 #   component of that control is inaccessible through the interface exposed in 
  16 #   TimeCtrl does not use validators, because it does careful manipulation 
  17 #   of the cursor in the text window on each keystroke, and validation is 
  18 #   cursor-position specific, so the control intercepts the key codes before the 
  19 #   validator would fire. 
  21 #   TimeCtrl now also supports .SetValue() with either strings or wxDateTime 
  22 #   values, as well as range limits, with the option of either enforcing them 
  23 #   or simply coloring the text of the control if the limits are exceeded. 
  25 #   Note: this class now makes heavy use of wxDateTime for parsing and 
  26 #   regularization, but it always does so with ephemeral instances of 
  27 #   wxDateTime, as the C++/Python validity of these instances seems to not 
  28 #   persist.  Because "today" can be a day for which an hour can "not exist" 
  29 #   or be counted twice (1 day each per year, for DST adjustments), the date 
  30 #   portion of all wxDateTimes used/returned have their date portion set to 
  31 #   Jan 1, 1970 (the "epoch.") 
  32 #---------------------------------------------------------------------------- 
  33 # 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) 
  35 # o Updated for V2.5 compatability 
  36 # o wx.SpinCtl has some issues that cause the control to 
  37 #   lock up. Noted in other places using it too, it's not this module 
  40 # 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) 
  42 # o wxMaskedTextCtrl -> masked.TextCtrl 
  43 # o wxTimeCtrl -> masked.TimeCtrl 
  48 <B>TimeCtrl</B> provides a multi-cell control that allows manipulation of a time 
  49 value.  It supports 12 or 24 hour format, and you can use wxDateTime or mxDateTime 
  50 to get/set values from the control. 
  52 Left/right/tab keys to switch cells within a TimeCtrl, and the up/down arrows act 
  53 like a spin control.  TimeCtrl also allows for an actual spin button to be attached 
  54 to the control, so that it acts like the up/down arrow keys. 
  56 The <B>!</B> or <B>c</B> key sets the value of the control to the current time. 
  58 Here's the API for TimeCtrl: 
  62          <B>value</B> = '12:00:00 AM', 
  63          pos = wx.DefaultPosition, 
  64          size = wx.DefaultSize, 
  65          <B>style</B> = wxTE_PROCESS_TAB, 
  66          <B>validator</B> = wx.DefaultValidator, 
  68          <B>format</B> = 'HHMMSS', 
  69          <B>fmt24hr</B> = False, 
  70          <B>displaySeconds</B> = True, 
  71          <B>spinButton</B> = None, 
  74          <B>limited</B> = None, 
  75          <B>oob_color</B> = "Yellow" 
  80     <DD>If no initial value is set, the default will be midnight; if an illegal string 
  81     is specified, a ValueError will result.  (You can always later set the initial time 
  82     with SetValue() after instantiation of the control.) 
  84     <DD>The size of the control will be automatically adjusted for 12/24 hour format 
  85     if wx.DefaultSize is specified. 
  87     <DD>By default, TimeCtrl will process TAB events, by allowing tab to the 
  88     different cells within the control. 
  90     <DD>By default, TimeCtrl just uses the default (empty) validator, as all 
  91     of its validation for entry control is handled internally.  However, a validator 
  92     can be supplied to provide data transfer capability to the control. 
  95     <DD>This parameter can be used instead of the fmt24hr and displaySeconds 
  96     parameters, respectively; it provides a shorthand way to specify the time 
  97     format you want.  Accepted values are 'HHMMSS', 'HHMM', '24HHMMSS', and 
  98     '24HHMM'.  If the format is specified, the other two  arguments will be ignored. 
 101     <DD>If True, control will display time in 24 hour time format; if False, it will 
 102     use 12 hour AM/PM format.  SetValue() will adjust values accordingly for the 
 103     control, based on the format specified.  (This value is ignored if the <i>format</i> 
 104     parameter is specified.) 
 106     <DT><B>displaySeconds</B> 
 107     <DD>If True, control will include a seconds field; if False, it will 
 108     just show hours and minutes. (This value is ignored if the <i>format</i> 
 109     parameter is specified.) 
 111     <DT><B>spinButton</B> 
 112     <DD>If specified, this button's events will be bound to the behavior of the 
 113     TimeCtrl, working like up/down cursor key events.  (See BindSpinButton.) 
 116     <DD>Defines the lower bound for "valid" selections in the control. 
 117     By default, TimeCtrl doesn't have bounds.  You must set both upper and lower 
 118     bounds to make the control pay attention to them, (as only one bound makes no sense 
 119     with times.) "Valid" times will fall between the min and max "pie wedge" of the 
 122     <DD>Defines the upper bound for "valid" selections in the control. 
 123     "Valid" times will fall between the min and max "pie wedge" of the 
 124     clock. (This can be a "big piece", ie. <b>min = 11pm, max= 10pm</b> 
 125     means <I>all but the hour from 10:00pm to 11pm are valid times.</I>) 
 127     <DD>If True, the control will not permit entry of values that fall outside the 
 131     <DD>Sets the background color used to indicate out-of-bounds values for the control 
 132     when the control is not limited.  This is set to "Yellow" by default. 
 138 <DT><B>EVT_TIMEUPDATE(win, id, func)</B> 
 139 <DD>func is fired whenever the value of the control changes. 
 142 <DT><B>SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta)</B> 
 143 <DD>Sets the value of the control to a particular time, given a valid 
 144 value; raises ValueError on invalid value. 
 145 <EM>NOTE:</EM> This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime 
 146 was successfully imported by the class module. 
 148 <DT><B>GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False)</B> 
 149 <DD>Retrieves the value of the time from the control.  By default this is 
 150 returned as a string, unless one of the other arguments is set; args are 
 151 searched in the order listed; only one value will be returned. 
 153 <DT><B>GetWxDateTime(value=None)</B> 
 154 <DD>When called without arguments, retrieves the value of the control, and applies 
 155 it to the wxDateTimeFromHMS() constructor, and returns the resulting value. 
 156 The date portion will always be set to Jan 1, 1970. This form is the same 
 157 as GetValue(as_wxDateTime=True).  GetWxDateTime can also be called with any of the 
 158 other valid time formats settable with SetValue, to regularize it to a single 
 159 wxDateTime form.  The function will raise ValueError on an unconvertable argument. 
 161 <DT><B>GetMxDateTime()</B> 
 162 <DD>Retrieves the value of the control and applies it to the DateTime.Time() 
 163 constructor,and returns the resulting value.  (The date portion will always be 
 164 set to Jan 1, 1970.) (Same as GetValue(as_wxDateTime=True); provided for backward 
 165 compatibility with previous release.) 
 168 <DT><B>BindSpinButton(SpinBtton)</B> 
 169 <DD>Binds an externally created spin button to the control, so that up/down spin 
 170 events change the active cell or selection in the control (in addition to the 
 171 up/down cursor keys.)  (This is primarily to allow you to create a "standard" 
 172 interface to time controls, as seen in Windows.) 
 175 <DT><B>SetMin(min=None)</B> 
 176 <DD>Sets the expected minimum value, or lower bound, of the control. 
 177 (The lower bound will only be enforced if the control is 
 178 configured to limit its values to the set bounds.) 
 179 If a value of <I>None</I> is provided, then the control will have 
 180 explicit lower bound.  If the value specified is greater than 
 181 the current lower bound, then the function returns False and the 
 182 lower bound will not change from its current setting.  On success, 
 183 the function returns True.  Even if set, if there is no corresponding 
 184 upper bound, the control will behave as if it is unbounded. 
 185 <DT><DD>If successful and the current value is outside the 
 186 new bounds, if the control is limited the value will be 
 187 automatically adjusted to the nearest bound; if not limited, 
 188 the background of the control will be colored with the current 
 191 <DT><B>GetMin(as_string=False)</B> 
 192 <DD>Gets the current lower bound value for the control, returning 
 193 None, if not set, or a wxDateTime, unless the as_string parameter 
 194 is set to True, at which point it will return the string 
 195 representation of the lower bound. 
 198 <DT><B>SetMax(max=None)</B> 
 199 <DD>Sets the expected maximum value, or upper bound, of the control. 
 200 (The upper bound will only be enforced if the control is 
 201 configured to limit its values to the set bounds.) 
 202 If a value of <I>None</I> is provided, then the control will 
 203 have no explicit upper bound.  If the value specified is less 
 204 than the current lower bound, then the function returns False and 
 205 the maximum will not change from its current setting. On success, 
 206 the function returns True.  Even if set, if there is no corresponding 
 207 lower bound, the control will behave as if it is unbounded. 
 208 <DT><DD>If successful and the current value is outside the 
 209 new bounds, if the control is limited the value will be 
 210 automatically adjusted to the nearest bound; if not limited, 
 211 the background of the control will be colored with the current 
 214 <DT><B>GetMax(as_string = False)</B> 
 215 <DD>Gets the current upper bound value for the control, returning 
 216 None, if not set, or a wxDateTime, unless the as_string parameter 
 217 is set to True, at which point it will return the string 
 218 representation of the lower bound. 
 222 <DT><B>SetBounds(min=None,max=None)</B> 
 223 <DD>This function is a convenience function for setting the min and max 
 224 values at the same time.  The function only applies the maximum bound 
 225 if setting the minimum bound is successful, and returns True 
 226 only if both operations succeed.  <B><I>Note: leaving out an argument 
 227 will remove the corresponding bound, and result in the behavior of 
 228 an unbounded control.</I></B> 
 230 <DT><B>GetBounds(as_string = False)</B> 
 231 <DD>This function returns a two-tuple (min,max), indicating the 
 232 current bounds of the control.  Each value can be None if 
 233 that bound is not set.  The values will otherwise be wxDateTimes 
 234 unless the as_string argument is set to True, at which point they 
 235 will be returned as string representations of the bounds. 
 238 <DT><B>IsInBounds(value=None)</B> 
 239 <DD>Returns <I>True</I> if no value is specified and the current value 
 240 of the control falls within the current bounds.  This function can also 
 241 be called with a value to see if that value would fall within the current 
 242 bounds of the given control.  It will raise ValueError if the value 
 243 specified is not a wxDateTime, mxDateTime (if available) or parsable string. 
 246 <DT><B>IsValid(value)</B> 
 247 <DD>Returns <I>True</I>if specified value is a legal time value and 
 248 falls within the current bounds of the given control. 
 251 <DT><B>SetLimited(bool)</B> 
 252 <DD>If called with a value of True, this function will cause the control 
 253 to limit the value to fall within the bounds currently specified. 
 254 (Provided both bounds have been set.) 
 255 If the control's value currently exceeds the bounds, it will then 
 256 be set to the nearest bound. 
 257 If called with a value of False, this function will disable value 
 258 limiting, but coloring of out-of-bounds values will still take 
 259 place if bounds have been set for the control. 
 260 <DT><B>IsLimited()</B> 
 261 <DD>Returns <I>True</I> if the control is currently limiting the 
 262 value to fall within the current bounds. 
 274 from wx
.tools
.dbg 
import Logger
 
 275 from wx
.lib
.masked 
import Field
, BaseMaskedTextCtrl
 
 281     from mx 
import DateTime
 
 286 # This class of event fires whenever the value of the time changes in the control: 
 287 wxEVT_TIMEVAL_UPDATED 
= wx
.NewEventType() 
 288 EVT_TIMEUPDATE 
= wx
.PyEventBinder(wxEVT_TIMEVAL_UPDATED
, 1) 
 290 class TimeUpdatedEvent(wx
.PyCommandEvent
): 
 291     def __init__(self
, id, value 
='12:00:00 AM'): 
 292         wx
.PyCommandEvent
.__init
__(self
, wxEVT_TIMEVAL_UPDATED
, id) 
 295         """Retrieve the value of the time control at the time this event was generated""" 
 298 class TimeCtrlAccessorsMixin
: 
 299     # Define TimeCtrl's list of attributes having their own 
 300     # Get/Set functions, ignoring those that make no sense for 
 301     # an numeric control. 
 302     exposed_basectrl_params 
= ( 
 307          'emptyBackgroundColour', 
 308          'validBackgroundColour', 
 309          'invalidBackgroundColour', 
 314     for param 
in exposed_basectrl_params
: 
 315         propname 
= param
[0].upper() + param
[1:] 
 316         exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname
, param
)) 
 317         exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) 
 319         if param.find('Colour
') != -1: 
 320             # add non-british spellings, for backward-compatibility 
 321             propname.replace('Colour
', 'Color
') 
 323             exec('def Set
%s(self
, value
): self
.SetCtrlParameters(%s=value
)' % (propname, param)) 
 324             exec('def Get
%s(self
): return self
.GetCtrlParameter("%s")''' % (propname, param)) 
 327 class TimeCtrl(BaseMaskedTextCtrl): 
 329     valid_ctrl_params = { 
 330         'format' :  'HHMMSS',       # default format code 
 331         'displaySeconds' : True,    # by default, shows seconds 
 332         'min': None,                # by default, no bounds set 
 334         'limited': False,           # by default, no limiting even if bounds set 
 335         'useFixedWidthFont': True,  # by default, use a fixed-width font 
 336         'oob_color': "Yellow"       # by default, the default masked.TextCtrl "invalid" color 
 340                 self, parent, id=-1, value = '12:00:00 AM', 
 341                 pos = wx.DefaultPosition, size = wx.DefaultSize, 
 344                 style = wx.TE_PROCESS_TAB, 
 345                 validator = wx.DefaultValidator, 
 349         # set defaults for control: 
 350 ##        dbg('setting defaults:') 
 351         for key, param_value in TimeCtrl.valid_ctrl_params.items(): 
 352             # This is done this way to make setattr behave consistently with 
 353             # "private attribute" name mangling 
 354             setattr(self, "_TimeCtrl__" + key, copy.copy(param_value)) 
 356         # create locals from current defaults, so we can override if 
 357         # specified in kwargs, and handle uniformly: 
 360         limited = self.__limited 
 361         self.__posCurrent = 0 
 362         # handle deprecated keword argument name: 
 363         if kwargs.has_key('display_seconds'): 
 364             kwargs['displaySeconds'] = kwargs['display_seconds'] 
 365             del kwargs['display_seconds'] 
 366         if not kwargs.has_key('displaySeconds'): 
 367             kwargs['displaySeconds'] = True 
 369         # (handle positional arg (from original release) differently from rest of kwargs:) 
 370         self.__fmt24hr = False 
 371         if not kwargs.has_key('format'): 
 373                 if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: 
 374                     kwargs['format'] = '24HHMMSS' 
 375                     del kwargs['displaySeconds'] 
 377                     kwargs['format'] = '24HHMM' 
 379                 if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: 
 380                     kwargs['format'] = 'HHMMSS' 
 381                     del kwargs['displaySeconds'] 
 383                     kwargs['format'] = 'HHMM' 
 385         if not kwargs.has_key('useFixedWidthFont'): 
 386             # allow control over font selection: 
 387             kwargs['useFixedWidthFont'] = self.__useFixedWidthFont 
 389         maskededit_kwargs = self.SetParameters(**kwargs) 
 391         # allow for explicit size specification: 
 392         if size != wx.DefaultSize: 
 393             # override (and remove) "autofit" autoformat code in standard time formats: 
 394             maskededit_kwargs['formatcodes'] = 'T!' 
 396         # This allows range validation if set 
 397         maskededit_kwargs['validFunc'] = self.IsInBounds 
 399         # This allows range limits to affect insertion into control or not 
 400         # dynamically without affecting individual field constraint validation 
 401         maskededit_kwargs['retainFieldValidation'] = True 
 403         # Now we can initialize the base control: 
 404         BaseMaskedTextCtrl.__init__( 
 408                 validator = validator, 
 410                 setupEventHandling = False, 
 414         # This makes ':' act like tab (after we fix each ':' key event to remove "shift") 
 415         self._SetKeyHandler(':', self._OnChangeField) 
 418         # This makes the up/down keys act like spin button controls: 
 419         self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp) 
 420         self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown) 
 423         # This allows ! and c/C to set the control to the current time: 
 424         self._SetKeyHandler('!', self.__OnSetToNow) 
 425         self._SetKeyHandler('c', self.__OnSetToNow) 
 426         self._SetKeyHandler('C', self.__OnSetToNow) 
 429         # Set up event handling ourselves, so we can insert special 
 430         # processing on the ":' key to remove the "shift" attribute 
 431         # *before* the default handlers have been installed, so 
 432         # that : takes you forward, not back, and so we can issue 
 433         # EVT_TIMEUPDATE events on changes: 
 435         self.Bind(wx.EVT_SET_FOCUS, self._OnFocus )         ## defeat automatic full selection 
 436         self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus )    ## run internal validator 
 437         self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection)    ## limit selections to single field 
 438         self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick 
 439         self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown )        ## capture control events not normally seen, eg ctrl-tab. 
 440         self.Bind(wx.EVT_CHAR, self.__OnChar )              ## remove "shift" attribute from colon key event, 
 441                                                             ## then call BaseMaskedTextCtrl._OnChar with 
 442                                                             ## the possibly modified event. 
 443         self.Bind(wx.EVT_TEXT, self.__OnTextChange, self )  ## color control appropriately and EVT_TIMEUPDATE events 
 446         # Validate initial value and set if appropriate 
 448             self.SetBounds(min, max) 
 449             self.SetLimited(limited) 
 452             self.SetValue('12:00:00 AM') 
 455             self.BindSpinButton(spinButton)     # bind spin button up/down events to this control 
 458     def SetParameters(self, **kwargs): 
 459 ##        dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1) 
 460         maskededit_kwargs = {} 
 463         if kwargs.has_key('display_seconds'): 
 464             kwargs['displaySeconds'] = kwargs['display_seconds'] 
 465             del kwargs['display_seconds'] 
 466         if kwargs.has_key('format') and kwargs.has_key('displaySeconds'): 
 467             del kwargs['displaySeconds']    # always apply format if specified 
 469         # assign keyword args as appropriate: 
 470         for key, param_value in kwargs.items(): 
 471             if key not in TimeCtrl.valid_ctrl_params.keys(): 
 472                 raise AttributeError('invalid keyword argument "%s"' % key) 
 475                 # handle both local or generic 'maskededit' autoformat codes: 
 476                 if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS': 
 477                     self.__displaySeconds = True 
 478                     self.__fmt24hr = False 
 479                 elif param_value == 'HHMM' or param_value == 'TIMEHHMM': 
 480                     self.__displaySeconds = False 
 481                     self.__fmt24hr = False 
 482                 elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS': 
 483                     self.__displaySeconds = True 
 484                     self.__fmt24hr = True 
 485                 elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM': 
 486                     self.__displaySeconds = False 
 487                     self.__fmt24hr = True 
 489                     raise AttributeError('"%s" is not a valid format' % param_value) 
 492             elif key in ("displaySeconds",  "display_seconds") and not kwargs.has_key('format'): 
 493                 self.__displaySeconds = param_value 
 496             elif key == "min":      min = param_value 
 497             elif key == "max":      max = param_value 
 498             elif key == "limited":  limited = param_value 
 500             elif key == "useFixedWidthFont": 
 501                 maskededit_kwargs[key] = param_value 
 503             elif key == "oob_color": 
 504                 maskededit_kwargs['invalidBackgroundColor'] = param_value 
 508                 if self.__displaySeconds:  maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS' 
 509                 else:                      maskededit_kwargs['autoformat'] = '24HRTIMEHHMM' 
 511                 # Set hour field to zero-pad, right-insert, require explicit field change, 
 512                 # select entire field on entry, and require a resultant valid entry 
 513                 # to allow character entry: 
 514                 hourfield = Field(formatcodes='0r<SV', validRegex='0\d|1\d|2[0123]', validRequired=True) 
 516                 if self.__displaySeconds:  maskededit_kwargs['autoformat'] = 'TIMEHHMMSS' 
 517                 else:                      maskededit_kwargs['autoformat'] = 'TIMEHHMM' 
 519                 # Set hour field to allow spaces (at start), right-insert, 
 520                 # require explicit field change, select entire field on entry, 
 521                 # and require a resultant valid entry to allow character entry: 
 522                 hourfield = Field(formatcodes='_0<rSV', validRegex='0[1-9]| [1-9]|1[012]', validRequired=True) 
 523                 ampmfield = Field(formatcodes='S', emptyInvalid = True, validRequired = True) 
 525             # Field 1 is always a zero-padded right-insert minute field, 
 526             # similarly configured as above: 
 527             minutefield = Field(formatcodes='0r<SV', validRegex='[0-5]\d', validRequired=True) 
 529             fields = [ hourfield, minutefield ] 
 530             if self.__displaySeconds: 
 531                 fields.append(copy.copy(minutefield))    # second field has same constraints as field 1 
 533             if not self.__fmt24hr: 
 534                 fields.append(ampmfield) 
 536             # set fields argument: 
 537             maskededit_kwargs['fields'] = fields 
 539             # This allows range validation if set 
 540             maskededit_kwargs['validFunc'] = self.IsInBounds 
 542             # This allows range limits to affect insertion into control or not 
 543             # dynamically without affecting individual field constraint validation 
 544             maskededit_kwargs['retainFieldValidation'] = True 
 546         if hasattr(self, 'controlInitialized') and self.controlInitialized: 
 547             self.SetCtrlParameters(**maskededit_kwargs)   # set appropriate parameters 
 549             # Validate initial value and set if appropriate 
 551                 self.SetBounds(min, max) 
 552                 self.SetLimited(limited) 
 555                 self.SetValue('12:00:00 AM') 
 557             return {}   # no arguments to return 
 560             return maskededit_kwargs 
 563     def BindSpinButton(self, sb): 
 565         This function binds an externally created spin button to the control, so that 
 566         up/down events from the button automatically change the control. 
 568 ##        dbg('TimeCtrl::BindSpinButton') 
 569         self.__spinButton = sb 
 570         if self.__spinButton: 
 571             # bind event handlers to spin ctrl 
 572             self.__spinButton.Bind(wx.EVT_SPIN_UP, self.__OnSpinUp, self.__spinButton) 
 573             self.__spinButton.Bind(wx.EVT_SPIN_DOWN, self.__OnSpinDown, self.__spinButton) 
 577         return "<TimeCtrl: %s>" % self.GetValue() 
 580     def SetValue(self, value): 
 582         Validating SetValue function for time values: 
 583         This function will do dynamic type checking on the value argument, 
 584         and convert wxDateTime, mxDateTime, or 12/24 format time string 
 585         into the appropriate format string for the control. 
 587 ##        dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1) 
 589             strtime = self._toGUI(self.__validateValue(value)) 
 591 ##            dbg('validation failed', indent=0) 
 594 ##        dbg('strtime:', strtime) 
 595         self._SetValue(strtime) 
 599                  as_wxDateTime = False, 
 600                  as_mxDateTime = False, 
 601                  as_wxTimeSpan = False, 
 602                  as_mxDateTimeDelta = False): 
 605         if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta: 
 606             value = self.GetWxDateTime() 
 610                 value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond()) 
 612                 value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond()) 
 613             elif as_mxDateTimeDelta: 
 614                 value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond()) 
 616             value = BaseMaskedTextCtrl.GetValue(self) 
 620     def SetWxDateTime(self, wxdt): 
 622         Because SetValue can take a wxDateTime, this is now just an alias. 
 627     def GetWxDateTime(self, value=None): 
 629         This function is the conversion engine for TimeCtrl; it takes 
 630         one of the following types: 
 636         and converts it to a wxDateTime that always has Jan 1, 1970 as its date 
 637         portion, so that range comparisons around values can work using 
 638         wxDateTime's built-in comparison function.  If a value is not 
 639         provided to convert, the string value of the control will be used. 
 640         If the value is not one of the accepted types, a ValueError will be 
 645 ##        dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1) 
 647 ##            dbg('getting control value') 
 648             value = self.GetValue() 
 649 ##            dbg('value = "%s"' % value) 
 651         if type(value) == types.UnicodeType: 
 652             value = str(value)  # convert to regular string 
 654         valid = True    # assume true 
 655         if type(value) == types.StringType: 
 657             # Construct constant wxDateTime, then try to parse the string: 
 658             wxdt = wx.DateTimeFromDMY(1, 0, 1970) 
 659 ##            dbg('attempting conversion') 
 660             value = value.strip()    # (parser doesn't like leading spaces) 
 661             checkTime    = wxdt.ParseTime(value) 
 662             valid = checkTime == len(value)     # entire string parsed? 
 663 ##            dbg('checkTime == len(value)?', valid) 
 666 ##                dbg(indent=0, suspend=0) 
 667                 raise ValueError('cannot convert string "%s" to valid time' % value) 
 670             if isinstance(value, wx.DateTime): 
 671                 hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond() 
 672             elif isinstance(value, wx.TimeSpan): 
 673                 totalseconds = value.GetSeconds() 
 674                 hour = totalseconds / 3600 
 675                 minute = totalseconds / 60 - (hour * 60) 
 676                 second = totalseconds - ((hour * 3600) + (minute * 60)) 
 678             elif accept_mx and isinstance(value, DateTime.DateTimeType): 
 679                 hour, minute, second = value.hour, value.minute, value.second 
 680             elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType): 
 681                 hour, minute, second = value.hour, value.minute, value.second 
 683                 # Not a valid function argument 
 685                     error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value) 
 687                     error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value) 
 688 ##                dbg(indent=0, suspend=0) 
 689                 raise ValueError(error) 
 691             wxdt = wx.DateTimeFromDMY(1, 0, 1970) 
 693             wxdt.SetMinute(minute) 
 694             wxdt.SetSecond(second) 
 696 ##        dbg('wxdt:', wxdt, indent=0, suspend=0) 
 700     def SetMxDateTime(self, mxdt): 
 702         Because SetValue can take an mxDateTime, (if DateTime is importable), 
 703         this is now just an alias. 
 708     def GetMxDateTime(self, value=None): 
 710             t = self.GetValue(as_mxDateTime=True) 
 712             # Convert string 1st to wxDateTime, then use components, since 
 713             # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM: 
 714             wxdt = self.GetWxDateTime(value) 
 715             hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond() 
 716             t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second) 
 720     def SetMin(self, min=None): 
 722         Sets the minimum value of the control.  If a value of None 
 723         is provided, then the control will have no explicit minimum value. 
 724         If the value specified is greater than the current maximum value, 
 725         then the function returns 0 and the minimum will not change from 
 726         its current setting.  On success, the function returns 1. 
 728         If successful and the current value is lower than the new lower 
 729         bound, if the control is limited, the value will be automatically 
 730         adjusted to the new minimum value; if not limited, the value in the 
 731         control will be colored as invalid. 
 733 ##        dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1) 
 736                 min = self.GetWxDateTime(min) 
 737                 self.__min = self._toGUI(min) 
 739 ##                dbg('exception occurred', indent=0) 
 744         if self.IsLimited() and not self.IsInBounds(): 
 745             self.SetLimited(self.__limited) # force limited value: 
 749 ##        dbg('ret:', ret, indent=0) 
 753     def GetMin(self, as_string = False): 
 755         Gets the minimum value of the control. 
 756         If None, it will return None.  Otherwise it will return 
 757         the current minimum bound on the control, as a wxDateTime 
 758         by default, or as a string if as_string argument is True. 
 761 ##        dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) 
 762         if self.__min is None: 
 763 ##            dbg('(min == None)') 
 770                 ret = self.GetWxDateTime(self.__min) 
 773 ##                dbg('exception occurred', indent=0) 
 775 ##            dbg('ret:', repr(ret)) 
 776 ##        dbg(indent=0, suspend=0) 
 780     def SetMax(self, max=None): 
 782         Sets the maximum value of the control. If a value of None 
 783         is provided, then the control will have no explicit maximum value. 
 784         If the value specified is less than the current minimum value, then 
 785         the function returns False and the maximum will not change from its 
 786         current setting. On success, the function returns True. 
 788         If successful and the current value is greater than the new upper 
 789         bound, if the control is limited the value will be automatically 
 790         adjusted to this maximum value; if not limited, the value in the 
 791         control will be colored as invalid. 
 793 ##        dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1) 
 796                 max = self.GetWxDateTime(max) 
 797                 self.__max = self._toGUI(max) 
 799 ##                dbg('exception occurred', indent=0) 
 803 ##        dbg('max:', repr(self.__max)) 
 804         if self.IsLimited() and not self.IsInBounds(): 
 805             self.SetLimited(self.__limited) # force limited value: 
 809 ##        dbg('ret:', ret, indent=0) 
 813     def GetMax(self, as_string = False): 
 815         Gets the minimum value of the control. 
 816         If None, it will return None.  Otherwise it will return 
 817         the current minimum bound on the control, as a wxDateTime 
 818         by default, or as a string if as_string argument is True. 
 821 ##        dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) 
 822         if self.__max is None: 
 823 ##            dbg('(max == None)') 
 830                 ret = self.GetWxDateTime(self.__max) 
 833 ##                dbg('exception occurred', indent=0) 
 835 ##            dbg('ret:', repr(ret)) 
 836 ##        dbg(indent=0, suspend=0) 
 840     def SetBounds(self, min=None, max=None): 
 842         This function is a convenience function for setting the min and max 
 843         values at the same time.  The function only applies the maximum bound 
 844         if setting the minimum bound is successful, and returns True 
 845         only if both operations succeed. 
 846         NOTE: leaving out an argument will remove the corresponding bound. 
 848         ret = self.SetMin(min) 
 849         return ret and self.SetMax(max) 
 852     def GetBounds(self, as_string = False): 
 854         This function returns a two-tuple (min,max), indicating the 
 855         current bounds of the control.  Each value can be None if 
 856         that bound is not set. 
 858         return (self.GetMin(as_string), self.GetMax(as_string)) 
 861     def SetLimited(self, limited): 
 863         If called with a value of True, this function will cause the control 
 864         to limit the value to fall within the bounds currently specified. 
 865         If the control's value currently exceeds the bounds, it will then 
 866         be limited accordingly. 
 868         If called with a value of 0, this function will disable value 
 869         limiting, but coloring of out-of-bounds values will still take 
 870         place if bounds have been set for the control. 
 872 ##        dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1) 
 873         self.__limited = limited 
 876             self.SetMaskParameters(validRequired = False) 
 881 ##        dbg('requiring valid value') 
 882         self.SetMaskParameters(validRequired = True) 
 886         if min is None or max is None: 
 887 ##            dbg('both bounds not set; no further action taken') 
 888             return  # can't limit without 2 bounds 
 890         elif not self.IsInBounds(): 
 891             # set value to the nearest bound: 
 893                 value = self.GetWxDateTime() 
 895 ##                dbg('exception occurred', indent=0) 
 898             if min <= max:   # valid range doesn't span midnight 
 900                 # which makes the "nearest bound" computation trickier... 
 902                 # determine how long the "invalid" pie wedge is, and cut 
 903                 # this interval in half for comparison purposes: 
 905                 # Note: relies on min and max and value date portions 
 906                 # always being the same. 
 907                 interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max 
 909                 half_interval = wx.TimeSpan( 
 912                                     interval.GetSeconds() / 2,  # seconds 
 915                 if value < min: # min is on next day, so use value on 
 916                     # "next day" for "nearest" interval calculation: 
 917                     cmp_value = value + wx.TimeSpan(24, 0, 0, 0) 
 918                 else:   # "before midnight; ok 
 921                 if (cmp_value - max) > half_interval: 
 922 ##                    dbg('forcing value to min (%s)' % min.FormatTime()) 
 925 ##                    dbg('forcing value to max (%s)' % max.FormatTime()) 
 929                 # therefore  max < value < min guaranteed to be true, 
 930                 # so "nearest bound" calculation is much easier: 
 931                 if (value - max) >= (min - value): 
 932                     # current value closer to min; pick that edge of pie wedge 
 933 ##                    dbg('forcing value to min (%s)' % min.FormatTime()) 
 936 ##                    dbg('forcing value to max (%s)' % max.FormatTime()) 
 945         Returns True if the control is currently limiting the 
 946         value to fall within any current bounds.  Note: can 
 947         be set even if there are no current bounds. 
 949         return self.__limited 
 952     def IsInBounds(self, value=None): 
 954         Returns True if no value is specified and the current value 
 955         of the control falls within the current bounds.  As the clock 
 956         is a "circle", both minimum and maximum bounds must be set for 
 957         a value to ever be considered "out of bounds".  This function can 
 958         also be called with a value to see if that value would fall within 
 959         the current bounds of the given control. 
 961         if value is not None: 
 963                 value = self.GetWxDateTime(value)   # try to regularize passed value 
 965 ##                dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0) 
 968 ##        dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1) 
 969         if self.__min is None or self.__max is None: 
 975                 value = self.GetWxDateTime() 
 977 ##                dbg('exception occurred', indent=0) 
 980 ##        dbg('value:', value.FormatTime()) 
 982         # Get wxDateTime representations of bounds: 
 986         midnight = wx.DateTimeFromDMY(1, 0, 1970) 
 987         if min <= max:   # they don't span midnight 
 988             ret = min <= value <= max 
 991             # have to break into 2 tests; to be in bounds 
 992             # either "min" <= value (<= midnight of *next day*) 
 993             # or midnight <= value <= "max" 
 994             ret = min <= value or (midnight <= value <= max) 
 995 ##        dbg('in bounds?', ret, indent=0) 
 999     def IsValid( self, value ): 
1001         Can be used to determine if a given value would be a legal and 
1002         in-bounds value for the control. 
1005             self.__validateValue(value) 
1010     def SetFormat(self, format): 
1011         self.SetParameters(format=format) 
1013     def GetFormat(self): 
1014         if self.__displaySeconds: 
1015             if self.__fmt24hr: return '24HHMMSS' 
1016             else:              return 'HHMMSS' 
1018             if self.__fmt24hr: return '24HHMM' 
1021 #------------------------------------------------------------------------------------------------------------- 
1022 # these are private functions and overrides: 
1025     def __OnTextChange(self, event=None): 
1026 ##        dbg('TimeCtrl::OnTextChange', indent=1) 
1028         # Allow Maskedtext base control to color as appropriate, 
1029         # and Skip the EVT_TEXT event (if appropriate.) 
1030         ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue() 
1031         ## call is generating two (2) EVT_TEXT events. (!) 
1032         ## The the only mechanism I can find to mask this problem is to 
1033         ## keep track of last value seen, and declare a valid EVT_TEXT 
1034         ## event iff the value has actually changed.  The masked edit 
1035         ## OnTextChange routine does this, and returns True on a valid event, 
1037         if not BaseMaskedTextCtrl._OnTextChange(self, event): 
1040 ##        dbg('firing TimeUpdatedEvent...') 
1041         evt = TimeUpdatedEvent(self.GetId(), self.GetValue()) 
1042         evt.SetEventObject(self) 
1043         self.GetEventHandler().ProcessEvent(evt) 
1047     def SetInsertionPoint(self, pos): 
1049         Records the specified position and associated cell before calling base class' function. 
1050         This is necessary to handle the optional spin button, because the insertion 
1051         point is lost when the focus shifts to the spin button. 
1053 ##        dbg('TimeCtrl::SetInsertionPoint', pos, indent=1) 
1054         BaseMaskedTextCtrl.SetInsertionPoint(self, pos)                 # (causes EVT_TEXT event to fire) 
1055         self.__posCurrent = self.GetInsertionPoint() 
1059     def SetSelection(self, sel_start, sel_to): 
1060 ##        dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1) 
1062         # Adjust selection range to legal extent if not already 
1066         if self.__posCurrent != sel_start:                      # force selection and insertion point to match 
1067             self.SetInsertionPoint(sel_start) 
1068         cell_start, cell_end = self._FindField(sel_start)._extent 
1069         if not cell_start <= sel_to <= cell_end: 
1072         self.__bSelection = sel_start != sel_to 
1073         BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to) 
1077     def __OnSpin(self, key): 
1079         This is the function that gets called in response to up/down arrow or 
1080         bound spin button events. 
1082         self.__IncrementValue(key, self.__posCurrent)   # changes the value 
1084         # Ensure adjusted control regains focus and has adjusted portion 
1087         start, end = self._FindField(self.__posCurrent)._extent 
1088         self.SetInsertionPoint(start) 
1089         self.SetSelection(start, end) 
1090 ##        dbg('current position:', self.__posCurrent) 
1093     def __OnSpinUp(self, event): 
1095         Event handler for any bound spin button on EVT_SPIN_UP; 
1096         causes control to behave as if up arrow was pressed. 
1098 ##        dbg('TimeCtrl::OnSpinUp', indent=1) 
1099         self.__OnSpin(wx.WXK_UP) 
1100         keep_processing = False 
1102         return keep_processing 
1105     def __OnSpinDown(self, event): 
1107         Event handler for any bound spin button on EVT_SPIN_DOWN; 
1108         causes control to behave as if down arrow was pressed. 
1110 ##        dbg('TimeCtrl::OnSpinDown', indent=1) 
1111         self.__OnSpin(wx.WXK_DOWN) 
1112         keep_processing = False 
1114         return keep_processing 
1117     def __OnChar(self, event): 
1119         Handler to explicitly look for ':' keyevents, and if found, 
1120         clear the m_shiftDown field, so it will behave as forward tab. 
1121         It then calls the base control's _OnChar routine with the modified 
1124 ##        dbg('TimeCtrl::OnChar', indent=1) 
1125         keycode = event.GetKeyCode() 
1126 ##        dbg('keycode:', keycode) 
1127         if keycode == ord(':'): 
1128 ##            dbg('colon seen! removing shift attribute') 
1129             event.m_shiftDown = False 
1130         BaseMaskedTextCtrl._OnChar(self, event )              ## handle each keypress 
1134     def __OnSetToNow(self, event): 
1136         This is the key handler for '!' and 'c'; this allows the user to 
1137         quickly set the value of the control to the current time. 
1139         self.SetValue(wx.DateTime_Now().FormatTime()) 
1140         keep_processing = False 
1141         return keep_processing 
1144     def __LimitSelection(self, event): 
1146         Event handler for motion events; this handler 
1147         changes limits the selection to the new cell boundaries. 
1149 ##        dbg('TimeCtrl::LimitSelection', indent=1) 
1150         pos = self.GetInsertionPoint() 
1151         self.__posCurrent = pos 
1152         sel_start, sel_to = self.GetSelection() 
1153         selection = sel_start != sel_to 
1155             # only allow selection to end of current cell: 
1156             start, end = self._FindField(sel_start)._extent 
1157             if sel_to < pos:   sel_to = start 
1158             elif sel_to > pos: sel_to = end 
1160 ##        dbg('new pos =', self.__posCurrent, 'select to ', sel_to) 
1161         self.SetInsertionPoint(self.__posCurrent) 
1162         self.SetSelection(self.__posCurrent, sel_to) 
1163         if event: event.Skip() 
1167     def __IncrementValue(self, key, pos): 
1168 ##        dbg('TimeCtrl::IncrementValue', key, pos, indent=1) 
1169         text = self.GetValue() 
1170         field = self._FindField(pos) 
1171 ##        dbg('field: ', field._index) 
1172         start, end = field._extent 
1173         slice = text[start:end] 
1174         if key == wx.WXK_UP: increment = 1 
1175         else:             increment = -1 
1177         if slice in ('A', 'P'): 
1178             if slice == 'A': newslice = 'P' 
1179             elif slice == 'P': newslice = 'A' 
1180             newvalue = text[:start] + newslice + text[end:] 
1182         elif field._index == 0: 
1183             # adjusting this field is trickier, as its value can affect the 
1184             # am/pm setting.  So, we use wxDateTime to generate a new value for us: 
1185             # (Use a fixed date not subject to DST variations:) 
1186             converter = wx.DateTimeFromDMY(1, 0, 1970) 
1187 ##            dbg('text: "%s"' % text) 
1188             converter.ParseTime(text.strip()) 
1189             currenthour = converter.GetHour() 
1190 ##            dbg('current hour:', currenthour) 
1191             newhour = (currenthour + increment) % 24 
1192 ##            dbg('newhour:', newhour) 
1193             converter.SetHour(newhour) 
1194 ##            dbg('converter.GetHour():', converter.GetHour()) 
1195             newvalue = converter     # take advantage of auto-conversion for am/pm in .SetValue() 
1197         else:   # minute or second field; handled the same way: 
1198             newslice = "%02d" % ((int(slice) + increment) % 60) 
1199             newvalue = text[:start] + newslice + text[end:] 
1202             self.SetValue(newvalue) 
1204         except ValueError:  # must not be in bounds: 
1205             if not wx.Validator_IsSilent(): 
1210     def _toGUI( self, wxdt ): 
1212         This function takes a wxdt as an unambiguous representation of a time, and 
1213         converts it to a string appropriate for the format of the control. 
1216             if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S') 
1217             else:                     strval = wxdt.Format('%H:%M') 
1219             if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p') 
1220             else:                     strval = wxdt.Format('%I:%M %p') 
1225     def __validateValue( self, value ): 
1227         This function converts the value to a wxDateTime if not already one, 
1228         does bounds checking and raises ValueError if argument is 
1229         not a valid value for the control as currently specified. 
1230         It is used by both the SetValue() and the IsValid() methods. 
1232 ##        dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1) 
1235             raise ValueError('%s not a valid time value' % repr(value)) 
1237         valid = True    # assume true 
1239             value = self.GetWxDateTime(value)   # regularize form; can generate ValueError if problem doing so 
1241 ##            dbg('exception occurred', indent=0) 
1244         if self.IsLimited() and not self.IsInBounds(value): 
1247                 'value %s is not within the bounds of the control' % str(value) ) 
1251 #---------------------------------------------------------------------------- 
1252 # Test jig for TimeCtrl: 
1254 if __name__ == '__main__': 
1257     class TestPanel(wx.Panel): 
1258         def __init__(self, parent, id, 
1259                      pos = wx.DefaultPosition, size = wx.DefaultSize, 
1260                      fmt24hr = 0, test_mx = 0, 
1261                      style = wx.TAB_TRAVERSAL ): 
1263             wx.Panel.__init__(self, parent, id, pos, size, style) 
1265             self.test_mx = test_mx 
1267             self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr) 
1268             sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 ) 
1269             self.tc.BindSpinButton(sb) 
1271             sizer = wx.BoxSizer( wx.HORIZONTAL ) 
1272             sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 ) 
1273             sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 ) 
1275             self.SetAutoLayout( True ) 
1276             self.SetSizer( sizer ) 
1278             sizer.SetSizeHints( self ) 
1280             self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc) 
1282         def OnTimeChange(self, event): 
1283 ##            dbg('OnTimeChange: value = ', event.GetValue()) 
1284             wxdt = self.tc.GetWxDateTime() 
1285 ##            dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()) 
1287                 mxdt = self.tc.GetMxDateTime() 
1288 ##                dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second) 
1291     class MyApp(wx.App): 
1294             fmt24hr = '24' in sys.argv 
1295             test_mx = 'mx' in sys.argv 
1297                 frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) ) 
1298                 panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx) 
1301                 traceback.print_exc() 
1309         traceback.print_exc() 
1312 ##   1. Changed parameter name display_seconds to displaySeconds, to follow 
1313 ##      other masked edit conventions. 
1314 ##   2. Added format parameter, to remove need to use both fmt24hr and displaySeconds. 
1315 ##   3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of 
1316 ##      nonsensical parameter methods from the control, so it will work 
1317 ##      properly with Boa.