X-Git-Url: https://git.saurik.com/wxWidgets.git/blobdiff_plain/8b9a4190f70909de9568f45389e7aa3ecbc66b8a..8179a16812e66ff0ac32bb09803e95756350c76c:/wxPython/wx/lib/timectrl.py?ds=sidebyside diff --git a/wxPython/wx/lib/timectrl.py b/wxPython/wx/lib/timectrl.py index bd17df1376..af42f0836b 100644 --- a/wxPython/wx/lib/timectrl.py +++ b/wxPython/wx/lib/timectrl.py @@ -1,8 +1,1315 @@ +#---------------------------------------------------------------------------- +# Name: timectrl.py +# Author: Will Sadkin +# Created: 09/19/2002 +# Copyright: (c) 2002 by Will Sadkin, 2002 +# RCS-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------------- +# NOTE: +# This was written way it is because of the lack of masked edit controls +# in wxWindows/wxPython. I would also have preferred to derive this +# control from a wxSpinCtrl rather than wxTextCtrl, but the wxTextCtrl +# component of that control is inaccessible through the interface exposed in +# wxPython. +# +# TimeCtrl does not use validators, because it does careful manipulation +# of the cursor in the text window on each keystroke, and validation is +# cursor-position specific, so the control intercepts the key codes before the +# validator would fire. +# +# TimeCtrl now also supports .SetValue() with either strings or wxDateTime +# values, as well as range limits, with the option of either enforcing them +# or simply coloring the text of the control if the limits are exceeded. +# +# Note: this class now makes heavy use of wxDateTime for parsing and +# regularization, but it always does so with ephemeral instances of +# wxDateTime, as the C++/Python validity of these instances seems to not +# persist. Because "today" can be a day for which an hour can "not exist" +# or be counted twice (1 day each per year, for DST adjustments), the date +# portion of all wxDateTimes used/returned have their date portion set to +# Jan 1, 1970 (the "epoch.") +#---------------------------------------------------------------------------- +# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o Updated for V2.5 compatability +# o wx.SpinCtl has some issues that cause the control to +# lock up. Noted in other places using it too, it's not this module +# that's at fault. +# +# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net) +# +# o wxMaskedTextCtrl -> MaskedTextCtrl +# o wxTimeCtrl -> TimeCtrl +# -"""Renamer stub: provides a way to drop the wx prefix from wxPython objects.""" +""" +

+TimeCtrl provides a multi-cell control that allows manipulation of a time +value. It supports 12 or 24 hour format, and you can use wxDateTime or mxDateTime +to get/set values from the control. +

+Left/right/tab keys to switch cells within a TimeCtrl, and the up/down arrows act +like a spin control. TimeCtrl also allows for an actual spin button to be attached +to the control, so that it acts like the up/down arrow keys. +

+The ! or c key sets the value of the control to the current time. +

+Here's the API for TimeCtrl: +

+    TimeCtrl(
+         parent, id = -1,
+         value = '12:00:00 AM',
+         pos = wx.DefaultPosition,
+         size = wx.DefaultSize,
+         style = wxTE_PROCESS_TAB,
+         validator = wx.DefaultValidator,
+         name = "time",
+         format = 'HHMMSS',         
+         fmt24hr = False,
+         displaySeconds = True,
+         spinButton = None,
+         min = None,
+         max = None,
+         limited = None,
+         oob_color = "Yellow"
+)
+
+ +
+
+
+
EVT_TIMEUPDATE(win, id, func) +
func is fired whenever the value of the control changes. +
+
+
SetValue(time_string | wxDateTime | wxTimeSpan | mx.DateTime | mx.DateTimeDelta) +
Sets the value of the control to a particular time, given a valid +value; raises ValueError on invalid value. +NOTE: This will only allow mx.DateTime or mx.DateTimeDelta if mx.DateTime +was successfully imported by the class module. +
+
GetValue(as_wxDateTime = False, as_mxDateTime = False, as_wxTimeSpan=False, as mxDateTimeDelta=False) +
Retrieves the value of the time from the control. By default this is +returned as a string, unless one of the other arguments is set; args are +searched in the order listed; only one value will be returned. +
+
GetWxDateTime(value=None) +
When called without arguments, retrieves the value of the control, and applies +it to the wxDateTimeFromHMS() constructor, and returns the resulting value. +The date portion will always be set to Jan 1, 1970. This form is the same +as GetValue(as_wxDateTime=True). GetWxDateTime can also be called with any of the +other valid time formats settable with SetValue, to regularize it to a single +wxDateTime form. The function will raise ValueError on an unconvertable argument. +
+
GetMxDateTime() +
Retrieves the value of the control and applies it to the DateTime.Time() +constructor,and returns the resulting value. (The date portion will always be +set to Jan 1, 1970.) (Same as GetValue(as_wxDateTime=True); provided for backward +compatibility with previous release.) +
+
+
BindSpinButton(SpinBtton) +
Binds an externally created spin button to the control, so that up/down spin +events change the active cell or selection in the control (in addition to the +up/down cursor keys.) (This is primarily to allow you to create a "standard" +interface to time controls, as seen in Windows.) +
+
+
SetMin(min=None) +
Sets the expected minimum value, or lower bound, of the control. +(The lower bound will only be enforced if the control is +configured to limit its values to the set bounds.) +If a value of None is provided, then the control will have +explicit lower bound. If the value specified is greater than +the current lower bound, then the function returns False and the +lower bound will not change from its current setting. On success, +the function returns True. Even if set, if there is no corresponding +upper bound, the control will behave as if it is unbounded. +
If successful and the current value is outside the +new bounds, if the control is limited the value will be +automatically adjusted to the nearest bound; if not limited, +the background of the control will be colored with the current +out-of-bounds color. +
+
GetMin(as_string=False) +
Gets the current lower bound value for the control, returning +None, if not set, or a wxDateTime, unless the as_string parameter +is set to True, at which point it will return the string +representation of the lower bound. +
+
+
SetMax(max=None) +
Sets the expected maximum value, or upper bound, of the control. +(The upper bound will only be enforced if the control is +configured to limit its values to the set bounds.) +If a value of None is provided, then the control will +have no explicit upper bound. If the value specified is less +than the current lower bound, then the function returns False and +the maximum will not change from its current setting. On success, +the function returns True. Even if set, if there is no corresponding +lower bound, the control will behave as if it is unbounded. +
If successful and the current value is outside the +new bounds, if the control is limited the value will be +automatically adjusted to the nearest bound; if not limited, +the background of the control will be colored with the current +out-of-bounds color. +
+
GetMax(as_string = False) +
Gets the current upper bound value for the control, returning +None, if not set, or a wxDateTime, unless the as_string parameter +is set to True, at which point it will return the string +representation of the lower bound. -from wx import _rename -from wxPython.lib import timectrl -_rename(globals(), timectrl.__dict__, modulename='lib.timectrl') -del timectrl -del _rename +
+
+
SetBounds(min=None,max=None) +
This function is a convenience function for setting the min and max +values at the same time. The function only applies the maximum bound +if setting the minimum bound is successful, and returns True +only if both operations succeed. Note: leaving out an argument +will remove the corresponding bound, and result in the behavior of +an unbounded control. +
+
GetBounds(as_string = False) +
This function returns a two-tuple (min,max), indicating the +current bounds of the control. Each value can be None if +that bound is not set. The values will otherwise be wxDateTimes +unless the as_string argument is set to True, at which point they +will be returned as string representations of the bounds. +
+
+
IsInBounds(value=None) +
Returns True if no value is specified and the current value +of the control falls within the current bounds. This function can also +be called with a value to see if that value would fall within the current +bounds of the given control. It will raise ValueError if the value +specified is not a wxDateTime, mxDateTime (if available) or parsable string. +
+
+
IsValid(value) +
Returns Trueif specified value is a legal time value and +falls within the current bounds of the given control. +
+
+
SetLimited(bool) +
If called with a value of True, this function will cause the control +to limit the value to fall within the bounds currently specified. +(Provided both bounds have been set.) +If the control's value currently exceeds the bounds, it will then +be set to the nearest bound. +If called with a value of False, this function will disable value +limiting, but coloring of out-of-bounds values will still take +place if bounds have been set for the control. +
IsLimited() +
Returns True if the control is currently limiting the +value to fall within the current bounds. +
+
+ +""" + +import copy +import string +import types + +import wx + +from wx.tools.dbg import Logger +from wx.lib.maskededit import BaseMaskedTextCtrl, Field + +dbg = Logger() +dbg(enable=0) + +try: + from mx import DateTime + accept_mx = True +except ImportError: + accept_mx = False + +# This class of event fires whenever the value of the time changes in the control: +wxEVT_TIMEVAL_UPDATED = wx.NewEventType() +EVT_TIMEUPDATE = wx.PyEventBinder(wxEVT_TIMEVAL_UPDATED, 1) + +class TimeUpdatedEvent(wx.PyCommandEvent): + def __init__(self, id, value ='12:00:00 AM'): + wx.PyCommandEvent.__init__(self, wxEVT_TIMEVAL_UPDATED, id) + self.value = value + def GetValue(self): + """Retrieve the value of the time control at the time this event was generated""" + return self.value + +class TimeCtrlAccessorsMixin: + # Define TimeCtrl's list of attributes having their own + # Get/Set functions, ignoring those that make no sense for + # an numeric control. + exposed_basectrl_params = ( + 'defaultValue', + 'description', + + 'useFixedWidthFont', + 'emptyBackgroundColour', + 'validBackgroundColour', + 'invalidBackgroundColour', + + 'validFunc', + 'validRequired', + ) + for param in exposed_basectrl_params: + propname = param[0].upper() + param[1:] + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + if param.find('Colour') != -1: + # add non-british spellings, for backward-compatibility + propname.replace('Colour', 'Color') + + exec('def Set%s(self, value): self.SetCtrlParameters(%s=value)' % (propname, param)) + exec('def Get%s(self): return self.GetCtrlParameter("%s")''' % (propname, param)) + + +class TimeCtrl(BaseMaskedTextCtrl): + + valid_ctrl_params = { + 'format' : 'HHMMSS', # default format code + 'displaySeconds' : True, # by default, shows seconds + 'min': None, # by default, no bounds set + 'max': None, + 'limited': False, # by default, no limiting even if bounds set + 'useFixedWidthFont': True, # by default, use a fixed-width font + 'oob_color': "Yellow" # by default, the default MaskedTextCtrl "invalid" color + } + + def __init__ ( + self, parent, id=-1, value = '12:00:00 AM', + pos = wx.DefaultPosition, size = wx.DefaultSize, + fmt24hr=False, + spinButton = None, + style = wx.TE_PROCESS_TAB, + validator = wx.DefaultValidator, + name = "time", + **kwargs ): + + # set defaults for control: + dbg('setting defaults:') + for key, param_value in TimeCtrl.valid_ctrl_params.items(): + # This is done this way to make setattr behave consistently with + # "private attribute" name mangling + setattr(self, "_TimeCtrl__" + key, copy.copy(param_value)) + + # create locals from current defaults, so we can override if + # specified in kwargs, and handle uniformly: + min = self.__min + max = self.__max + limited = self.__limited + self.__posCurrent = 0 + # handle deprecated keword argument name: + if kwargs.has_key('display_seconds'): + kwargs['displaySeconds'] = kwargs['display_seconds'] + del kwargs['display_seconds'] + if not kwargs.has_key('displaySeconds'): + kwargs['displaySeconds'] = True + + # (handle positional arg (from original release) differently from rest of kwargs:) + self.__fmt24hr = False + if not kwargs.has_key('format'): + if fmt24hr: + if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: + kwargs['format'] = '24HHMMSS' + del kwargs['displaySeconds'] + else: + kwargs['format'] = '24HHMM' + else: + if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']: + kwargs['format'] = 'HHMMSS' + del kwargs['displaySeconds'] + else: + kwargs['format'] = 'HHMM' + + if not kwargs.has_key('useFixedWidthFont'): + # allow control over font selection: + kwargs['useFixedWidthFont'] = self.__useFixedWidthFont + + maskededit_kwargs = self.SetParameters(**kwargs) + + # allow for explicit size specification: + if size != wx.DefaultSize: + # override (and remove) "autofit" autoformat code in standard time formats: + maskededit_kwargs['formatcodes'] = 'T!' + + # This allows range validation if set + maskededit_kwargs['validFunc'] = self.IsInBounds + + # This allows range limits to affect insertion into control or not + # dynamically without affecting individual field constraint validation + maskededit_kwargs['retainFieldValidation'] = True + + # Now we can initialize the base control: + BaseMaskedTextCtrl.__init__( + self, parent, id=id, + pos=pos, size=size, + style = style, + validator = validator, + name = name, + setupEventHandling = False, + **maskededit_kwargs) + + + # This makes ':' act like tab (after we fix each ':' key event to remove "shift") + self._SetKeyHandler(':', self._OnChangeField) + + + # This makes the up/down keys act like spin button controls: + self._SetKeycodeHandler(wx.WXK_UP, self.__OnSpinUp) + self._SetKeycodeHandler(wx.WXK_DOWN, self.__OnSpinDown) + + + # This allows ! and c/C to set the control to the current time: + self._SetKeyHandler('!', self.__OnSetToNow) + self._SetKeyHandler('c', self.__OnSetToNow) + self._SetKeyHandler('C', self.__OnSetToNow) + + + # Set up event handling ourselves, so we can insert special + # processing on the ":' key to remove the "shift" attribute + # *before* the default handlers have been installed, so + # that : takes you forward, not back, and so we can issue + # EVT_TIMEUPDATE events on changes: + + self.Bind(wx.EVT_SET_FOCUS, self._OnFocus ) ## defeat automatic full selection + self.Bind(wx.EVT_KILL_FOCUS, self._OnKillFocus ) ## run internal validator + self.Bind(wx.EVT_LEFT_UP, self.__LimitSelection) ## limit selections to single field + self.Bind(wx.EVT_LEFT_DCLICK, self._OnDoubleClick ) ## select field under cursor on dclick + self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab. + self.Bind(wx.EVT_CHAR, self.__OnChar ) ## remove "shift" attribute from colon key event, + ## then call BaseMaskedTextCtrl._OnChar with + ## the possibly modified event. + self.Bind(wx.EVT_TEXT, self.__OnTextChange, self ) ## color control appropriately and EVT_TIMEUPDATE events + + + # Validate initial value and set if appropriate + try: + self.SetBounds(min, max) + self.SetLimited(limited) + self.SetValue(value) + except: + self.SetValue('12:00:00 AM') + + if spinButton: + self.BindSpinButton(spinButton) # bind spin button up/down events to this control + + + def SetParameters(self, **kwargs): + dbg('TimeCtrl::SetParameters(%s)' % repr(kwargs), indent=1) + maskededit_kwargs = {} + reset_format = False + + if kwargs.has_key('display_seconds'): + kwargs['displaySeconds'] = kwargs['display_seconds'] + del kwargs['display_seconds'] + if kwargs.has_key('format') and kwargs.has_key('displaySeconds'): + del kwargs['displaySeconds'] # always apply format if specified + + # assign keyword args as appropriate: + for key, param_value in kwargs.items(): + if key not in TimeCtrl.valid_ctrl_params.keys(): + raise AttributeError('invalid keyword argument "%s"' % key) + + if key == 'format': + # handle both local or generic 'maskededit' autoformat codes: + if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS': + self.__displaySeconds = True + self.__fmt24hr = False + elif param_value == 'HHMM' or param_value == 'TIMEHHMM': + self.__displaySeconds = False + self.__fmt24hr = False + elif param_value == '24HHMMSS' or param_value == '24HRTIMEHHMMSS': + self.__displaySeconds = True + self.__fmt24hr = True + elif param_value == '24HHMM' or param_value == '24HRTIMEHHMM': + self.__displaySeconds = False + self.__fmt24hr = True + else: + raise AttributeError('"%s" is not a valid format' % param_value) + reset_format = True + + elif key in ("displaySeconds", "display_seconds") and not kwargs.has_key('format'): + self.__displaySeconds = param_value + reset_format = True + + elif key == "min": min = param_value + elif key == "max": max = param_value + elif key == "limited": limited = param_value + + elif key == "useFixedWidthFont": + maskededit_kwargs[key] = param_value + + elif key == "oob_color": + maskededit_kwargs['invalidBackgroundColor'] = param_value + + if reset_format: + if self.__fmt24hr: + if self.__displaySeconds: maskededit_kwargs['autoformat'] = '24HRTIMEHHMMSS' + else: maskededit_kwargs['autoformat'] = '24HRTIMEHHMM' + + # Set hour field to zero-pad, right-insert, require explicit field change, + # select entire field on entry, and require a resultant valid entry + # to allow character entry: + hourfield = Field(formatcodes='0r" % self.GetValue() + + + def SetValue(self, value): + """ + Validating SetValue function for time values: + This function will do dynamic type checking on the value argument, + and convert wxDateTime, mxDateTime, or 12/24 format time string + into the appropriate format string for the control. + """ + dbg('TimeCtrl::SetValue(%s)' % repr(value), indent=1) + try: + strtime = self._toGUI(self.__validateValue(value)) + except: + dbg('validation failed', indent=0) + raise + + dbg('strtime:', strtime) + self._SetValue(strtime) + dbg(indent=0) + + def GetValue(self, + as_wxDateTime = False, + as_mxDateTime = False, + as_wxTimeSpan = False, + as_mxDateTimeDelta = False): + + + if as_wxDateTime or as_mxDateTime or as_wxTimeSpan or as_mxDateTimeDelta: + value = self.GetWxDateTime() + if as_wxDateTime: + pass + elif as_mxDateTime: + value = DateTime.DateTime(1970, 1, 1, value.GetHour(), value.GetMinute(), value.GetSecond()) + elif as_wxTimeSpan: + value = wx.TimeSpan(value.GetHour(), value.GetMinute(), value.GetSecond()) + elif as_mxDateTimeDelta: + value = DateTime.DateTimeDelta(0, value.GetHour(), value.GetMinute(), value.GetSecond()) + else: + value = BaseMaskedTextCtrl.GetValue(self) + return value + + + def SetWxDateTime(self, wxdt): + """ + Because SetValue can take a wxDateTime, this is now just an alias. + """ + self.SetValue(wxdt) + + + def GetWxDateTime(self, value=None): + """ + This function is the conversion engine for TimeCtrl; it takes + one of the following types: + time string + wxDateTime + wxTimeSpan + mxDateTime + mxDateTimeDelta + and converts it to a wxDateTime that always has Jan 1, 1970 as its date + portion, so that range comparisons around values can work using + wxDateTime's built-in comparison function. If a value is not + provided to convert, the string value of the control will be used. + If the value is not one of the accepted types, a ValueError will be + raised. + """ + global accept_mx + dbg(suspend=1) + dbg('TimeCtrl::GetWxDateTime(%s)' % repr(value), indent=1) + if value is None: + dbg('getting control value') + value = self.GetValue() + dbg('value = "%s"' % value) + + if type(value) == types.UnicodeType: + value = str(value) # convert to regular string + + valid = True # assume true + if type(value) == types.StringType: + + # Construct constant wxDateTime, then try to parse the string: + wxdt = wx.DateTimeFromDMY(1, 0, 1970) + dbg('attempting conversion') + value = value.strip() # (parser doesn't like leading spaces) + checkTime = wxdt.ParseTime(value) + valid = checkTime == len(value) # entire string parsed? + dbg('checkTime == len(value)?', valid) + + if not valid: + dbg(indent=0, suspend=0) + raise ValueError('cannot convert string "%s" to valid time' % value) + + else: + if isinstance(value, wx.DateTime): + hour, minute, second = value.GetHour(), value.GetMinute(), value.GetSecond() + elif isinstance(value, wx.TimeSpan): + totalseconds = value.GetSeconds() + hour = totalseconds / 3600 + minute = totalseconds / 60 - (hour * 60) + second = totalseconds - ((hour * 3600) + (minute * 60)) + + elif accept_mx and isinstance(value, DateTime.DateTimeType): + hour, minute, second = value.hour, value.minute, value.second + elif accept_mx and isinstance(value, DateTime.DateTimeDeltaType): + hour, minute, second = value.hour, value.minute, value.second + else: + # Not a valid function argument + if accept_mx: + error = 'GetWxDateTime requires wxDateTime, mxDateTime or parsable time string, passed %s'% repr(value) + else: + error = 'GetWxDateTime requires wxDateTime or parsable time string, passed %s'% repr(value) + dbg(indent=0, suspend=0) + raise ValueError(error) + + wxdt = wx.DateTimeFromDMY(1, 0, 1970) + wxdt.SetHour(hour) + wxdt.SetMinute(minute) + wxdt.SetSecond(second) + + dbg('wxdt:', wxdt, indent=0, suspend=0) + return wxdt + + + def SetMxDateTime(self, mxdt): + """ + Because SetValue can take an mxDateTime, (if DateTime is importable), + this is now just an alias. + """ + self.SetValue(value) + + + def GetMxDateTime(self, value=None): + if value is None: + t = self.GetValue(as_mxDateTime=True) + else: + # Convert string 1st to wxDateTime, then use components, since + # mx' DateTime.Parser.TimeFromString() doesn't handle AM/PM: + wxdt = self.GetWxDateTime(value) + hour, minute, second = wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond() + t = DateTime.DateTime(1970,1,1) + DateTimeDelta(0, hour, minute, second) + return t + + + def SetMin(self, min=None): + """ + Sets the minimum value of the control. If a value of None + is provided, then the control will have no explicit minimum value. + If the value specified is greater than the current maximum value, + then the function returns 0 and the minimum will not change from + its current setting. On success, the function returns 1. + + If successful and the current value is lower than the new lower + bound, if the control is limited, the value will be automatically + adjusted to the new minimum value; if not limited, the value in the + control will be colored as invalid. + """ + dbg('TimeCtrl::SetMin(%s)'% repr(min), indent=1) + if min is not None: + try: + min = self.GetWxDateTime(min) + self.__min = self._toGUI(min) + except: + dbg('exception occurred', indent=0) + return False + else: + self.__min = min + + if self.IsLimited() and not self.IsInBounds(): + self.SetLimited(self.__limited) # force limited value: + else: + self._CheckValid() + ret = True + dbg('ret:', ret, indent=0) + return ret + + + def GetMin(self, as_string = False): + """ + Gets the minimum value of the control. + If None, it will return None. Otherwise it will return + the current minimum bound on the control, as a wxDateTime + by default, or as a string if as_string argument is True. + """ + dbg(suspend=1) + dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) + if self.__min is None: + dbg('(min == None)') + ret = self.__min + elif as_string: + ret = self.__min + dbg('ret:', ret) + else: + try: + ret = self.GetWxDateTime(self.__min) + except: + dbg(suspend=0) + dbg('exception occurred', indent=0) + dbg('ret:', repr(ret)) + dbg(indent=0, suspend=0) + return ret + + + def SetMax(self, max=None): + """ + Sets the maximum value of the control. If a value of None + is provided, then the control will have no explicit maximum value. + If the value specified is less than the current minimum value, then + the function returns False and the maximum will not change from its + current setting. On success, the function returns True. + + If successful and the current value is greater than the new upper + bound, if the control is limited the value will be automatically + adjusted to this maximum value; if not limited, the value in the + control will be colored as invalid. + """ + dbg('TimeCtrl::SetMax(%s)' % repr(max), indent=1) + if max is not None: + try: + max = self.GetWxDateTime(max) + self.__max = self._toGUI(max) + except: + dbg('exception occurred', indent=0) + return False + else: + self.__max = max + dbg('max:', repr(self.__max)) + if self.IsLimited() and not self.IsInBounds(): + self.SetLimited(self.__limited) # force limited value: + else: + self._CheckValid() + ret = True + dbg('ret:', ret, indent=0) + return ret + + + def GetMax(self, as_string = False): + """ + Gets the minimum value of the control. + If None, it will return None. Otherwise it will return + the current minimum bound on the control, as a wxDateTime + by default, or as a string if as_string argument is True. + """ + dbg(suspend=1) + dbg('TimeCtrl::GetMin, as_string?', as_string, indent=1) + if self.__max is None: + dbg('(max == None)') + ret = self.__max + elif as_string: + ret = self.__max + dbg('ret:', ret) + else: + try: + ret = self.GetWxDateTime(self.__max) + except: + dbg(suspend=0) + dbg('exception occurred', indent=0) + raise + dbg('ret:', repr(ret)) + dbg(indent=0, suspend=0) + return ret + + + def SetBounds(self, min=None, max=None): + """ + This function is a convenience function for setting the min and max + values at the same time. The function only applies the maximum bound + if setting the minimum bound is successful, and returns True + only if both operations succeed. + NOTE: leaving out an argument will remove the corresponding bound. + """ + ret = self.SetMin(min) + return ret and self.SetMax(max) + + + def GetBounds(self, as_string = False): + """ + This function returns a two-tuple (min,max), indicating the + current bounds of the control. Each value can be None if + that bound is not set. + """ + return (self.GetMin(as_string), self.GetMax(as_string)) + + + def SetLimited(self, limited): + """ + If called with a value of True, this function will cause the control + to limit the value to fall within the bounds currently specified. + If the control's value currently exceeds the bounds, it will then + be limited accordingly. + + If called with a value of 0, this function will disable value + limiting, but coloring of out-of-bounds values will still take + place if bounds have been set for the control. + """ + dbg('TimeCtrl::SetLimited(%d)' % limited, indent=1) + self.__limited = limited + + if not limited: + self.SetMaskParameters(validRequired = False) + self._CheckValid() + dbg(indent=0) + return + + dbg('requiring valid value') + self.SetMaskParameters(validRequired = True) + + min = self.GetMin() + max = self.GetMax() + if min is None or max is None: + dbg('both bounds not set; no further action taken') + return # can't limit without 2 bounds + + elif not self.IsInBounds(): + # set value to the nearest bound: + try: + value = self.GetWxDateTime() + except: + dbg('exception occurred', indent=0) + raise + + if min <= max: # valid range doesn't span midnight + dbg('min <= max') + # which makes the "nearest bound" computation trickier... + + # determine how long the "invalid" pie wedge is, and cut + # this interval in half for comparison purposes: + + # Note: relies on min and max and value date portions + # always being the same. + interval = (min + wx.TimeSpan(24, 0, 0, 0)) - max + + half_interval = wx.TimeSpan( + 0, # hours + 0, # minutes + interval.GetSeconds() / 2, # seconds + 0) # msec + + if value < min: # min is on next day, so use value on + # "next day" for "nearest" interval calculation: + cmp_value = value + wx.TimeSpan(24, 0, 0, 0) + else: # "before midnight; ok + cmp_value = value + + if (cmp_value - max) > half_interval: + dbg('forcing value to min (%s)' % min.FormatTime()) + self.SetValue(min) + else: + dbg('forcing value to max (%s)' % max.FormatTime()) + self.SetValue(max) + else: + dbg('max < min') + # therefore max < value < min guaranteed to be true, + # so "nearest bound" calculation is much easier: + if (value - max) >= (min - value): + # current value closer to min; pick that edge of pie wedge + dbg('forcing value to min (%s)' % min.FormatTime()) + self.SetValue(min) + else: + dbg('forcing value to max (%s)' % max.FormatTime()) + self.SetValue(max) + + dbg(indent=0) + + + + def IsLimited(self): + """ + Returns True if the control is currently limiting the + value to fall within any current bounds. Note: can + be set even if there are no current bounds. + """ + return self.__limited + + + def IsInBounds(self, value=None): + """ + Returns True if no value is specified and the current value + of the control falls within the current bounds. As the clock + is a "circle", both minimum and maximum bounds must be set for + a value to ever be considered "out of bounds". This function can + also be called with a value to see if that value would fall within + the current bounds of the given control. + """ + if value is not None: + try: + value = self.GetWxDateTime(value) # try to regularize passed value + except ValueError: + dbg('ValueError getting wxDateTime for %s' % repr(value), indent=0) + raise + + dbg('TimeCtrl::IsInBounds(%s)' % repr(value), indent=1) + if self.__min is None or self.__max is None: + dbg(indent=0) + return True + + elif value is None: + try: + value = self.GetWxDateTime() + except: + dbg('exception occurred', indent=0) + + dbg('value:', value.FormatTime()) + + # Get wxDateTime representations of bounds: + min = self.GetMin() + max = self.GetMax() + + midnight = wx.DateTimeFromDMY(1, 0, 1970) + if min <= max: # they don't span midnight + ret = min <= value <= max + + else: + # have to break into 2 tests; to be in bounds + # either "min" <= value (<= midnight of *next day*) + # or midnight <= value <= "max" + ret = min <= value or (midnight <= value <= max) + dbg('in bounds?', ret, indent=0) + return ret + + + def IsValid( self, value ): + """ + Can be used to determine if a given value would be a legal and + in-bounds value for the control. + """ + try: + self.__validateValue(value) + return True + except ValueError: + return False + + def SetFormat(self, format): + self.SetParameters(format=format) + + def GetFormat(self): + if self.__displaySeconds: + if self.__fmt24hr: return '24HHMMSS' + else: return 'HHMMSS' + else: + if self.__fmt24hr: return '24HHMM' + else: return 'HHMM' + +#------------------------------------------------------------------------------------------------------------- +# these are private functions and overrides: + + + def __OnTextChange(self, event=None): + dbg('TimeCtrl::OnTextChange', indent=1) + + # Allow Maskedtext base control to color as appropriate, + # and Skip the EVT_TEXT event (if appropriate.) + ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue() + ## call is generating two (2) EVT_TEXT events. (!) + ## The the only mechanism I can find to mask this problem is to + ## keep track of last value seen, and declare a valid EVT_TEXT + ## event iff the value has actually changed. The masked edit + ## OnTextChange routine does this, and returns True on a valid event, + ## False otherwise. + if not BaseMaskedTextCtrl._OnTextChange(self, event): + return + + dbg('firing TimeUpdatedEvent...') + evt = TimeUpdatedEvent(self.GetId(), self.GetValue()) + evt.SetEventObject(self) + self.GetEventHandler().ProcessEvent(evt) + dbg(indent=0) + + + def SetInsertionPoint(self, pos): + """ + Records the specified position and associated cell before calling base class' function. + This is necessary to handle the optional spin button, because the insertion + point is lost when the focus shifts to the spin button. + """ + dbg('TimeCtrl::SetInsertionPoint', pos, indent=1) + BaseMaskedTextCtrl.SetInsertionPoint(self, pos) # (causes EVT_TEXT event to fire) + self.__posCurrent = self.GetInsertionPoint() + dbg(indent=0) + + + def SetSelection(self, sel_start, sel_to): + dbg('TimeCtrl::SetSelection', sel_start, sel_to, indent=1) + + # Adjust selection range to legal extent if not already + if sel_start < 0: + sel_start = 0 + + if self.__posCurrent != sel_start: # force selection and insertion point to match + self.SetInsertionPoint(sel_start) + cell_start, cell_end = self._FindField(sel_start)._extent + if not cell_start <= sel_to <= cell_end: + sel_to = cell_end + + self.__bSelection = sel_start != sel_to + BaseMaskedTextCtrl.SetSelection(self, sel_start, sel_to) + dbg(indent=0) + + + def __OnSpin(self, key): + """ + This is the function that gets called in response to up/down arrow or + bound spin button events. + """ + self.__IncrementValue(key, self.__posCurrent) # changes the value + + # Ensure adjusted control regains focus and has adjusted portion + # selected: + self.SetFocus() + start, end = self._FindField(self.__posCurrent)._extent + self.SetInsertionPoint(start) + self.SetSelection(start, end) + dbg('current position:', self.__posCurrent) + + + def __OnSpinUp(self, event): + """ + Event handler for any bound spin button on EVT_SPIN_UP; + causes control to behave as if up arrow was pressed. + """ + dbg('TimeCtrl::OnSpinUp', indent=1) + self.__OnSpin(wx.WXK_UP) + keep_processing = False + dbg(indent=0) + return keep_processing + + + def __OnSpinDown(self, event): + """ + Event handler for any bound spin button on EVT_SPIN_DOWN; + causes control to behave as if down arrow was pressed. + """ + dbg('TimeCtrl::OnSpinDown', indent=1) + self.__OnSpin(wx.WXK_DOWN) + keep_processing = False + dbg(indent=0) + return keep_processing + + + def __OnChar(self, event): + """ + Handler to explicitly look for ':' keyevents, and if found, + clear the m_shiftDown field, so it will behave as forward tab. + It then calls the base control's _OnChar routine with the modified + event instance. + """ + dbg('TimeCtrl::OnChar', indent=1) + keycode = event.GetKeyCode() + dbg('keycode:', keycode) + if keycode == ord(':'): + dbg('colon seen! removing shift attribute') + event.m_shiftDown = False + BaseMaskedTextCtrl._OnChar(self, event ) ## handle each keypress + dbg(indent=0) + + + def __OnSetToNow(self, event): + """ + This is the key handler for '!' and 'c'; this allows the user to + quickly set the value of the control to the current time. + """ + self.SetValue(wx.DateTime_Now().FormatTime()) + keep_processing = False + return keep_processing + + + def __LimitSelection(self, event): + """ + Event handler for motion events; this handler + changes limits the selection to the new cell boundaries. + """ + dbg('TimeCtrl::LimitSelection', indent=1) + pos = self.GetInsertionPoint() + self.__posCurrent = pos + sel_start, sel_to = self.GetSelection() + selection = sel_start != sel_to + if selection: + # only allow selection to end of current cell: + start, end = self._FindField(sel_start)._extent + if sel_to < pos: sel_to = start + elif sel_to > pos: sel_to = end + + dbg('new pos =', self.__posCurrent, 'select to ', sel_to) + self.SetInsertionPoint(self.__posCurrent) + self.SetSelection(self.__posCurrent, sel_to) + if event: event.Skip() + dbg(indent=0) + + + def __IncrementValue(self, key, pos): + dbg('TimeCtrl::IncrementValue', key, pos, indent=1) + text = self.GetValue() + field = self._FindField(pos) + dbg('field: ', field._index) + start, end = field._extent + slice = text[start:end] + if key == wx.WXK_UP: increment = 1 + else: increment = -1 + + if slice in ('A', 'P'): + if slice == 'A': newslice = 'P' + elif slice == 'P': newslice = 'A' + newvalue = text[:start] + newslice + text[end:] + + elif field._index == 0: + # adjusting this field is trickier, as its value can affect the + # am/pm setting. So, we use wxDateTime to generate a new value for us: + # (Use a fixed date not subject to DST variations:) + converter = wx.DateTimeFromDMY(1, 0, 1970) + dbg('text: "%s"' % text) + converter.ParseTime(text.strip()) + currenthour = converter.GetHour() + dbg('current hour:', currenthour) + newhour = (currenthour + increment) % 24 + dbg('newhour:', newhour) + converter.SetHour(newhour) + dbg('converter.GetHour():', converter.GetHour()) + newvalue = converter # take advantage of auto-conversion for am/pm in .SetValue() + + else: # minute or second field; handled the same way: + newslice = "%02d" % ((int(slice) + increment) % 60) + newvalue = text[:start] + newslice + text[end:] + + try: + self.SetValue(newvalue) + + except ValueError: # must not be in bounds: + if not wx.Validator_IsSilent(): + wx.Bell() + dbg(indent=0) + + + def _toGUI( self, wxdt ): + """ + This function takes a wxdt as an unambiguous representation of a time, and + converts it to a string appropriate for the format of the control. + """ + if self.__fmt24hr: + if self.__displaySeconds: strval = wxdt.Format('%H:%M:%S') + else: strval = wxdt.Format('%H:%M') + else: + if self.__displaySeconds: strval = wxdt.Format('%I:%M:%S %p') + else: strval = wxdt.Format('%I:%M %p') + + return strval + + + def __validateValue( self, value ): + """ + This function converts the value to a wxDateTime if not already one, + does bounds checking and raises ValueError if argument is + not a valid value for the control as currently specified. + It is used by both the SetValue() and the IsValid() methods. + """ + dbg('TimeCtrl::__validateValue(%s)' % repr(value), indent=1) + if not value: + dbg(indent=0) + raise ValueError('%s not a valid time value' % repr(value)) + + valid = True # assume true + try: + value = self.GetWxDateTime(value) # regularize form; can generate ValueError if problem doing so + except: + dbg('exception occurred', indent=0) + raise + + if self.IsLimited() and not self.IsInBounds(value): + dbg(indent=0) + raise ValueError ( + 'value %s is not within the bounds of the control' % str(value) ) + dbg(indent=0) + return value + +#---------------------------------------------------------------------------- +# Test jig for TimeCtrl: + +if __name__ == '__main__': + import traceback + + class TestPanel(wx.Panel): + def __init__(self, parent, id, + pos = wx.DefaultPosition, size = wx.DefaultSize, + fmt24hr = 0, test_mx = 0, + style = wx.TAB_TRAVERSAL ): + + wx.Panel.__init__(self, parent, id, pos, size, style) + + self.test_mx = test_mx + + self.tc = TimeCtrl(self, 10, fmt24hr = fmt24hr) + sb = wx.SpinButton( self, 20, wx.DefaultPosition, (-1,20), 0 ) + self.tc.BindSpinButton(sb) + + sizer = wx.BoxSizer( wx.HORIZONTAL ) + sizer.Add( self.tc, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.TOP|wx.BOTTOM, 5 ) + sizer.Add( sb, 0, wx.ALIGN_CENTRE|wx.RIGHT|wx.TOP|wx.BOTTOM, 5 ) + + self.SetAutoLayout( True ) + self.SetSizer( sizer ) + sizer.Fit( self ) + sizer.SetSizeHints( self ) + + self.Bind(EVT_TIMEUPDATE, self.OnTimeChange, self.tc) + + def OnTimeChange(self, event): + dbg('OnTimeChange: value = ', event.GetValue()) + wxdt = self.tc.GetWxDateTime() + dbg('wxdt =', wxdt.GetHour(), wxdt.GetMinute(), wxdt.GetSecond()) + if self.test_mx: + mxdt = self.tc.GetMxDateTime() + dbg('mxdt =', mxdt.hour, mxdt.minute, mxdt.second) + + + class MyApp(wx.App): + def OnInit(self): + import sys + fmt24hr = '24' in sys.argv + test_mx = 'mx' in sys.argv + try: + frame = wx.Frame(None, -1, "TimeCtrl Test", (20,20), (100,100) ) + panel = TestPanel(frame, -1, (-1,-1), fmt24hr=fmt24hr, test_mx = test_mx) + frame.Show(True) + except: + traceback.print_exc() + return False + return True + + try: + app = MyApp(0) + app.MainLoop() + except: + traceback.print_exc() +i=0 +## Version 1.2 +## 1. Changed parameter name display_seconds to displaySeconds, to follow +## other masked edit conventions. +## 2. Added format parameter, to remove need to use both fmt24hr and displaySeconds. +## 3. Changed inheritance to use BaseMaskedTextCtrl, to remove exposure of +## nonsensical parameter methods from the control, so it will work +## properly with Boa.