-#----------------------------------------------------------------------------
-# Name: maskededit.py
-# Authors: Jeff Childers, Will Sadkin
-# Email: jchilders_98@yahoo.com, wsadkin@nameconnector.com
-# Created: 02/11/2003
-# Copyright: (c) 2003 by Jeff Childers, Will Sadkin, 2003
-# Portions: (c) 2002 by Will Sadkin, 2002-2003
-# RCS-ID: $Id$
-# License: wxWidgets license
-#----------------------------------------------------------------------------
-# NOTE:
-# MaskedEdit controls are based on a suggestion made on [wxPython-Users] by
-# Jason Hihn, and borrows liberally from Will Sadkin's original masked edit
-# control for time entry, TimeCtrl (which is now rewritten using this
-# control!).
-#
-# MaskedEdit controls do not normally use validators, because they do
-# 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. However, validators can be
-# provided to do data transfer to the controls.
-#
-#----------------------------------------------------------------------------
-#
-# 12/09/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o Updated for wx namespace. No guarantees. This is one huge file.
-#
-# 12/13/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o Missed wx.DateTime stuff earlier.
-#
-# 12/20/2003 - Jeff Grimmett (grimmtooth@softhome.net)
-#
-# o wxMaskedEditMixin -> MaskedEditMixin
-# o wxMaskedTextCtrl -> MaskedTextCtrl
-# o wxMaskedComboBoxSelectEvent -> MaskedComboBoxSelectEvent
-# o wxMaskedComboBox -> MaskedComboBox
-# o wxIpAddrCtrl -> IpAddrCtrl
-# o wxTimeCtrl -> TimeCtrl
-#
-
-"""\
-<b>Masked Edit Overview:
-=====================</b>
-<b>MaskedTextCtrl</b>
- is a sublassed text control that can carefully control the user's input
- based on a mask string you provide.
-
- General usage example:
- control = MaskedTextCtrl( win, -1, '', mask = '(###) ###-####')
-
- The example above will create a text control that allows only numbers to be
- entered and then only in the positions indicated in the mask by the # sign.
-
-<b>MaskedComboBox</b>
- is a similar subclass of wxComboBox that allows the same sort of masking,
- but also can do auto-complete of values, and can require the value typed
- to be in the list of choices to be colored appropriately.
-
-<b>wxMaskedCtrl</b>
- is actually a factory function for several types of masked edit controls:
-
- <b>MaskedTextCtrl</b> - standard masked edit text box
- <b>MaskedComboBox</b> - adds combobox capabilities
- <b>IpAddrCtrl</b> - adds special semantics for IP address entry
- <b>TimeCtrl</b> - special subclass handling lots of types as values
- <b>wxMaskedNumCtrl</b> - special subclass handling numeric values
-
- It works by looking for a <b><i>controlType</i></b> parameter in the keyword
- arguments of the control, to determine what kind of instance to return.
- If not specified as a keyword argument, the default control type returned
- will be MaskedTextCtrl.
-
- Each of the above classes has its own set of arguments, but wxMaskedCtrl
- provides a single "unified" interface for masked controls. Those for
- MaskedTextCtrl, MaskedComboBox and IpAddrCtrl are all documented
- below; the others have their own demo pages and interface descriptions.
- (See end of following discussion for how to configure the wxMaskedCtrl()
- to select the above control types.)
-
-
-<b>INITILIZATION PARAMETERS
-========================
-mask=</b>
-Allowed mask characters and function:
- Character Function
- # Allow numeric only (0-9)
- N Allow letters and numbers (0-9)
- A Allow uppercase letters only
- a Allow lowercase letters only
- C Allow any letter, upper or lower
- X Allow string.letters, string.punctuation, string.digits
- & Allow string.punctuation only
-
-
- These controls define these sets of characters using string.letters,
- string.uppercase, etc. These sets are affected by the system locale
- setting, so in order to have the masked controls accept characters
- that are specific to your users' language, your application should
- set the locale.
- For example, to allow international characters to be used in the
- above masks, you can place the following in your code as part of
- your application's initialization code:
-
- import locale
- locale.setlocale(locale.LC_ALL, '')
-
-
- Using these mask characters, a variety of template masks can be built. See
- the demo for some other common examples include date+time, social security
- number, etc. If any of these characters are needed as template rather
- than mask characters, they can be escaped with \, ie. \N means "literal N".
- (use \\ for literal backslash, as in: r'CCC\\NNN'.)
-
-
- <b>Note:</b>
- Masks containing only # characters and one optional decimal point
- character are handled specially, as "numeric" controls. Such
- controls have special handling for typing the '-' key, handling
- the "decimal point" character as truncating the integer portion,
- optionally allowing grouping characters and so forth.
- There are several parameters and format codes that only make sense
- when combined with such masks, eg. groupChar, decimalChar, and so
- forth (see below). These allow you to construct reasonable
- numeric entry controls.
-
- <b>Note:</b>
- Changing the mask for a control deletes any previous field classes
- (and any associated validation or formatting constraints) for them.
-
-<b>useFixedWidthFont=</b>
- By default, masked edit controls use a fixed width font, so that
- the mask characters are fixed within the control, regardless of
- subsequent modifications to the value. Set to False if having
- the control font be the same as other controls is required.
-
-
-<b>formatcodes=</b>
- These other properties can be passed to the class when instantiating it:
- Formatcodes are specified as a string of single character formatting
- codes that modify behavior of the control:
- _ Allow spaces
- ! Force upper
- ^ Force lower
- R Right-align field(s)
- r Right-insert in field(s) (implies R)
- < Stay in field until explicit navigation out of it
-
- > Allow insert/delete within partially filled fields (as
- opposed to the default "overwrite" mode for fixed-width
- masked edit controls.) This allows single-field controls
- or each field within a multi-field control to optionally
- behave more like standard text controls.
- (See EMAIL or phone number autoformat examples.)
-
- <i>Note: This also governs whether backspace/delete operations
- shift contents of field to right of cursor, or just blank the
- erased section.
-
- Also, when combined with 'r', this indicates that the field
- or control allows right insert anywhere within the current
- non-empty value in the field. (Otherwise right-insert behavior
- is only performed to when the entire right-insertable field is
- selected or the cursor is at the right edge of the field.</i>
-
-
- , Allow grouping character in integer fields of numeric controls
- and auto-group/regroup digits (if the result fits) when leaving
- such a field. (If specified, .SetValue() will attempt to
- auto-group as well.)
- ',' is also the default grouping character. To change the
- grouping character and/or decimal character, use the groupChar
- and decimalChar parameters, respectively.
- Note: typing the "decimal point" character in such fields will
- clip the value to that left of the cursor for integer
- fields of controls with "integer" or "floating point" masks.
- If the ',' format code is specified, this will also cause the
- resulting digits to be regrouped properly, using the current
- grouping character.
- - Prepend and reserve leading space for sign to mask and allow
- signed values (negative #s shown in red by default.) Can be
- used with argument useParensForNegatives (see below.)
- 0 integer fields get leading zeros
- D Date[/time] field
- T Time field
- F Auto-Fit: the control calulates its size from
- the length of the template mask
- V validate entered chars against validRegex before allowing them
- to be entered vs. being allowed by basic mask and then having
- the resulting value just colored as invalid.
- (See USSTATE autoformat demo for how this can be used.)
- S select entire field when navigating to new field
-
-<b>fillChar=
-defaultValue=</b>
- These controls have two options for the initial state of the control.
- If a blank control with just the non-editable characters showing
- is desired, simply leave the constructor variable fillChar as its
- default (' '). If you want some other character there, simply
- change the fillChar to that value. Note: changing the control's fillChar
- will implicitly reset all of the fields' fillChars to this value.
-
- If you need different default characters in each mask position,
- you can specify a defaultValue parameter in the constructor, or
- set them for each field individually.
- This value must satisfy the non-editable characters of the mask,
- but need not conform to the replaceable characters.
-
-<b>groupChar=
-decimalChar=</b>
- These parameters govern what character is used to group numbers
- and is used to indicate the decimal point for numeric format controls.
- The default groupChar is ',', the default decimalChar is '.'
- By changing these, you can customize the presentation of numbers
- for your location.
- eg: formatcodes = ',', groupChar="'" allows 12'345.34
- formatcodes = ',', groupChar='.', decimalChar=',' allows 12.345,34
-
-<b>shiftDecimalChar=</b>
- The default "shiftDecimalChar" (used for "backwards-tabbing" until
- shift-tab is fixed in wxPython) is '>' (for QUERTY keyboards.) for
- other keyboards, you may want to customize this, eg '?' for shift ',' on
- AZERTY keyboards, ':' or ';' for other European keyboards, etc.
-
-<b>useParensForNegatives=False</b>
- This option can be used with signed numeric format controls to
- indicate signs via () rather than '-'.
-
-<b>autoSelect=False</b>
- This option can be used to have a field or the control try to
- auto-complete on each keystroke if choices have been specified.
-
-<b>autoCompleteKeycodes=[]</b>
- By default, DownArrow, PageUp and PageDown will auto-complete a
- partially entered field. Shift-DownArrow, Shift-UpArrow, PageUp
- and PageDown will also auto-complete, but if the field already
- contains a matched value, these keys will cycle through the list
- of choices forward or backward as appropriate. Shift-Up and
- Shift-Down also take you to the next/previous field after any
- auto-complete action.
-
- Additional auto-complete keys can be specified via this parameter.
- Any keys so specified will act like PageDown.
-
-
-
-<b>Validating User Input:
-======================</b>
- There are a variety of initialization parameters that are used to validate
- user input. These parameters can apply to the control as a whole, and/or
- to individual fields:
-
- excludeChars= A string of characters to exclude even if otherwise allowed
- includeChars= A string of characters to allow even if otherwise disallowed
- validRegex= Use a regular expression to validate the contents of the text box
- validRange= Pass a rangeas list (low,high) to limit numeric fields/values
- choices= A list of strings that are allowed choices for the control.
- choiceRequired= value must be member of choices list
- compareNoCase= Perform case-insensitive matching when validating against list
- <i>Note: for MaskedComboBox, this defaults to True.</i>
- emptyInvalid= Boolean indicating whether an empty value should be considered invalid
-
- validFunc= A function to call of the form: bool = func(candidate_value)
- which will return True if the candidate_value satisfies some
- external criteria for the control in addition to the the
- other validation, or False if not. (This validation is
- applied last in the chain of validations.)
-
- validRequired= Boolean indicating whether or not keys that are allowed by the
- mask, but result in an invalid value are allowed to be entered
- into the control. Setting this to True implies that a valid
- default value is set for the control.
-
- retainFieldValidation=
- False by default; if True, this allows individual fields to
- retain their own validation constraints independently of any
- subsequent changes to the control's overall parameters.
-
- validator= Validators are not normally needed for masked controls, because
- of the nature of the validation and control of input. However,
- you can supply one to provide data transfer routines for the
- controls.
-
-
-<b>Coloring Behavior:
-==================</b>
- The following parameters have been provided to allow you to change the default
- coloring behavior of the control. These can be set at construction, or via
- the .SetCtrlParameters() function. Pass a color as string e.g. 'Yellow':
-
- emptyBackgroundColour= Control Background color when identified as empty. Default=White
- invalidBackgroundColour= Control Background color when identified as Not valid. Default=Yellow
- validBackgroundColour= Control Background color when identified as Valid. Default=white
-
-
- The following parameters control the default foreground color coloring behavior of the
- control. Pass a color as string e.g. 'Yellow':
- foregroundColour= Control foreground color when value is not negative. Default=Black
- signedForegroundColour= Control foreground color when value is negative. Default=Red
-
-
-<b>Fields:
-=======</b>
- Each part of the mask that allows user input is considered a field. The fields
- are represented by their own class instances. You can specify field-specific
- constraints by constructing or accessing the field instances for the control
- and then specifying those constraints via parameters.
-
-<b>fields=</b>
- This parameter allows you to specify Field instances containing
- constraints for the individual fields of a control, eg: local
- choice lists, validation rules, functions, regexps, etc.
- It can be either an ordered list or a dictionary. If a list,
- the fields will be applied as fields 0, 1, 2, etc.
- If a dictionary, it should be keyed by field index.
- the values should be a instances of maskededit.Field.
-
- Any field not represented by the list or dictionary will be
- implicitly created by the control.
-
- eg:
- fields = [ Field(formatcodes='_r'), Field('choices=['a', 'b', 'c']) ]
- or
- fields = {
- 1: ( Field(formatcodes='_R', choices=['a', 'b', 'c']),
- 3: ( Field(choices=['01', '02', '03'], choiceRequired=True)
- }
-
- The following parameters are available for individual fields, with the
- same semantics as for the whole control but applied to the field in question:
-
- fillChar # if set for a field, it will override the control's fillChar for that field
- groupChar # if set for a field, it will override the control's default
- defaultValue # sets field-specific default value; overrides any default from control
- compareNoCase # overrides control's settings
- emptyInvalid # determines whether field is required to be filled at all times
- validRequired # if set, requires field to contain valid value
-
- If any of the above parameters are subsequently specified for the control as a
- whole, that new value will be propagated to each field, unless the
- retainFieldValidation control-level parameter is set.
-
- formatcodes # Augments control's settings
- excludeChars # ' ' '
- includeChars # ' ' '
- validRegex # ' ' '
- validRange # ' ' '
- choices # ' ' '
- choiceRequired # ' ' '
- validFunc # ' ' '
-
-
-
-<b>Control Class Functions:
-========================
- .GetPlainValue(value=None)</b>
- Returns the value specified (or the control's text value
- not specified) without the formatting text.
- In the example above, might return phone no='3522640075',
- whereas control.GetValue() would return '(352) 264-0075'
- <b>.ClearValue()</b>
- Returns the control's value to its default, and places the
- cursor at the beginning of the control.
- <b>.SetValue()</b>
- Does "smart replacement" of passed value into the control, as does
- the .Paste() method. As with other text entry controls, the
- .SetValue() text replacement begins at left-edge of the control,
- with missing mask characters inserted as appropriate.
- .SetValue will also adjust integer, float or date mask entry values,
- adding commas, auto-completing years, etc. as appropriate.
- For "right-aligned" numeric controls, it will also now automatically
- right-adjust any value whose length is less than the width of the
- control before attempting to set the value.
- If a value does not follow the format of the control's mask, or will
- not fit into the control, a ValueError exception will be raised.
- Eg:
- mask = '(###) ###-####'
- .SetValue('1234567890') => '(123) 456-7890'
- .SetValue('(123)4567890') => '(123) 456-7890'
- .SetValue('(123)456-7890') => '(123) 456-7890'
- .SetValue('123/4567-890') => illegal paste; ValueError
-
- mask = '#{6}.#{2}', formatcodes = '_,-',
- .SetValue('111') => ' 111 . '
- .SetValue(' %9.2f' % -111.12345 ) => ' -111.12'
- .SetValue(' %9.2f' % 1234.00 ) => ' 1,234.00'
- .SetValue(' %9.2f' % -1234567.12345 ) => insufficient room; ValueError
-
- mask = '#{6}.#{2}', formatcodes = '_,-R' # will right-adjust value for right-aligned control
- .SetValue('111') => padded value misalignment ValueError: " 111" will not fit
- .SetValue('%.2f' % 111 ) => ' 111.00'
- .SetValue('%.2f' % -111.12345 ) => ' -111.12'
-
-
- <b>.IsValid(value=None)</b>
- Returns True if the value specified (or the value of the control
- if not specified) passes validation tests
- <b>.IsEmpty(value=None)</b>
- Returns True if the value specified (or the value of the control
- if not specified) is equal to an "empty value," ie. all
- editable characters == the fillChar for their respective fields.
- <b>.IsDefault(value=None)</b>
- Returns True if the value specified (or the value of the control
- if not specified) is equal to the initial value of the control.
-
- <b>.Refresh()</b>
- Recolors the control as appropriate to its current settings.
-
- <b>.SetCtrlParameters(**kwargs)</b>
- This function allows you to set up and/or change the control parameters
- after construction; it takes a list of key/value pairs as arguments,
- where the keys can be any of the mask-specific parameters in the constructor.
- Eg:
- ctl = MaskedTextCtrl( self, -1 )
- ctl.SetCtrlParameters( mask='###-####',
- defaultValue='555-1212',
- formatcodes='F')
-
- <b>.GetCtrlParameter(parametername)</b>
- This function allows you to retrieve the current value of a parameter
- from the control.
-
- <b><i>Note:</i></b> Each of the control parameters can also be set using its
- own Set and Get function. These functions follow a regular form:
- All of the parameter names start with lower case; for their
- corresponding Set/Get function, the parameter name is capitalized.
- Eg: ctl.SetMask('###-####')
- ctl.SetDefaultValue('555-1212')
- ctl.GetChoiceRequired()
- ctl.GetFormatcodes()
-
- <b><i>Note:</i></b> After any change in parameters, the choices for the
- control are reevaluated to ensure that they are still legal. If you
- have large choice lists, it is therefore more efficient to set parameters
- before setting the choices available.
-
- <b>.SetFieldParameters(field_index, **kwargs)</b>
- This function allows you to specify change individual field
- parameters after construction. (Indices are 0-based.)
-
- <b>.GetFieldParameter(field_index, parametername)</b>
- Allows the retrieval of field parameters after construction
-
-
-The control detects certain common constructions. In order to use the signed feature
-(negative numbers and coloring), the mask has to be all numbers with optionally one
-decimal point. Without a decimal (e.g. '######', the control will treat it as an integer
-value. With a decimal (e.g. '###.##'), the control will act as a floating point control
-(i.e. press decimal to 'tab' to the decimal position). Pressing decimal in the
-integer control truncates the value. However, for a true numeric control,
-MaskedNumCtrl provides all this, and true numeric input/output support as well.
-
-
-Check your controls by calling each control's .IsValid() function and the
-.IsEmpty() function to determine which controls have been a) filled in and
-b) filled in properly.
-
-
-Regular expression validations can be used flexibly and creatively.
-Take a look at the demo; the zip-code validation succeeds as long as the
-first five numerals are entered. the last four are optional, but if
-any are entered, there must be 4 to be valid.
-
-<B>wxMaskedCtrl Configuration
-==========================</B>
-wxMaskedCtrl works by looking for a special <b><i>controlType</i></b>
-parameter in the variable arguments of the control, to determine
-what kind of instance to return.
-controlType can be one of:
-
- controlTypes.MASKEDTEXT
- controlTypes.MASKEDCOMBO
- controlTypes.IPADDR
- controlTypes.TIME
- controlTypes.NUMBER
-
-These constants are also available individually, ie, you can
-use either of the following:
-
- from wxPython.wx.lib.maskedctrl import wxMaskedCtrl, controlTypes
- from wxPython.wx.lib.maskedctrl import wxMaskedCtrl, MASKEDCOMBO, MASKEDTEXT, NUMBER
-
-If not specified as a keyword argument, the default controlType is
-controlTypes.TEXT.
-"""
-
-"""
-+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-DEVELOPER COMMENTS:
-
-Naming Conventions
-------------------
- All methods of the Mixin that are not meant to be exposed to the external
- interface are prefaced with '_'. Those functions that are primarily
- intended to be internal subroutines subsequently start with a lower-case
- letter; those that are primarily intended to be used and/or overridden
- by derived subclasses start with a capital letter.
-
- The following methods must be used and/or defined when deriving a control
- from wxMaskedEditMixin. NOTE: if deriving from a *masked edit* control
- (eg. class IpAddrCtrl(MaskedTextCtrl) ), then this is NOT necessary,
- as it's already been done for you in the base class.
-
- ._SetInitialValue()
- This function must be called after the associated base
- control has been initialized in the subclass __init__
- function. It sets the initial value of the control,
- either to the value specified if non-empty, the
- default value if specified, or the "template" for
- the empty control as necessary. It will also set/reset
- the font if necessary and apply formatting to the
- control at this time.
-
- ._GetSelection()
- REQUIRED
- Each class derived from wxMaskedEditMixin must define
- the function for getting the start and end of the
- current text selection. The reason for this is
- that not all controls have the same function name for
- doing this; eg. wxTextCtrl uses .GetSelection(),
- whereas we had to write a .GetMark() function for
- wxComboBox, because .GetSelection() for the control
- gets the currently selected list item from the combo
- box, and the control doesn't (yet) natively provide
- a means of determining the text selection.
- ._SetSelection()
- REQUIRED
- Similarly to _GetSelection, each class derived from
- wxMaskedEditMixin must define the function for setting
- the start and end of the current text selection.
- (eg. .SetSelection() for MaskedTextCtrl, and .SetMark() for
- MaskedComboBox.
-
- ._GetInsertionPoint()
- ._SetInsertionPoint()
- REQUIRED
- For consistency, and because the mixin shouldn't rely
- on fixed names for any manipulations it does of any of
- the base controls, we require each class derived from
- wxMaskedEditMixin to define these functions as well.
-
- ._GetValue()
- ._SetValue() REQUIRED
- Each class derived from wxMaskedEditMixin must define
- the functions used to get and set the raw value of the
- control.
- This is necessary so that recursion doesn't take place
- when setting the value, and so that the mixin can
- call the appropriate function after doing all its
- validation and manipulation without knowing what kind
- of base control it was mixed in with. To handle undo
- functionality, the ._SetValue() must record the current
- selection prior to setting the value.
-
- .Cut()
- .Paste()
- .Undo()
- .SetValue() REQUIRED
- Each class derived from wxMaskedEditMixin must redefine
- these functions to call the _Cut(), _Paste(), _Undo()
- and _SetValue() methods, respectively for the control,
- so as to prevent programmatic corruption of the control's
- value. This must be done in each derivation, as the
- mixin cannot itself override a member of a sibling class.
-
- ._Refresh() REQUIRED
- Each class derived from wxMaskedEditMixin must define
- the function used to refresh the base control.
-
- .Refresh() REQUIRED
- Each class derived from wxMaskedEditMixin must redefine
- this function so that it checks the validity of the
- control (via self._CheckValid) and then refreshes
- control using the base class method.
-
- ._IsEditable() REQUIRED
- Each class derived from wxMaskedEditMixin must define
- the function used to determine if the base control is
- editable or not. (For MaskedComboBox, this has to
- be done with code, rather than specifying the proper
- function in the base control, as there isn't one...)
- ._CalcSize() REQUIRED
- Each class derived from wxMaskedEditMixin must define
- the function used to determine how wide the control
- should be given the mask. (The mixin function
- ._calcSize() provides a baseline estimate.)
-
-
-Event Handling
---------------
- Event handlers are "chained", and wxMaskedEditMixin usually
- swallows most of the events it sees, thereby preventing any other
- handlers from firing in the chain. It is therefore required that
- each class derivation using the mixin to have an option to hook up
- the event handlers itself or forego this operation and let a
- subclass of the masked control do so. For this reason, each
- subclass should probably include the following code:
-
- if setupEventHandling:
- ## Setup event handlers
- EVT_SET_FOCUS( self, self._OnFocus ) ## defeat automatic full selection
- EVT_KILL_FOCUS( self, self._OnKillFocus ) ## run internal validator
- EVT_LEFT_DCLICK(self, self._OnDoubleClick) ## select field under cursor on dclick
- EVT_RIGHT_UP(self, self._OnContextMenu ) ## bring up an appropriate context menu
- EVT_KEY_DOWN( self, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab.
- EVT_CHAR( self, self._OnChar ) ## handle each keypress
- EVT_TEXT( self, self.GetId(), self._OnTextChange ) ## color control appropriately & keep
- ## track of previous value for undo
-
- where setupEventHandling is an argument to its constructor.
-
- These 5 handlers must be "wired up" for the wxMaskedEdit
- control to provide default behavior. (The setupEventHandling
- is an argument to MaskedTextCtrl and MaskedComboBox, so
- that controls derived from *them* may replace one of these
- handlers if they so choose.)
-
- If your derived control wants to preprocess events before
- taking action, it should then set up the event handling itself,
- so it can be first in the event handler chain.
-
-
- The following routines are available to facilitate changing
- the default behavior of wxMaskedEdit controls:
-
- ._SetKeycodeHandler(keycode, func)
- ._SetKeyHandler(char, func)
- Use to replace default handling for any given keycode.
- func should take the key event as argument and return
- False if no further action is required to handle the
- key. Eg:
- self._SetKeycodeHandler(WXK_UP, self.IncrementValue)
- self._SetKeyHandler('-', self._OnChangeSign)
-
- "Navigation" keys are assumed to change the cursor position, and
- therefore don't cause automatic motion of the cursor as insertable
- characters do.
-
- ._AddNavKeycode(keycode, handler=None)
- ._AddNavKey(char, handler=None)
- Allows controls to specify other keys (and optional handlers)
- to be treated as navigational characters. (eg. '.' in IpAddrCtrl)
-
- ._GetNavKeycodes() Returns the current list of navigational keycodes.
-
- ._SetNavKeycodes(key_func_tuples)
- Allows replacement of the current list of keycode
- processed as navigation keys, and bind associated
- optional keyhandlers. argument is a list of key/handler
- tuples. Passing a value of None for the handler in a
- given tuple indicates that default processing for the key
- is desired.
-
- ._FindField(pos) Returns the Field object associated with this position
- in the control.
-
- ._FindFieldExtent(pos, getslice=False, value=None)
- Returns edit_start, edit_end of the field corresponding
- to the specified position within the control, and
- optionally also returns the current contents of that field.
- If value is specified, it will retrieve the slice the corresponding
- slice from that value, rather than the current value of the
- control.
-
- ._AdjustField(pos)
- This is, the function that gets called for a given position
- whenever the cursor is adjusted to leave a given field.
- By default, it adjusts the year in date fields if mask is a date,
- It can be overridden by a derived class to
- adjust the value of the control at that time.
- (eg. IpAddrCtrl reformats the address in this way.)
-
- ._Change() Called by internal EVT_TEXT handler. Return False to force
- skip of the normal class change event.
- ._Keypress(key) Called by internal EVT_CHAR handler. Return False to force
- skip of the normal class keypress event.
- ._LostFocus() Called by internal EVT_KILL_FOCUS handler
-
- ._OnKeyDown(event)
- This is the default EVT_KEY_DOWN routine; it just checks for
- "navigation keys", and if event.ControlDown(), it fires the
- mixin's _OnChar() routine, as such events are not always seen
- by the "cooked" EVT_CHAR routine.
-
- ._OnChar(event) This is the main EVT_CHAR handler for the
- wxMaskedEditMixin.
-
- The following routines are used to handle standard actions
- for control keys:
- _OnArrow(event) used for arrow navigation events
- _OnCtrl_A(event) 'select all'
- _OnCtrl_C(event) 'copy' (uses base control function, as copy is non-destructive)
- _OnCtrl_S(event) 'save' (does nothing)
- _OnCtrl_V(event) 'paste' - calls _Paste() method, to do smart paste
- _OnCtrl_X(event) 'cut' - calls _Cut() method, to "erase" selection
- _OnCtrl_Z(event) 'undo' - resets value to previous value (if any)
-
- _OnChangeField(event) primarily used for tab events, but can be
- used for other keys (eg. '.' in IpAddrCtrl)
-
- _OnErase(event) used for backspace and delete
- _OnHome(event)
- _OnEnd(event)
-
- The following routine provides a hook back to any class derivations, so that
- they can react to parameter changes before any value is set/reset as a result of
- those changes. (eg. MaskedComboBox needs to detect when the choices list is
- modified, either implicitly or explicitly, so it can reset the base control
- to have the appropriate choice list *before* the initial value is reset to match.)
-
- _OnCtrlParametersChanged()
-
-Accessor Functions
-------------------
- For convenience, each class derived from MaskedEditMixin should
- define an accessors mixin, so that it exposes only those parameters
- that make sense for the derivation. This is done with an intermediate
- level of inheritance, ie:
-
- class BaseMaskedTextCtrl( TextCtrl, MaskedEditMixin ):
-
- class MaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ):
- class MaskedNumCtrl( BaseMaskedTextCtrl, MaskedNumCtrlAccessorsMixin ):
- class IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ):
- class TimeCtrl( BaseMaskedTextCtrl, TimeCtrlAccessorsMixin ):
-
- etc.
-
- Each accessors mixin defines Get/Set functions for the base class parameters
- that are appropriate for that derivation.
- This allows the base classes to be "more generic," exposing the widest
- set of options, while not requiring derived classes to be so general.
-"""
-
-import copy
-import difflib
-import re
-import string
-import types
-
-import wx
-
-# jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would
-# be a good place to implement the 2.3 logger class
-from wx.tools.dbg import Logger
-
-dbg = Logger()
-##dbg(enable=0)
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-## Constants for identifying control keys and classes of keys:
-
-WXK_CTRL_A = (ord('A')+1) - ord('A') ## These keys are not already defined in wx
-WXK_CTRL_C = (ord('C')+1) - ord('A')
-WXK_CTRL_S = (ord('S')+1) - ord('A')
-WXK_CTRL_V = (ord('V')+1) - ord('A')
-WXK_CTRL_X = (ord('X')+1) - ord('A')
-WXK_CTRL_Z = (ord('Z')+1) - ord('A')
-
-nav = (
- wx.WXK_BACK, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP, wx.WXK_DOWN, wx.WXK_TAB,
- wx.WXK_HOME, wx.WXK_END, wx.WXK_RETURN, wx.WXK_PRIOR, wx.WXK_NEXT
- )
-
-control = (
- wx.WXK_BACK, wx.WXK_DELETE, WXK_CTRL_A, WXK_CTRL_C, WXK_CTRL_S, WXK_CTRL_V,
- WXK_CTRL_X, WXK_CTRL_Z
- )
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-## Constants for masking. This is where mask characters
-## are defined.
-## maskchars used to identify valid mask characters from all others
-## #- allow numeric 0-9 only
-## A- allow uppercase only. Combine with forceupper to force lowercase to upper
-## a- allow lowercase only. Combine with forcelower to force upper to lowercase
-## X- allow any character (string.letters, string.punctuation, string.digits)
-## Note: locale settings affect what "uppercase", lowercase, etc comprise.
-##
-maskchars = ("#","A","a","X","C","N", '&')
-
-months = '(01|02|03|04|05|06|07|08|09|10|11|12)'
-charmonths = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)'
-charmonths_dict = {'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
- 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12}
-
-days = '(01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)'
-hours = '(0\d| \d|1[012])'
-milhours = '(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23)'
-minutes = """(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\
-16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|\
-36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|\
-56|57|58|59)"""
-seconds = minutes
-am_pm_exclude = 'BCDEFGHIJKLMNOQRSTUVWXYZ\x8a\x8c\x8e\x9f\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd8\xd9\xda\xdb\xdc\xdd\xde'
-
-states = "AL,AK,AZ,AR,CA,CO,CT,DE,DC,FL,GA,GU,HI,ID,IL,IN,IA,KS,KY,LA,MA,ME,MD,MI,MN,MS,MO,MT,NE,NV,NH,NJ,NM,NY,NC,ND,OH,OK,OR,PA,PR,RI,SC,SD,TN,TX,UT,VA,VT,VI,WA,WV,WI,WY".split(',')
-
-state_names = ['Alabama','Alaska','Arizona','Arkansas',
- 'California','Colorado','Connecticut',
- 'Delaware','District of Columbia',
- 'Florida','Georgia','Hawaii',
- 'Idaho','Illinois','Indiana','Iowa',
- 'Kansas','Kentucky','Louisiana',
- 'Maine','Maryland','Massachusetts','Michigan',
- 'Minnesota','Mississippi','Missouri','Montana',
- 'Nebraska','Nevada','New Hampshire','New Jersey',
- 'New Mexico','New York','North Carolina','North Dakokta',
- 'Ohio','Oklahoma','Oregon',
- 'Pennsylvania','Puerto Rico','Rhode Island',
- 'South Carolina','South Dakota',
- 'Tennessee','Texas','Utah',
- 'Vermont','Virginia',
- 'Washington','West Virginia',
- 'Wisconsin','Wyoming']
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-## The following dictionary defines the current set of autoformats:
-
-masktags = {
- "USPHONEFULLEXT": {
- 'mask': "(###) ###-#### x:###",
- 'formatcodes': 'F^->',
- 'validRegex': "^\(\d{3}\) \d{3}-\d{4}",
- 'description': "Phone Number w/opt. ext"
- },
- "USPHONETIGHTEXT": {
- 'mask': "###-###-#### x:###",
- 'formatcodes': 'F^->',
- 'validRegex': "^\d{3}-\d{3}-\d{4}",
- 'description': "Phone Number\n (w/hyphens and opt. ext)"
- },
- "USPHONEFULL": {
- 'mask': "(###) ###-####",
- 'formatcodes': 'F^->',
- 'validRegex': "^\(\d{3}\) \d{3}-\d{4}",
- 'description': "Phone Number only"
- },
- "USPHONETIGHT": {
- 'mask': "###-###-####",
- 'formatcodes': 'F^->',
- 'validRegex': "^\d{3}-\d{3}-\d{4}",
- 'description': "Phone Number\n(w/hyphens)"
- },
- "USSTATE": {
- 'mask': "AA",
- 'formatcodes': 'F!V',
- 'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(states,'|'),
- 'choices': states,
- 'choiceRequired': True,
- 'description': "US State Code"
- },
- "USSTATENAME": {
- 'mask': "ACCCCCCCCCCCCCCCCCCC",
- 'formatcodes': 'F_',
- 'validRegex': "([ACDFGHIKLMNOPRSTUVW] |%s)" % string.join(state_names,'|'),
- 'choices': state_names,
- 'choiceRequired': True,
- 'description': "US State Name"
- },
-
- "USDATETIMEMMDDYYYY/HHMMSS": {
- 'mask': "##/##/#### ##:##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
- 'description': "US Date + Time"
- },
- "USDATETIMEMMDDYYYY-HHMMSS": {
- 'mask': "##-##-#### ##:##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
- 'description': "US Date + Time\n(w/hypens)"
- },
- "USDATE24HRTIMEMMDDYYYY/HHMMSS": {
- 'mask': "##/##/#### ##:##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
- 'description': "US Date + 24Hr (Military) Time"
- },
- "USDATE24HRTIMEMMDDYYYY-HHMMSS": {
- 'mask': "##-##-#### ##:##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
- 'description': "US Date + 24Hr Time\n(w/hypens)"
- },
- "USDATETIMEMMDDYYYY/HHMM": {
- 'mask': "##/##/#### ##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
- 'description': "US Date + Time\n(without seconds)"
- },
- "USDATE24HRTIMEMMDDYYYY/HHMM": {
- 'mask': "##/##/#### ##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + months + '/' + days + '/' + '\d{4} ' + milhours + ':' + minutes,
- 'description': "US Date + 24Hr Time\n(without seconds)"
- },
- "USDATETIMEMMDDYYYY-HHMM": {
- 'mask': "##-##-#### ##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
- 'description': "US Date + Time\n(w/hypens and w/o secs)"
- },
- "USDATE24HRTIMEMMDDYYYY-HHMM": {
- 'mask': "##-##-#### ##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + months + '-' + days + '-' + '\d{4} ' + milhours + ':' + minutes,
- 'description': "US Date + 24Hr Time\n(w/hyphens and w/o seconds)"
- },
- "USDATEMMDDYYYY/": {
- 'mask': "##/##/####",
- 'formatcodes': 'DF',
- 'validRegex': '^' + months + '/' + days + '/' + '\d{4}',
- 'description': "US Date\n(MMDDYYYY)"
- },
- "USDATEMMDDYY/": {
- 'mask': "##/##/##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + months + '/' + days + '/\d\d',
- 'description': "US Date\n(MMDDYY)"
- },
- "USDATEMMDDYYYY-": {
- 'mask': "##-##-####",
- 'formatcodes': 'DF',
- 'validRegex': '^' + months + '-' + days + '-' +'\d{4}',
- 'description': "MM-DD-YYYY"
- },
-
- "EUDATEYYYYMMDD/": {
- 'mask': "####/##/##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days,
- 'description': "YYYY/MM/DD"
- },
- "EUDATEYYYYMMDD.": {
- 'mask': "####.##.##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days,
- 'description': "YYYY.MM.DD"
- },
- "EUDATEDDMMYYYY/": {
- 'mask': "##/##/####",
- 'formatcodes': 'DF',
- 'validRegex': '^' + days + '/' + months + '/' + '\d{4}',
- 'description': "DD/MM/YYYY"
- },
- "EUDATEDDMMYYYY.": {
- 'mask': "##.##.####",
- 'formatcodes': 'DF',
- 'validRegex': '^' + days + '.' + months + '.' + '\d{4}',
- 'description': "DD.MM.YYYY"
- },
- "EUDATEDDMMMYYYY.": {
- 'mask': "##.CCC.####",
- 'formatcodes': 'DF',
- 'validRegex': '^' + days + '.' + charmonths + '.' + '\d{4}',
- 'description': "DD.Month.YYYY"
- },
- "EUDATEDDMMMYYYY/": {
- 'mask': "##/CCC/####",
- 'formatcodes': 'DF',
- 'validRegex': '^' + days + '/' + charmonths + '/' + '\d{4}',
- 'description': "DD/Month/YYYY"
- },
-
- "EUDATETIMEYYYYMMDD/HHMMSS": {
- 'mask': "####/##/## ##:##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
- 'description': "YYYY/MM/DD HH:MM:SS"
- },
- "EUDATETIMEYYYYMMDD.HHMMSS": {
- 'mask': "####.##.## ##:##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
- 'description': "YYYY.MM.DD HH:MM:SS"
- },
- "EUDATETIMEDDMMYYYY/HHMMSS": {
- 'mask': "##/##/#### ##:##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
- 'description': "DD/MM/YYYY HH:MM:SS"
- },
- "EUDATETIMEDDMMYYYY.HHMMSS": {
- 'mask': "##.##.#### ##:##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
- 'description': "DD.MM.YYYY HH:MM:SS"
- },
-
- "EUDATETIMEYYYYMMDD/HHMM": {
- 'mask': "####/##/## ##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + hours + ':' + minutes + ' (A|P)M',
- 'description': "YYYY/MM/DD HH:MM"
- },
- "EUDATETIMEYYYYMMDD.HHMM": {
- 'mask': "####.##.## ##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + hours + ':' + minutes + ' (A|P)M',
- 'description': "YYYY.MM.DD HH:MM"
- },
- "EUDATETIMEDDMMYYYY/HHMM": {
- 'mask': "##/##/#### ##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
- 'description': "DD/MM/YYYY HH:MM"
- },
- "EUDATETIMEDDMMYYYY.HHMM": {
- 'mask': "##.##.#### ##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'DF!',
- 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + hours + ':' + minutes + ' (A|P)M',
- 'description': "DD.MM.YYYY HH:MM"
- },
-
- "EUDATE24HRTIMEYYYYMMDD/HHMMSS": {
- 'mask': "####/##/## ##:##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes + ':' + seconds,
- 'description': "YYYY/MM/DD 24Hr Time"
- },
- "EUDATE24HRTIMEYYYYMMDD.HHMMSS": {
- 'mask': "####.##.## ##:##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes + ':' + seconds,
- 'description': "YYYY.MM.DD 24Hr Time"
- },
- "EUDATE24HRTIMEDDMMYYYY/HHMMSS": {
- 'mask': "##/##/#### ##:##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
- 'description': "DD/MM/YYYY 24Hr Time"
- },
- "EUDATE24HRTIMEDDMMYYYY.HHMMSS": {
- 'mask': "##.##.#### ##:##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes + ':' + seconds,
- 'description': "DD.MM.YYYY 24Hr Time"
- },
- "EUDATE24HRTIMEYYYYMMDD/HHMM": {
- 'mask': "####/##/## ##:##",
- 'formatcodes': 'DF','validRegex': '^' + '\d{4}'+ '/' + months + '/' + days + ' ' + milhours + ':' + minutes,
- 'description': "YYYY/MM/DD 24Hr Time\n(w/o seconds)"
- },
- "EUDATE24HRTIMEYYYYMMDD.HHMM": {
- 'mask': "####.##.## ##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + '\d{4}'+ '.' + months + '.' + days + ' ' + milhours + ':' + minutes,
- 'description': "YYYY.MM.DD 24Hr Time\n(w/o seconds)"
- },
- "EUDATE24HRTIMEDDMMYYYY/HHMM": {
- 'mask': "##/##/#### ##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + days + '/' + months + '/' + '\d{4} ' + milhours + ':' + minutes,
- 'description': "DD/MM/YYYY 24Hr Time\n(w/o seconds)"
- },
- "EUDATE24HRTIMEDDMMYYYY.HHMM": {
- 'mask': "##.##.#### ##:##",
- 'formatcodes': 'DF',
- 'validRegex': '^' + days + '.' + months + '.' + '\d{4} ' + milhours + ':' + minutes,
- 'description': "DD.MM.YYYY 24Hr Time\n(w/o seconds)"
- },
-
- "TIMEHHMMSS": {
- 'mask': "##:##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'TF!',
- 'validRegex': '^' + hours + ':' + minutes + ':' + seconds + ' (A|P)M',
- 'description': "HH:MM:SS (A|P)M\n(see TimeCtrl)"
- },
- "TIMEHHMM": {
- 'mask': "##:## AM",
- 'excludeChars': am_pm_exclude,
- 'formatcodes': 'TF!',
- 'validRegex': '^' + hours + ':' + minutes + ' (A|P)M',
- 'description': "HH:MM (A|P)M\n(see TimeCtrl)"
- },
- "24HRTIMEHHMMSS": {
- 'mask': "##:##:##",
- 'formatcodes': 'TF',
- 'validRegex': '^' + milhours + ':' + minutes + ':' + seconds,
- 'description': "24Hr HH:MM:SS\n(see TimeCtrl)"
- },
- "24HRTIMEHHMM": {
- 'mask': "##:##",
- 'formatcodes': 'TF',
- 'validRegex': '^' + milhours + ':' + minutes,
- 'description': "24Hr HH:MM\n(see TimeCtrl)"
- },
- "USSOCIALSEC": {
- 'mask': "###-##-####",
- 'formatcodes': 'F',
- 'validRegex': "\d{3}-\d{2}-\d{4}",
- 'description': "Social Sec#"
- },
- "CREDITCARD": {
- 'mask': "####-####-####-####",
- 'formatcodes': 'F',
- 'validRegex': "\d{4}-\d{4}-\d{4}-\d{4}",
- 'description': "Credit Card"
- },
- "EXPDATEMMYY": {
- 'mask': "##/##",
- 'formatcodes': "F",
- 'validRegex': "^" + months + "/\d\d",
- 'description': "Expiration MM/YY"
- },
- "USZIP": {
- 'mask': "#####",
- 'formatcodes': 'F',
- 'validRegex': "^\d{5}",
- 'description': "US 5-digit zip code"
- },
- "USZIPPLUS4": {
- 'mask': "#####-####",
- 'formatcodes': 'F',
- 'validRegex': "\d{5}-(\s{4}|\d{4})",
- 'description': "US zip+4 code"
- },
- "PERCENT": {
- 'mask': "0.##",
- 'formatcodes': 'F',
- 'validRegex': "^0.\d\d",
- 'description': "Percentage"
- },
- "AGE": {
- 'mask': "###",
- 'formatcodes': "F",
- 'validRegex': "^[1-9]{1} |[1-9][0-9] |1[0|1|2][0-9]",
- 'description': "Age"
- },
- "EMAIL": {
- 'mask': "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
- 'excludeChars': " \\/*&%$#!+='\"",
- 'formatcodes': "F>",
- 'validRegex': "^\w+([\-\.]\w+)*@((([a-zA-Z0-9]+(\-[a-zA-Z0-9]+)*\.)+)[a-zA-Z]{2,4}|\[(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.(\d|\d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}\]) *$",
- 'description': "Email address"
- },
- "IPADDR": {
- 'mask': "###.###.###.###",
- 'formatcodes': 'F_Sr',
- 'validRegex': "( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}",
- 'description': "IP Address\n(see IpAddrCtrl)"
- }
- }
-
-# build demo-friendly dictionary of descriptions of autoformats
-autoformats = []
-for key, value in masktags.items():
- autoformats.append((key, value['description']))
-autoformats.sort()
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-class Field:
- valid_params = {
- 'index': None, ## which field of mask; set by parent control.
- 'mask': "", ## mask chars for this field
- 'extent': (), ## (edit start, edit_end) of field; set by parent control.
- 'formatcodes': "", ## codes indicating formatting options for the control
- 'fillChar': ' ', ## used as initial value for each mask position if initial value is not given
- 'groupChar': ',', ## used with numeric fields; indicates what char groups 3-tuple digits
- 'decimalChar': '.', ## used with numeric fields; indicates what char separates integer from fraction
- 'shiftDecimalChar': '>', ## used with numeric fields, indicates what is above the decimal point char on keyboard
- 'useParensForNegatives': False, ## used with numeric fields, indicates that () should be used vs. - to show negative numbers.
- 'defaultValue': "", ## use if you want different positional defaults vs. all the same fillChar
- 'excludeChars': "", ## optional string of chars to exclude even if main mask type does
- 'includeChars': "", ## optional string of chars to allow even if main mask type doesn't
- 'validRegex': "", ## optional regular expression to use to validate the control
- 'validRange': (), ## Optional hi-low range for numerics
- 'choices': [], ## Optional list for character expressions
- 'choiceRequired': False, ## If choices supplied this specifies if valid value must be in the list
- 'compareNoCase': False, ## Optional flag to indicate whether or not to use case-insensitive list search
- 'autoSelect': False, ## Set to True to try auto-completion on each keystroke:
- 'validFunc': None, ## Optional function for defining additional, possibly dynamic validation constraints on contrl
- 'validRequired': False, ## Set to True to disallow input that results in an invalid value
- 'emptyInvalid': False, ## Set to True to make EMPTY = INVALID
- 'description': "", ## primarily for autoformats, but could be useful elsewhere
- }
-
- # This list contains all parameters that when set at the control level should
- # propagate down to each field:
- propagating_params = ('fillChar', 'groupChar', 'decimalChar','useParensForNegatives',
- 'compareNoCase', 'emptyInvalid', 'validRequired')
-
- def __init__(self, **kwargs):
- """
- This is the "constructor" for setting up parameters for fields.
- a field_index of -1 is used to indicate "the entire control."
- """
-#### dbg('Field::Field', indent=1)
- # Validate legitimate set of parameters:
- for key in kwargs.keys():
- if key not in Field.valid_params.keys():
-#### dbg(indent=0)
- raise TypeError('invalid parameter "%s"' % (key))
-
- # Set defaults for each parameter for this instance, and fully
- # populate initial parameter list for configuration:
- for key, value in Field.valid_params.items():
- setattr(self, '_' + key, copy.copy(value))
- if not kwargs.has_key(key):
- kwargs[key] = copy.copy(value)
-
- self._autoCompleteIndex = -1
- self._SetParameters(**kwargs)
- self._ValidateParameters(**kwargs)
-
-#### dbg(indent=0)
-
-
- def _SetParameters(self, **kwargs):
- """
- This function can be used to set individual or multiple parameters for
- a masked edit field parameter after construction.
- """
-## dbg(suspend=1)
-## dbg('maskededit.Field::_SetParameters', indent=1)
- # Validate keyword arguments:
- for key in kwargs.keys():
- if key not in Field.valid_params.keys():
-## dbg(indent=0, suspend=0)
- raise AttributeError('invalid keyword argument "%s"' % key)
-
- if self._index is not None: dbg('field index:', self._index)
-## dbg('parameters:', indent=1)
- for key, value in kwargs.items():
-## dbg('%s:' % key, value)
- pass
-## dbg(indent=0)
-
-
- old_fillChar = self._fillChar # store so we can change choice lists accordingly if it changes
-
- # First, Assign all parameters specified:
- for key in Field.valid_params.keys():
- if kwargs.has_key(key):
- setattr(self, '_' + key, kwargs[key] )
-
- if kwargs.has_key('formatcodes'): # (set/changed)
- self._forceupper = '!' in self._formatcodes
- self._forcelower = '^' in self._formatcodes
- self._groupdigits = ',' in self._formatcodes
- self._okSpaces = '_' in self._formatcodes
- self._padZero = '0' in self._formatcodes
- self._autofit = 'F' in self._formatcodes
- self._insertRight = 'r' in self._formatcodes
- self._allowInsert = '>' in self._formatcodes
- self._alignRight = 'R' in self._formatcodes or 'r' in self._formatcodes
- self._moveOnFieldFull = not '<' in self._formatcodes
- self._selectOnFieldEntry = 'S' in self._formatcodes
-
- if kwargs.has_key('groupChar'):
- self._groupChar = kwargs['groupChar']
- if kwargs.has_key('decimalChar'):
- self._decimalChar = kwargs['decimalChar']
- if kwargs.has_key('shiftDecimalChar'):
- self._shiftDecimalChar = kwargs['shiftDecimalChar']
-
- if kwargs.has_key('formatcodes') or kwargs.has_key('validRegex'):
- self._regexMask = 'V' in self._formatcodes and self._validRegex
-
- if kwargs.has_key('fillChar'):
- self._old_fillChar = old_fillChar
-#### dbg("self._old_fillChar: '%s'" % self._old_fillChar)
-
- if kwargs.has_key('mask') or kwargs.has_key('validRegex'): # (set/changed)
- self._isInt = isInteger(self._mask)
-## dbg('isInt?', self._isInt, 'self._mask:"%s"' % self._mask)
-
-## dbg(indent=0, suspend=0)
-
-
- def _ValidateParameters(self, **kwargs):
- """
- This function can be used to validate individual or multiple parameters for
- a masked edit field parameter after construction.
- """
-## dbg(suspend=1)
-## dbg('maskededit.Field::_ValidateParameters', indent=1)
- if self._index is not None: dbg('field index:', self._index)
-#### dbg('parameters:', indent=1)
-## for key, value in kwargs.items():
-#### dbg('%s:' % key, value)
-#### dbg(indent=0)
-#### dbg("self._old_fillChar: '%s'" % self._old_fillChar)
-
- # Verify proper numeric format params:
- if self._groupdigits and self._groupChar == self._decimalChar:
-## dbg(indent=0, suspend=0)
- raise AttributeError("groupChar '%s' cannot be the same as decimalChar '%s'" % (self._groupChar, self._decimalChar))
-
-
- # Now go do validation, semantic and inter-dependency parameter processing:
- if kwargs.has_key('choices') or kwargs.has_key('compareNoCase') or kwargs.has_key('choiceRequired'): # (set/changed)
-
- self._compareChoices = [choice.strip() for choice in self._choices]
-
- if self._compareNoCase and self._choices:
- self._compareChoices = [item.lower() for item in self._compareChoices]
-
- if kwargs.has_key('choices'):
- self._autoCompleteIndex = -1
-
-
- if kwargs.has_key('validRegex'): # (set/changed)
- if self._validRegex:
- try:
- if self._compareNoCase:
- self._filter = re.compile(self._validRegex, re.IGNORECASE)
- else:
- self._filter = re.compile(self._validRegex)
- except:
-## dbg(indent=0, suspend=0)
- raise TypeError('%s: validRegex "%s" not a legal regular expression' % (str(self._index), self._validRegex))
- else:
- self._filter = None
-
- if kwargs.has_key('validRange'): # (set/changed)
- self._hasRange = False
- self._rangeHigh = 0
- self._rangeLow = 0
- if self._validRange:
- if type(self._validRange) != types.TupleType or len( self._validRange )!= 2 or self._validRange[0] > self._validRange[1]:
-## dbg(indent=0, suspend=0)
- raise TypeError('%s: validRange %s parameter must be tuple of form (a,b) where a <= b'
- % (str(self._index), repr(self._validRange)) )
-
- self._hasRange = True
- self._rangeLow = self._validRange[0]
- self._rangeHigh = self._validRange[1]
-
- if kwargs.has_key('choices') or (len(self._choices) and len(self._choices[0]) != len(self._mask)): # (set/changed)
- self._hasList = False
- if self._choices and type(self._choices) not in (types.TupleType, types.ListType):
-## dbg(indent=0, suspend=0)
- raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
- elif len( self._choices) > 0:
- for choice in self._choices:
- if type(choice) not in (types.StringType, types.UnicodeType):
-## dbg(indent=0, suspend=0)
- raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
-
- length = len(self._mask)
-## dbg('len(%s)' % self._mask, length, 'len(self._choices):', len(self._choices), 'length:', length, 'self._alignRight?', self._alignRight)
- if len(self._choices) and length:
- if len(self._choices[0]) > length:
- # changed mask without respecifying choices; readjust the width as appropriate:
- self._choices = [choice.strip() for choice in self._choices]
- if self._alignRight:
- self._choices = [choice.rjust( length ) for choice in self._choices]
- else:
- self._choices = [choice.ljust( length ) for choice in self._choices]
-## dbg('aligned choices:', self._choices)
-
- if hasattr(self, '_template'):
- # Verify each choice specified is valid:
- for choice in self._choices:
- if self.IsEmpty(choice) and not self._validRequired:
- # allow empty values even if invalid, (just colored differently)
- continue
- if not self.IsValid(choice):
-## dbg(indent=0, suspend=0)
- raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
- self._hasList = True
-
-#### dbg("kwargs.has_key('fillChar')?", kwargs.has_key('fillChar'), "len(self._choices) > 0?", len(self._choices) > 0)
-#### dbg("self._old_fillChar:'%s'" % self._old_fillChar, "self._fillChar: '%s'" % self._fillChar)
- if kwargs.has_key('fillChar') and len(self._choices) > 0:
- if kwargs['fillChar'] != ' ':
- self._choices = [choice.replace(' ', self._fillChar) for choice in self._choices]
- else:
- self._choices = [choice.replace(self._old_fillChar, self._fillChar) for choice in self._choices]
-## dbg('updated choices:', self._choices)
-
-
- if kwargs.has_key('autoSelect') and kwargs['autoSelect']:
- if not self._hasList:
-## dbg('no list to auto complete; ignoring "autoSelect=True"')
- self._autoSelect = False
-
- # reset field validity assumption:
- self._valid = True
-## dbg(indent=0, suspend=0)
-
-
- def _GetParameter(self, paramname):
- """
- Routine for retrieving the value of any given parameter
- """
- if Field.valid_params.has_key(paramname):
- return getattr(self, '_' + paramname)
- else:
- TypeError('Field._GetParameter: invalid parameter "%s"' % key)
-
-
- def IsEmpty(self, slice):
- """
- Indicates whether the specified slice is considered empty for the
- field.
- """
-## dbg('Field::IsEmpty("%s")' % slice, indent=1)
- if not hasattr(self, '_template'):
-## dbg(indent=0)
- raise AttributeError('_template')
-
-## dbg('self._template: "%s"' % self._template)
-## dbg('self._defaultValue: "%s"' % str(self._defaultValue))
- if slice == self._template and not self._defaultValue:
-## dbg(indent=0)
- return True
-
- elif slice == self._template:
- empty = True
- for pos in range(len(self._template)):
-#### dbg('slice[%(pos)d] != self._fillChar?' %locals(), slice[pos] != self._fillChar[pos])
- if slice[pos] not in (' ', self._fillChar):
- empty = False
- break
-## dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals(), indent=0)
- return empty
- else:
-## dbg("IsEmpty? 0 (slice doesn't match template)", indent=0)
- return False
-
-
- def IsValid(self, slice):
- """
- Indicates whether the specified slice is considered a valid value for the
- field.
- """
-## dbg(suspend=1)
-## dbg('Field[%s]::IsValid("%s")' % (str(self._index), slice), indent=1)
- valid = True # assume true to start
-
- if self.IsEmpty(slice):
-## dbg(indent=0, suspend=0)
- if self._emptyInvalid:
- return False
- else:
- return True
-
- elif self._hasList and self._choiceRequired:
-## dbg("(member of list required)")
- # do case-insensitive match on list; strip surrounding whitespace from slice (already done for choices):
- if self._fillChar != ' ':
- slice = slice.replace(self._fillChar, ' ')
-## dbg('updated slice:"%s"' % slice)
- compareStr = slice.strip()
-
- if self._compareNoCase:
- compareStr = compareStr.lower()
- valid = compareStr in self._compareChoices
-
- elif self._hasRange and not self.IsEmpty(slice):
-## dbg('validating against range')
- try:
- # allow float as well as int ranges (int comparisons for free.)
- valid = self._rangeLow <= float(slice) <= self._rangeHigh
- except:
- valid = False
-
- elif self._validRegex and self._filter:
-## dbg('validating against regex')
- valid = (re.match( self._filter, slice) is not None)
-
- if valid and self._validFunc:
-## dbg('validating against supplied function')
- valid = self._validFunc(slice)
-## dbg('valid?', valid, indent=0, suspend=0)
- return valid
-
-
- def _AdjustField(self, slice):
- """ 'Fixes' an integer field. Right or left-justifies, as required."""
-## dbg('Field::_AdjustField("%s")' % slice, indent=1)
- length = len(self._mask)
-#### dbg('length(self._mask):', length)
-#### dbg('self._useParensForNegatives?', self._useParensForNegatives)
- if self._isInt:
- if self._useParensForNegatives:
- signpos = slice.find('(')
- right_signpos = slice.find(')')
- intStr = slice.replace('(', '').replace(')', '') # drop sign, if any
- else:
- signpos = slice.find('-')
- intStr = slice.replace( '-', '' ) # drop sign, if any
- right_signpos = -1
-
- intStr = intStr.replace(' ', '') # drop extra spaces
- intStr = string.replace(intStr,self._fillChar,"") # drop extra fillchars
- intStr = string.replace(intStr,"-","") # drop sign, if any
- intStr = string.replace(intStr, self._groupChar, "") # lose commas/dots
-#### dbg('intStr:"%s"' % intStr)
- start, end = self._extent
- field_len = end - start
- if not self._padZero and len(intStr) != field_len and intStr.strip():
- intStr = str(long(intStr))
-#### dbg('raw int str: "%s"' % intStr)
-#### dbg('self._groupdigits:', self._groupdigits, 'self._formatcodes:', self._formatcodes)
- if self._groupdigits:
- new = ''
- cnt = 1
- for i in range(len(intStr)-1, -1, -1):
- new = intStr[i] + new
- if (cnt) % 3 == 0:
- new = self._groupChar + new
- cnt += 1
- if new and new[0] == self._groupChar:
- new = new[1:]
- if len(new) <= length:
- # expanded string will still fit and leave room for sign:
- intStr = new
- # else... leave it without the commas...
-
-## dbg('padzero?', self._padZero)
-## dbg('len(intStr):', len(intStr), 'field length:', length)
- if self._padZero and len(intStr) < length:
- intStr = '0' * (length - len(intStr)) + intStr
- if signpos != -1: # we had a sign before; restore it
- if self._useParensForNegatives:
- intStr = '(' + intStr[1:]
- if right_signpos != -1:
- intStr += ')'
- else:
- intStr = '-' + intStr[1:]
- elif signpos != -1 and slice[0:signpos].strip() == '': # - was before digits
- if self._useParensForNegatives:
- intStr = '(' + intStr
- if right_signpos != -1:
- intStr += ')'
- else:
- intStr = '-' + intStr
- elif right_signpos != -1:
- # must have had ')' but '(' was before field; re-add ')'
- intStr += ')'
- slice = intStr
-
- slice = slice.strip() # drop extra spaces
-
- if self._alignRight: ## Only if right-alignment is enabled
- slice = slice.rjust( length )
- else:
- slice = slice.ljust( length )
- if self._fillChar != ' ':
- slice = slice.replace(' ', self._fillChar)
-## dbg('adjusted slice: "%s"' % slice, indent=0)
- return slice
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-class MaskedEditMixin:
- """
- This class allows us to abstract the masked edit functionality that could
- be associated with any text entry control. (eg. wxTextCtrl, wxComboBox, etc.)
- """
- valid_ctrl_params = {
- 'mask': 'XXXXXXXXXXXXX', ## mask string for formatting this control
- 'autoformat': "", ## optional auto-format code to set format from masktags dictionary
- 'fields': {}, ## optional list/dictionary of maskededit.Field class instances, indexed by position in mask
- 'datestyle': 'MDY', ## optional date style for date-type values. Can trigger autocomplete year
- 'autoCompleteKeycodes': [], ## Optional list of additional keycodes which will invoke field-auto-complete
- 'useFixedWidthFont': True, ## Use fixed-width font instead of default for base control
- 'retainFieldValidation': False, ## Set this to true if setting control-level parameters independently,
- ## from field validation constraints
- 'emptyBackgroundColour': "White",
- 'validBackgroundColour': "White",
- 'invalidBackgroundColour': "Yellow",
- 'foregroundColour': "Black",
- 'signedForegroundColour': "Red",
- 'demo': False}
-
-
- def __init__(self, name = 'wxMaskedEdit', **kwargs):
- """
- This is the "constructor" for setting up the mixin variable parameters for the composite class.
- """
-
- self.name = name
-
- # set up flag for doing optional things to base control if possible
- if not hasattr(self, 'controlInitialized'):
- self.controlInitialized = False
-
- # Set internal state var for keeping track of whether or not a character
- # action results in a modification of the control, since .SetValue()
- # doesn't modify the base control's internal state:
- self.modified = False
- self._previous_mask = None
-
- # Validate legitimate set of parameters:
- for key in kwargs.keys():
- if key.replace('Color', 'Colour') not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys():
- raise TypeError('%s: invalid parameter "%s"' % (name, key))
-
- ## Set up dictionary that can be used by subclasses to override or add to default
- ## behavior for individual characters. Derived subclasses needing to change
- ## default behavior for keys can either redefine the default functions for the
- ## common keys or add functions for specific keys to this list. Each function
- ## added should take the key event as argument, and return False if the key
- ## requires no further processing.
- ##
- ## Initially populated with navigation and function control keys:
- self._keyhandlers = {
- # default navigation keys and handlers:
- wx.WXK_BACK: self._OnErase,
- wx.WXK_LEFT: self._OnArrow,
- wx.WXK_RIGHT: self._OnArrow,
- wx.WXK_UP: self._OnAutoCompleteField,
- wx.WXK_DOWN: self._OnAutoCompleteField,
- wx.WXK_TAB: self._OnChangeField,
- wx.WXK_HOME: self._OnHome,
- wx.WXK_END: self._OnEnd,
- wx.WXK_RETURN: self._OnReturn,
- wx.WXK_PRIOR: self._OnAutoCompleteField,
- wx.WXK_NEXT: self._OnAutoCompleteField,
-
- # default function control keys and handlers:
- wx.WXK_DELETE: self._OnErase,
- WXK_CTRL_A: self._OnCtrl_A,
- WXK_CTRL_C: self._OnCtrl_C,
- WXK_CTRL_S: self._OnCtrl_S,
- WXK_CTRL_V: self._OnCtrl_V,
- WXK_CTRL_X: self._OnCtrl_X,
- WXK_CTRL_Z: self._OnCtrl_Z,
- }
-
- ## bind standard navigational and control keycodes to this instance,
- ## so that they can be augmented and/or changed in derived classes:
- self._nav = list(nav)
- self._control = list(control)
-
- ## Dynamically evaluate and store string constants for mask chars
- ## so that locale settings can be made after this module is imported
- ## and the controls created after that is done can allow the
- ## appropriate characters:
- self.maskchardict = {
- '#': string.digits,
- 'A': string.uppercase,
- 'a': string.lowercase,
- 'X': string.letters + string.punctuation + string.digits,
- 'C': string.letters,
- 'N': string.letters + string.digits,
- '&': string.punctuation
- }
-
- ## self._ignoreChange is used by MaskedComboBox, because
- ## of the hack necessary to determine the selection; it causes
- ## EVT_TEXT messages from the combobox to be ignored if set.
- self._ignoreChange = False
-
- # These are used to keep track of previous value, for undo functionality:
- self._curValue = None
- self._prevValue = None
-
- self._valid = True
-
- # Set defaults for each parameter for this instance, and fully
- # populate initial parameter list for configuration:
- for key, value in MaskedEditMixin.valid_ctrl_params.items():
- setattr(self, '_' + key, copy.copy(value))
- if not kwargs.has_key(key):
-#### dbg('%s: "%s"' % (key, repr(value)))
- kwargs[key] = copy.copy(value)
-
- # Create a "field" that holds global parameters for control constraints
- self._ctrl_constraints = self._fields[-1] = Field(index=-1)
- self.SetCtrlParameters(**kwargs)
-
-
-
- def SetCtrlParameters(self, **kwargs):
- """
- This public function can be used to set individual or multiple masked edit
- parameters after construction.
- """
-## dbg(suspend=1)
-## dbg('MaskedEditMixin::SetCtrlParameters', indent=1)
-#### dbg('kwargs:', indent=1)
-## for key, value in kwargs.items():
-#### dbg(key, '=', value)
-#### dbg(indent=0)
-
- # Validate keyword arguments:
- constraint_kwargs = {}
- ctrl_kwargs = {}
- for key, value in kwargs.items():
- key = key.replace('Color', 'Colour') # for b-c, and standard wxPython spelling
- if key not in MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys():
-## dbg(indent=0, suspend=0)
- raise TypeError('Invalid keyword argument "%s" for control "%s"' % (key, self.name))
- elif key in Field.valid_params.keys():
- constraint_kwargs[key] = value
- else:
- ctrl_kwargs[key] = value
-
- mask = None
- reset_args = {}
-
- if ctrl_kwargs.has_key('autoformat'):
- autoformat = ctrl_kwargs['autoformat']
- else:
- autoformat = None
-
- # handle "parochial name" backward compatibility:
- if autoformat and autoformat.find('MILTIME') != -1 and autoformat not in masktags.keys():
- autoformat = autoformat.replace('MILTIME', '24HRTIME')
-
- if autoformat != self._autoformat and autoformat in masktags.keys():
-## dbg('autoformat:', autoformat)
- self._autoformat = autoformat
- mask = masktags[self._autoformat]['mask']
- # gather rest of any autoformat parameters:
- for param, value in masktags[self._autoformat].items():
- if param == 'mask': continue # (must be present; already accounted for)
- constraint_kwargs[param] = value
-
- elif autoformat and not autoformat in masktags.keys():
- raise AttributeError('invalid value for autoformat parameter: %s' % repr(autoformat))
- else:
-## dbg('autoformat not selected')
- if kwargs.has_key('mask'):
- mask = kwargs['mask']
-## dbg('mask:', mask)
-
- ## Assign style flags
- if mask is None:
-## dbg('preserving previous mask')
- mask = self._previous_mask # preserve previous mask
- else:
-## dbg('mask (re)set')
- reset_args['reset_mask'] = mask
- constraint_kwargs['mask'] = mask
-
- # wipe out previous fields; preserve new control-level constraints
- self._fields = {-1: self._ctrl_constraints}
-
-
- if ctrl_kwargs.has_key('fields'):
- # do field parameter type validation, and conversion to internal dictionary
- # as appropriate:
- fields = ctrl_kwargs['fields']
- if type(fields) in (types.ListType, types.TupleType):
- for i in range(len(fields)):
- field = fields[i]
- if not isinstance(field, Field):
-## dbg(indent=0, suspend=0)
- raise AttributeError('invalid type for field parameter: %s' % repr(field))
- self._fields[i] = field
-
- elif type(fields) == types.DictionaryType:
- for index, field in fields.items():
- if not isinstance(field, Field):
-## dbg(indent=0, suspend=0)
- raise AttributeError('invalid type for field parameter: %s' % repr(field))
- self._fields[index] = field
- else:
-## dbg(indent=0, suspend=0)
- raise AttributeError('fields parameter must be a list or dictionary; not %s' % repr(fields))
-
- # Assign constraint parameters for entire control:
-#### dbg('control constraints:', indent=1)
-## for key, value in constraint_kwargs.items():
-#### dbg('%s:' % key, value)
-#### dbg(indent=0)
-
- # determine if changing parameters that should affect the entire control:
- for key in MaskedEditMixin.valid_ctrl_params.keys():
- if key in ( 'mask', 'fields' ): continue # (processed separately)
- if ctrl_kwargs.has_key(key):
- setattr(self, '_' + key, ctrl_kwargs[key])
-
- # Validate color parameters, converting strings to named colors and validating
- # result if appropriate:
- for key in ('emptyBackgroundColour', 'invalidBackgroundColour', 'validBackgroundColour',
- 'foregroundColour', 'signedForegroundColour'):
- if ctrl_kwargs.has_key(key):
- if type(ctrl_kwargs[key]) in (types.StringType, types.UnicodeType):
- c = wx.NamedColour(ctrl_kwargs[key])
- if c.Get() == (-1, -1, -1):
- raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key))
- else:
- # replace attribute with wxColour object:
- setattr(self, '_' + key, c)
- # attach a python dynamic attribute to wxColour for debug printouts
- c._name = ctrl_kwargs[key]
-
- elif type(ctrl_kwargs[key]) != type(wx.BLACK):
- raise TypeError('%s not a legal color specification for %s' % (repr(ctrl_kwargs[key]), key))
-
-
-## dbg('self._retainFieldValidation:', self._retainFieldValidation)
- if not self._retainFieldValidation:
- # Build dictionary of any changing parameters which should be propagated to the
- # component fields:
- for arg in Field.propagating_params:
-#### dbg('kwargs.has_key(%s)?' % arg, kwargs.has_key(arg))
-#### dbg('getattr(self._ctrl_constraints, _%s)?' % arg, getattr(self._ctrl_constraints, '_'+arg))
- reset_args[arg] = kwargs.has_key(arg) and kwargs[arg] != getattr(self._ctrl_constraints, '_'+arg)
-#### dbg('reset_args[%s]?' % arg, reset_args[arg])
-
- # Set the control-level constraints:
- self._ctrl_constraints._SetParameters(**constraint_kwargs)
-
- # This routine does the bulk of the interdependent parameter processing, determining
- # the field extents of the mask if changed, resetting parameters as appropriate,
- # determining the overall template value for the control, etc.
- self._configure(mask, **reset_args)
-
- # now that we've propagated the field constraints and mask portions to the
- # various fields, validate the constraints
- self._ctrl_constraints._ValidateParameters(**constraint_kwargs)
-
- # Validate that all choices for given fields are at least of the
- # necessary length, and that they all would be valid pastes if pasted
- # into their respective fields:
-#### dbg('validating choices')
- self._validateChoices()
-
-
- self._autofit = self._ctrl_constraints._autofit
- self._isNeg = False
-
- self._isDate = 'D' in self._ctrl_constraints._formatcodes and isDateType(mask)
- self._isTime = 'T' in self._ctrl_constraints._formatcodes and isTimeType(mask)
- if self._isDate:
- # Set _dateExtent, used in date validation to locate date in string;
- # always set as though year will be 4 digits, even if mask only has
- # 2 digits, so we can always properly process the intended year for
- # date validation (leap years, etc.)
- if self._mask.find('CCC') != -1: self._dateExtent = 11
- else: self._dateExtent = 10
-
- self._4digityear = len(self._mask) > 8 and self._mask[9] == '#'
-
- if self._isDate and self._autoformat:
- # Auto-decide datestyle:
- if self._autoformat.find('MDDY') != -1: self._datestyle = 'MDY'
- elif self._autoformat.find('YMMD') != -1: self._datestyle = 'YMD'
- elif self._autoformat.find('YMMMD') != -1: self._datestyle = 'YMD'
- elif self._autoformat.find('DMMY') != -1: self._datestyle = 'DMY'
- elif self._autoformat.find('DMMMY') != -1: self._datestyle = 'DMY'
-
- # Give derived controls a chance to react to parameter changes before
- # potentially changing current value of the control.
- self._OnCtrlParametersChanged()
-
- if self.controlInitialized:
- # Then the base control is available for configuration;
- # take action on base control based on new settings, as appropriate.
- if kwargs.has_key('useFixedWidthFont'):
- # Set control font - fixed width by default
- self._setFont()
-
- if reset_args.has_key('reset_mask'):
-## dbg('reset mask')
- curvalue = self._GetValue()
- if curvalue.strip():
- try:
-## dbg('attempting to _SetInitialValue(%s)' % self._GetValue())
- self._SetInitialValue(self._GetValue())
- except Exception, e:
-## dbg('exception caught:', e)
-## dbg("current value doesn't work; attempting to reset to template")
- self._SetInitialValue()
- else:
-## dbg('attempting to _SetInitialValue() with template')
- self._SetInitialValue()
-
- elif kwargs.has_key('useParensForNegatives'):
- newvalue = self._getSignedValue()[0]
-
- if newvalue is not None:
- # Adjust for new mask:
- if len(newvalue) < len(self._mask):
- newvalue += ' '
- elif len(newvalue) > len(self._mask):
- if newvalue[-1] in (' ', ')'):
- newvalue = newvalue[:-1]
-
-## dbg('reconfiguring value for parens:"%s"' % newvalue)
- self._SetValue(newvalue)
-
- if self._prevValue != newvalue:
- self._prevValue = newvalue # disallow undo of sign type
-
- if self._autofit:
-## dbg('setting client size to:', self._CalcSize())
- self.SetClientSize(self._CalcSize())
-
- # Set value/type-specific formatting
- self._applyFormatting()
-## dbg(indent=0, suspend=0)
-
- def SetMaskParameters(self, **kwargs):
- """ old name for this function """
- return self.SetCtrlParameters(**kwargs)
-
-
- def GetCtrlParameter(self, paramname):
- """
- Routine for retrieving the value of any given parameter
- """
- if MaskedEditMixin.valid_ctrl_params.has_key(paramname.replace('Color','Colour')):
- return getattr(self, '_' + paramname.replace('Color', 'Colour'))
- elif Field.valid_params.has_key(paramname):
- return self._ctrl_constraints._GetParameter(paramname)
- else:
- TypeError('"%s".GetCtrlParameter: invalid parameter "%s"' % (self.name, paramname))
-
- def GetMaskParameter(self, paramname):
- """ old name for this function """
- return self.GetCtrlParameter(paramname)
-
-
-## This idea worked, but Boa was unable to use this solution...
-## def _attachMethod(self, func):
-## import new
-## setattr(self, func.__name__, new.instancemethod(func, self, self.__class__))
-##
-##
-## def _DefinePropertyFunctions(exposed_params):
-## for param in exposed_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))
-## self._attachMethod(locals()['Set%s' % propname])
-## self._attachMethod(locals()['Get%s' % propname])
-##
-## 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))
-## self._attachMethod(locals()['Set%s' % propname])
-## self._attachMethod(locals()['Get%s' % propname])
-##
-
-
- def SetFieldParameters(self, field_index, **kwargs):
- """
- Routine provided to modify the parameters of a given field.
- Because changes to fields can affect the overall control,
- direct access to the fields is prevented, and the control
- is always "reconfigured" after setting a field parameter.
- """
- if field_index not in self._field_indices:
- raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
- # set parameters as requested:
- self._fields[field_index]._SetParameters(**kwargs)
-
- # Possibly reprogram control template due to resulting changes, and ensure
- # control-level params are still propagated to fields:
- self._configure(self._previous_mask)
- self._fields[field_index]._ValidateParameters(**kwargs)
-
- if self.controlInitialized:
- if kwargs.has_key('fillChar') or kwargs.has_key('defaultValue'):
- self._SetInitialValue()
-
- if self._autofit:
- self.SetClientSize(self._CalcSize())
-
- # Set value/type-specific formatting
- self._applyFormatting()
-
-
- def GetFieldParameter(self, field_index, paramname):
- """
- Routine provided for getting a parameter of an individual field.
- """
- if field_index not in self._field_indices:
- raise IndexError('%s is not a valid field for control "%s".' % (str(field_index), self.name))
- elif Field.valid_params.has_key(paramname):
- return self._fields[field_index]._GetParameter(paramname)
- else:
- TypeError('"%s".GetFieldParameter: invalid parameter "%s"' % (self.name, paramname))
-
-
- def _SetKeycodeHandler(self, keycode, func):
- """
- This function adds and/or replaces key event handling functions
- used by the control. <func> should take the event as argument
- and return False if no further action on the key is necessary.
- """
- self._keyhandlers[keycode] = func
-
-
- def _SetKeyHandler(self, char, func):
- """
- This function adds and/or replaces key event handling functions
- for ascii characters. <func> should take the event as argument
- and return False if no further action on the key is necessary.
- """
- self._SetKeycodeHandler(ord(char), func)
-
-
- def _AddNavKeycode(self, keycode, handler=None):
- """
- This function allows a derived subclass to augment the list of
- keycodes that are considered "navigational" keys.
- """
- self._nav.append(keycode)
- if handler:
- self._keyhandlers[keycode] = handler
-
-
- def _AddNavKey(self, char, handler=None):
- """
- This function is a convenience function so you don't have to
- remember to call ord() for ascii chars to be used for navigation.
- """
- self._AddNavKeycode(ord(char), handler)
-
-
- def _GetNavKeycodes(self):
- """
- This function retrieves the current list of navigational keycodes for
- the control.
- """
- return self._nav
-
-
- def _SetNavKeycodes(self, keycode_func_tuples):
- """
- This function allows you to replace the current list of keycode processed
- as navigation keys, and bind associated optional keyhandlers.
- """
- self._nav = []
- for keycode, func in keycode_func_tuples:
- self._nav.append(keycode)
- if func:
- self._keyhandlers[keycode] = func
-
-
- def _processMask(self, mask):
- """
- This subroutine expands {n} syntax in mask strings, and looks for escaped
- special characters and returns the expanded mask, and an dictionary
- of booleans indicating whether or not a given position in the mask is
- a mask character or not.
- """
-## dbg('_processMask: mask', mask, indent=1)
- # regular expression for parsing c{n} syntax:
- rex = re.compile('([' +string.join(maskchars,"") + '])\{(\d+)\}')
- s = mask
- match = rex.search(s)
- while match: # found an(other) occurrence
- maskchr = s[match.start(1):match.end(1)] # char to be repeated
- repcount = int(s[match.start(2):match.end(2)]) # the number of times
- replacement = string.join( maskchr * repcount, "") # the resulting substr
- s = s[:match.start(1)] + replacement + s[match.end(2)+1:] #account for trailing '}'
- match = rex.search(s) # look for another such entry in mask
-
- self._decimalChar = self._ctrl_constraints._decimalChar
- self._shiftDecimalChar = self._ctrl_constraints._shiftDecimalChar
-
- self._isFloat = isFloatingPoint(s) and not self._ctrl_constraints._validRegex
- self._isInt = isInteger(s) and not self._ctrl_constraints._validRegex
- self._signOk = '-' in self._ctrl_constraints._formatcodes and (self._isFloat or self._isInt)
- self._useParens = self._ctrl_constraints._useParensForNegatives
- self._isNeg = False
-#### dbg('self._signOk?', self._signOk, 'self._useParens?', self._useParens)
-#### dbg('isFloatingPoint(%s)?' % (s), isFloatingPoint(s),
-## 'ctrl regex:', self._ctrl_constraints._validRegex)
-
- if self._signOk and s[0] != ' ':
- s = ' ' + s
- if self._ctrl_constraints._defaultValue and self._ctrl_constraints._defaultValue[0] != ' ':
- self._ctrl_constraints._defaultValue = ' ' + self._ctrl_constraints._defaultValue
- self._signpos = 0
-
- if self._useParens:
- s += ' '
- self._ctrl_constraints._defaultValue += ' '
-
- # Now, go build up a dictionary of booleans, indexed by position,
- # indicating whether or not a given position is masked or not
- ismasked = {}
- i = 0
- while i < len(s):
- if s[i] == '\\': # if escaped character:
- ismasked[i] = False # mark position as not a mask char
- if i+1 < len(s): # if another char follows...
- s = s[:i] + s[i+1:] # elide the '\'
- if i+2 < len(s) and s[i+1] == '\\':
- # if next char also a '\', char is a literal '\'
- s = s[:i] + s[i+1:] # elide the 2nd '\' as well
- else: # else if special char, mark position accordingly
- ismasked[i] = s[i] in maskchars
-#### dbg('ismasked[%d]:' % i, ismasked[i], s)
- i += 1 # increment to next char
-#### dbg('ismasked:', ismasked)
-## dbg('new mask: "%s"' % s, indent=0)
-
- return s, ismasked
-
-
- def _calcFieldExtents(self):
- """
- Subroutine responsible for establishing/configuring field instances with
- indices and editable extents appropriate to the specified mask, and building
- the lookup table mapping each position to the corresponding field.
- """
- self._lookupField = {}
- if self._mask:
-
- ## Create dictionary of positions,characters in mask
- self.maskdict = {}
- for charnum in range( len( self._mask)):
- self.maskdict[charnum] = self._mask[charnum:charnum+1]
-
- # For the current mask, create an ordered list of field extents
- # and a dictionary of positions that map to field indices:
-
- if self._signOk: start = 1
- else: start = 0
-
- if self._isFloat:
- # Skip field "discovery", and just construct a 2-field control with appropriate
- # constraints for a floating-point entry.
-
- # .setdefault always constructs 2nd argument even if not needed, so we do this
- # the old-fashioned way...
- if not self._fields.has_key(0):
- self._fields[0] = Field()
- if not self._fields.has_key(1):
- self._fields[1] = Field()
-
- self._decimalpos = string.find( self._mask, '.')
-## dbg('decimal pos =', self._decimalpos)
-
- formatcodes = self._fields[0]._GetParameter('formatcodes')
- if 'R' not in formatcodes: formatcodes += 'R'
- self._fields[0]._SetParameters(index=0, extent=(start, self._decimalpos),
- mask=self._mask[start:self._decimalpos], formatcodes=formatcodes)
- end = len(self._mask)
- if self._signOk and self._useParens:
- end -= 1
- self._fields[1]._SetParameters(index=1, extent=(self._decimalpos+1, end),
- mask=self._mask[self._decimalpos+1:end])
-
- for i in range(self._decimalpos+1):
- self._lookupField[i] = 0
-
- for i in range(self._decimalpos+1, len(self._mask)+1):
- self._lookupField[i] = 1
-
- elif self._isInt:
- # Skip field "discovery", and just construct a 1-field control with appropriate
- # constraints for a integer entry.
- if not self._fields.has_key(0):
- self._fields[0] = Field(index=0)
- end = len(self._mask)
- if self._signOk and self._useParens:
- end -= 1
- self._fields[0]._SetParameters(index=0, extent=(start, end),
- mask=self._mask[start:end])
- for i in range(len(self._mask)+1):
- self._lookupField[i] = 0
- else:
- # generic control; parse mask to figure out where the fields are:
- field_index = 0
- pos = 0
- i = self._findNextEntry(pos,adjustInsert=False) # go to 1st entry point:
- if i < len(self._mask): # no editable chars!
- for j in range(pos, i+1):
- self._lookupField[j] = field_index
- pos = i # figure out field for 1st editable space:
-
- while i <= len(self._mask):
-#### dbg('searching: outer field loop: i = ', i)
- if self._isMaskChar(i):
-#### dbg('1st char is mask char; recording edit_start=', i)
- edit_start = i
- # Skip to end of editable part of current field:
- while i < len(self._mask) and self._isMaskChar(i):
- self._lookupField[i] = field_index
- i += 1
-#### dbg('edit_end =', i)
- edit_end = i
- self._lookupField[i] = field_index
-#### dbg('self._fields.has_key(%d)?' % field_index, self._fields.has_key(field_index))
- if not self._fields.has_key(field_index):
- kwargs = Field.valid_params.copy()
- kwargs['index'] = field_index
- kwargs['extent'] = (edit_start, edit_end)
- kwargs['mask'] = self._mask[edit_start:edit_end]
- self._fields[field_index] = Field(**kwargs)
- else:
- self._fields[field_index]._SetParameters(
- index=field_index,
- extent=(edit_start, edit_end),
- mask=self._mask[edit_start:edit_end])
- pos = i
- i = self._findNextEntry(pos, adjustInsert=False) # go to next field:
- if i > pos:
- for j in range(pos, i+1):
- self._lookupField[j] = field_index
- if i >= len(self._mask):
- break # if past end, we're done
- else:
- field_index += 1
-#### dbg('next field:', field_index)
-
- indices = self._fields.keys()
- indices.sort()
- self._field_indices = indices[1:]
-#### dbg('lookupField map:', indent=1)
-## for i in range(len(self._mask)):
-#### dbg('pos %d:' % i, self._lookupField[i])
-#### dbg(indent=0)
-
- # Verify that all field indices specified are valid for mask:
- for index in self._fields.keys():
- if index not in [-1] + self._lookupField.values():
- raise IndexError('field %d is not a valid field for mask "%s"' % (index, self._mask))
-
-
- def _calcTemplate(self, reset_fillchar, reset_default):
- """
- Subroutine for processing current fillchars and default values for
- whole control and individual fields, constructing the resulting
- overall template, and adjusting the current value as necessary.
- """
- default_set = False
- if self._ctrl_constraints._defaultValue:
- default_set = True
- else:
- for field in self._fields.values():
- if field._defaultValue and not reset_default:
- default_set = True
-## dbg('default set?', default_set)
-
- # Determine overall new template for control, and keep track of previous
- # values, so that current control value can be modified as appropriate:
- if self.controlInitialized: curvalue = list(self._GetValue())
- else: curvalue = None
-
- if hasattr(self, '_fillChar'): old_fillchars = self._fillChar
- else: old_fillchars = None
-
- if hasattr(self, '_template'): old_template = self._template
- else: old_template = None
-
- self._template = ""
-
- self._fillChar = {}
- reset_value = False
-
- for field in self._fields.values():
- field._template = ""
-
- for pos in range(len(self._mask)):
-#### dbg('pos:', pos)
- field = self._FindField(pos)
-#### dbg('field:', field._index)
- start, end = field._extent
-
- if pos == 0 and self._signOk:
- self._template = ' ' # always make 1st 1st position blank, regardless of fillchar
- elif self._isFloat and pos == self._decimalpos:
- self._template += self._decimalChar
- elif self._isMaskChar(pos):
- if field._fillChar != self._ctrl_constraints._fillChar and not reset_fillchar:
- fillChar = field._fillChar
- else:
- fillChar = self._ctrl_constraints._fillChar
- self._fillChar[pos] = fillChar
-
- # Replace any current old fillchar with new one in current value;
- # if action required, set reset_value flag so we can take that action
- # after we're all done
- if self.controlInitialized and old_fillchars and old_fillchars.has_key(pos) and curvalue:
- if curvalue[pos] == old_fillchars[pos] and old_fillchars[pos] != fillChar:
- reset_value = True
- curvalue[pos] = fillChar
-
- if not field._defaultValue and not self._ctrl_constraints._defaultValue:
-#### dbg('no default value')
- self._template += fillChar
- field._template += fillChar
-
- elif field._defaultValue and not reset_default:
-#### dbg('len(field._defaultValue):', len(field._defaultValue))
-#### dbg('pos-start:', pos-start)
- if len(field._defaultValue) > pos-start:
-#### dbg('field._defaultValue[pos-start]: "%s"' % field._defaultValue[pos-start])
- self._template += field._defaultValue[pos-start]
- field._template += field._defaultValue[pos-start]
- else:
-#### dbg('field default not long enough; using fillChar')
- self._template += fillChar
- field._template += fillChar
- else:
- if len(self._ctrl_constraints._defaultValue) > pos:
-#### dbg('using control default')
- self._template += self._ctrl_constraints._defaultValue[pos]
- field._template += self._ctrl_constraints._defaultValue[pos]
- else:
-#### dbg('ctrl default not long enough; using fillChar')
- self._template += fillChar
- field._template += fillChar
-#### dbg('field[%d]._template now "%s"' % (field._index, field._template))
-#### dbg('self._template now "%s"' % self._template)
- else:
- self._template += self._mask[pos]
-
- self._fields[-1]._template = self._template # (for consistency)
-
- if curvalue: # had an old value, put new one back together
- newvalue = string.join(curvalue, "")
- else:
- newvalue = None
-
- if default_set:
- self._defaultValue = self._template
-## dbg('self._defaultValue:', self._defaultValue)
- if not self.IsEmpty(self._defaultValue) and not self.IsValid(self._defaultValue):
-#### dbg(indent=0)
- raise ValueError('Default value of "%s" is not a valid value for control "%s"' % (self._defaultValue, self.name))
-
- # if no fillchar change, but old value == old template, replace it:
- if newvalue == old_template:
- newvalue = self._template
- reset_value = True
- else:
- self._defaultValue = None
-
- if reset_value:
-## dbg('resetting value to: "%s"' % newvalue)
- pos = self._GetInsertionPoint()
- sel_start, sel_to = self._GetSelection()
- self._SetValue(newvalue)
- self._SetInsertionPoint(pos)
- self._SetSelection(sel_start, sel_to)
-
-
- def _propagateConstraints(self, **reset_args):
- """
- Subroutine for propagating changes to control-level constraints and
- formatting to the individual fields as appropriate.
- """
- parent_codes = self._ctrl_constraints._formatcodes
- parent_includes = self._ctrl_constraints._includeChars
- parent_excludes = self._ctrl_constraints._excludeChars
- for i in self._field_indices:
- field = self._fields[i]
- inherit_args = {}
- if len(self._field_indices) == 1:
- inherit_args['formatcodes'] = parent_codes
- inherit_args['includeChars'] = parent_includes
- inherit_args['excludeChars'] = parent_excludes
- else:
- field_codes = current_codes = field._GetParameter('formatcodes')
- for c in parent_codes:
- if c not in field_codes: field_codes += c
- if field_codes != current_codes:
- inherit_args['formatcodes'] = field_codes
-
- include_chars = current_includes = field._GetParameter('includeChars')
- for c in parent_includes:
- if not c in include_chars: include_chars += c
- if include_chars != current_includes:
- inherit_args['includeChars'] = include_chars
-
- exclude_chars = current_excludes = field._GetParameter('excludeChars')
- for c in parent_excludes:
- if not c in exclude_chars: exclude_chars += c
- if exclude_chars != current_excludes:
- inherit_args['excludeChars'] = exclude_chars
-
- if reset_args.has_key('defaultValue') and reset_args['defaultValue']:
- inherit_args['defaultValue'] = "" # (reset for field)
-
- for param in Field.propagating_params:
-#### dbg('reset_args.has_key(%s)?' % param, reset_args.has_key(param))
-#### dbg('reset_args.has_key(%(param)s) and reset_args[%(param)s]?' % locals(), reset_args.has_key(param) and reset_args[param])
- if reset_args.has_key(param):
- inherit_args[param] = self.GetCtrlParameter(param)
-#### dbg('inherit_args[%s]' % param, inherit_args[param])
-
- if inherit_args:
- field._SetParameters(**inherit_args)
- field._ValidateParameters(**inherit_args)
-
-
- def _validateChoices(self):
- """
- Subroutine that validates that all choices for given fields are at
- least of the necessary length, and that they all would be valid pastes
- if pasted into their respective fields.
- """
- for field in self._fields.values():
- if field._choices:
- index = field._index
- if len(self._field_indices) == 1 and index == 0 and field._choices == self._ctrl_constraints._choices:
-## dbg('skipping (duplicate) choice validation of field 0')
- continue
-#### dbg('checking for choices for field', field._index)
- start, end = field._extent
- field_length = end - start
-#### dbg('start, end, length:', start, end, field_length)
- for choice in field._choices:
-#### dbg('testing "%s"' % choice)
- valid_paste, ignore, replace_to = self._validatePaste(choice, start, end)
- if not valid_paste:
-#### dbg(indent=0)
- raise ValueError('"%s" could not be entered into field %d of control "%s"' % (choice, index, self.name))
- elif replace_to > end:
-#### dbg(indent=0)
- raise ValueError('"%s" will not fit into field %d of control "%s"' (choice, index, self.name))
-#### dbg(choice, 'valid in field', index)
-
-
- def _configure(self, mask, **reset_args):
- """
- This function sets flags for automatic styling options. It is
- called whenever a control or field-level parameter is set/changed.
-
- This routine does the bulk of the interdependent parameter processing, determining
- the field extents of the mask if changed, resetting parameters as appropriate,
- determining the overall template value for the control, etc.
-
- reset_args is supplied if called from control's .SetCtrlParameters()
- routine, and indicates which if any parameters which can be
- overridden by individual fields have been reset by request for the
- whole control.
-
- """
-## dbg(suspend=1)
-## dbg('MaskedEditMixin::_configure("%s")' % mask, indent=1)
-
- # Preprocess specified mask to expand {n} syntax, handle escaped
- # mask characters, etc and build the resulting positionally keyed
- # dictionary for which positions are mask vs. template characters:
- self._mask, self.ismasked = self._processMask(mask)
- self._masklength = len(self._mask)
-#### dbg('processed mask:', self._mask)
-
- # Preserve original mask specified, for subsequent reprocessing
- # if parameters change.
-## dbg('mask: "%s"' % self._mask, 'previous mask: "%s"' % self._previous_mask)
- self._previous_mask = mask # save unexpanded mask for next time
- # Set expanded mask and extent of field -1 to width of entire control:
- self._ctrl_constraints._SetParameters(mask = self._mask, extent=(0,self._masklength))
-
- # Go parse mask to determine where each field is, construct field
- # instances as necessary, configure them with those extents, and
- # build lookup table mapping each position for control to its corresponding
- # field.
-#### dbg('calculating field extents')
-
- self._calcFieldExtents()
-
-
- # Go process defaultValues and fillchars to construct the overall
- # template, and adjust the current value as necessary:
- reset_fillchar = reset_args.has_key('fillChar') and reset_args['fillChar']
- reset_default = reset_args.has_key('defaultValue') and reset_args['defaultValue']
-
-#### dbg('calculating template')
- self._calcTemplate(reset_fillchar, reset_default)
-
- # Propagate control-level formatting and character constraints to each
- # field if they don't already have them; if only one field, propagate
- # control-level validation constraints to field as well:
-#### dbg('propagating constraints')
- self._propagateConstraints(**reset_args)
-
-
- if self._isFloat and self._fields[0]._groupChar == self._decimalChar:
- raise AttributeError('groupChar (%s) and decimalChar (%s) must be distinct.' %
- (self._fields[0]._groupChar, self._decimalChar) )
-
-#### dbg('fields:', indent=1)
-## for i in [-1] + self._field_indices:
-#### dbg('field %d:' % i, self._fields[i].__dict__)
-#### dbg(indent=0)
-
- # Set up special parameters for numeric control, if appropriate:
- if self._signOk:
- self._signpos = 0 # assume it starts here, but it will move around on floats
- signkeys = ['-', '+', ' ']
- if self._useParens:
- signkeys += ['(', ')']
- for key in signkeys:
- keycode = ord(key)
- if not self._keyhandlers.has_key(keycode):
- self._SetKeyHandler(key, self._OnChangeSign)
-
-
-
- if self._isFloat or self._isInt:
- if self.controlInitialized:
- value = self._GetValue()
-#### dbg('value: "%s"' % value, 'len(value):', len(value),
-## 'len(self._ctrl_constraints._mask):',len(self._ctrl_constraints._mask))
- if len(value) < len(self._ctrl_constraints._mask):
- newvalue = value
- if self._useParens and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find('(') == -1:
- newvalue += ' '
- if self._signOk and len(newvalue) < len(self._ctrl_constraints._mask) and newvalue.find(')') == -1:
- newvalue = ' ' + newvalue
- if len(newvalue) < len(self._ctrl_constraints._mask):
- if self._ctrl_constraints._alignRight:
- newvalue = newvalue.rjust(len(self._ctrl_constraints._mask))
- else:
- newvalue = newvalue.ljust(len(self._ctrl_constraints._mask))
-## dbg('old value: "%s"' % value)
-## dbg('new value: "%s"' % newvalue)
- try:
- self._SetValue(newvalue)
- except Exception, e:
-## dbg('exception raised:', e, 'resetting to initial value')
- self._SetInitialValue()
-
- elif len(value) > len(self._ctrl_constraints._mask):
- newvalue = value
- if not self._useParens and newvalue[-1] == ' ':
- newvalue = newvalue[:-1]
- if not self._signOk and len(newvalue) > len(self._ctrl_constraints._mask):
- newvalue = newvalue[1:]
- if not self._signOk:
- newvalue, signpos, right_signpos = self._getSignedValue(newvalue)
-
-## dbg('old value: "%s"' % value)
-## dbg('new value: "%s"' % newvalue)
- try:
- self._SetValue(newvalue)
- except Exception, e:
-## dbg('exception raised:', e, 'resetting to initial value')
- self._SetInitialValue()
- elif not self._signOk and ('(' in value or '-' in value):
- newvalue, signpos, right_signpos = self._getSignedValue(value)
-## dbg('old value: "%s"' % value)
-## dbg('new value: "%s"' % newvalue)
- try:
- self._SetValue(newvalue)
- except e:
-## dbg('exception raised:', e, 'resetting to initial value')
- self._SetInitialValue()
-
- # Replace up/down arrow default handling:
- # make down act like tab, up act like shift-tab:
-
-#### dbg('Registering numeric navigation and control handlers (if not already set)')
- if not self._keyhandlers.has_key(wx.WXK_DOWN):
- self._SetKeycodeHandler(wx.WXK_DOWN, self._OnChangeField)
- if not self._keyhandlers.has_key(wx.WXK_UP):
- self._SetKeycodeHandler(wx.WXK_UP, self._OnUpNumeric) # (adds "shift" to up arrow, and calls _OnChangeField)
-
- # On ., truncate contents right of cursor to decimal point (if any)
- # leaves cusor after decimal point if floating point, otherwise at 0.
- if not self._keyhandlers.has_key(ord(self._decimalChar)):
- self._SetKeyHandler(self._decimalChar, self._OnDecimalPoint)
- if not self._keyhandlers.has_key(ord(self._shiftDecimalChar)):
- self._SetKeyHandler(self._shiftDecimalChar, self._OnChangeField) # (Shift-'.' == '>' on US keyboards)
-
- # Allow selective insert of groupchar in numbers:
- if not self._keyhandlers.has_key(ord(self._fields[0]._groupChar)):
- self._SetKeyHandler(self._fields[0]._groupChar, self._OnGroupChar)
-
-## dbg(indent=0, suspend=0)
-
-
- def _SetInitialValue(self, value=""):
- """
- fills the control with the generated or supplied default value.
- It will also set/reset the font if necessary and apply
- formatting to the control at this time.
- """
-## dbg('MaskedEditMixin::_SetInitialValue("%s")' % value, indent=1)
- if not value:
- self._prevValue = self._curValue = self._template
- # don't apply external validation rules in this case, as template may
- # not coincide with "legal" value...
- try:
- self._SetValue(self._curValue) # note the use of "raw" ._SetValue()...
- except Exception, e:
-## dbg('exception thrown:', e, indent=0)
- raise
- else:
- # Otherwise apply validation as appropriate to passed value:
-#### dbg('value = "%s", length:' % value, len(value))
- self._prevValue = self._curValue = value
- try:
- self.SetValue(value) # use public (validating) .SetValue()
- except Exception, e:
-## dbg('exception thrown:', e, indent=0)
- raise
-
-
- # Set value/type-specific formatting
- self._applyFormatting()
-## dbg(indent=0)
-
-
- def _calcSize(self, size=None):
- """ Calculate automatic size if allowed; must be called after the base control is instantiated"""
-#### dbg('MaskedEditMixin::_calcSize', indent=1)
- cont = (size is None or size == wx.DefaultSize)
-
- if cont and self._autofit:
- sizing_text = 'M' * self._masklength
- if wx.Platform != "__WXMSW__": # give it a little extra space
- sizing_text += 'M'
- if wx.Platform == "__WXMAC__": # give it even a little more...
- sizing_text += 'M'
-#### dbg('len(sizing_text):', len(sizing_text), 'sizing_text: "%s"' % sizing_text)
- w, h = self.GetTextExtent(sizing_text)
- size = (w+4, self.GetClientSize().height)
-#### dbg('size:', size, indent=0)
- return size
-
-
- def _setFont(self):
- """ Set the control's font typeface -- pass the font name as str."""
-#### dbg('MaskedEditMixin::_setFont', indent=1)
- if not self._useFixedWidthFont:
- self._font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT)
- else:
- font = self.GetFont() # get size, weight, etc from current font
-
- # Set to teletype font (guaranteed to be mappable to all wxWidgets
- # platforms:
- self._font = wx.Font( font.GetPointSize(), wx.TELETYPE, font.GetStyle(),
- font.GetWeight(), font.GetUnderlined())
-#### dbg('font string: "%s"' % font.GetNativeFontInfo().ToString())
-
- self.SetFont(self._font)
-#### dbg(indent=0)
-
-
- def _OnTextChange(self, event):
- """
- Handler for EVT_TEXT event.
- self._Change() is provided for subclasses, and may return False to
- skip this method logic. This function returns True if the event
- detected was a legitimate event, or False if it was a "bogus"
- EVT_TEXT event. (NOTE: There is currently an issue with calling
- .SetValue from within the EVT_CHAR handler that causes duplicate
- EVT_TEXT events for the same change.)
- """
- newvalue = self._GetValue()
-## dbg('MaskedEditMixin::_OnTextChange: value: "%s"' % newvalue, indent=1)
- bValid = False
- if self._ignoreChange: # ie. if an "intermediate text change event"
-## dbg(indent=0)
- return bValid
-
- ##! WS: For some inexplicable reason, every wxTextCtrl.SetValue
- ## call is generating two (2) EVT_TEXT events.
- ## This is the only mechanism I can find to mask this problem:
- if newvalue == self._curValue:
-## dbg('ignoring bogus text change event', indent=0)
- pass
- else:
-## dbg('curvalue: "%s", newvalue: "%s"' % (self._curValue, newvalue))
- if self._Change():
- if self._signOk and self._isNeg and newvalue.find('-') == -1 and newvalue.find('(') == -1:
-## dbg('clearing self._isNeg')
- self._isNeg = False
- text, self._signpos, self._right_signpos = self._getSignedValue()
- self._CheckValid() # Recolor control as appropriate
-## dbg('calling event.Skip()')
- event.Skip()
- bValid = True
- self._prevValue = self._curValue # save for undo
- self._curValue = newvalue # Save last seen value for next iteration
-## dbg(indent=0)
- return bValid
-
-
- def _OnKeyDown(self, event):
- """
- This function allows the control to capture Ctrl-events like Ctrl-tab,
- that are not normally seen by the "cooked" EVT_CHAR routine.
- """
- # Get keypress value, adjusted by control options (e.g. convert to upper etc)
- key = event.GetKeyCode()
- if key in self._nav and event.ControlDown():
- # then this is the only place we will likely see these events;
- # process them now:
-## dbg('MaskedEditMixin::OnKeyDown: calling _OnChar')
- self._OnChar(event)
- return
- # else allow regular EVT_CHAR key processing
- event.Skip()
-
-
- def _OnChar(self, event):
- """
- This is the engine of wxMaskedEdit controls. It examines each keystroke,
- decides if it's allowed, where it should go or what action to take.
- """
-## dbg('MaskedEditMixin::_OnChar', indent=1)
-
- # Get keypress value, adjusted by control options (e.g. convert to upper etc)
- key = event.GetKeyCode()
- orig_pos = self._GetInsertionPoint()
- orig_value = self._GetValue()
-## dbg('keycode = ', key)
-## dbg('current pos = ', orig_pos)
-## dbg('current selection = ', self._GetSelection())
-
- if not self._Keypress(key):
-## dbg(indent=0)
- return
-
- # If no format string for this control, or the control is marked as "read-only",
- # skip the rest of the special processing, and just "do the standard thing:"
- if not self._mask or not self._IsEditable():
- event.Skip()
-## dbg(indent=0)
- return
-
- # Process navigation and control keys first, with
- # position/selection unadulterated:
- if key in self._nav + self._control:
- if self._keyhandlers.has_key(key):
- keep_processing = self._keyhandlers[key](event)
- if self._GetValue() != orig_value:
- self.modified = True
- if not keep_processing:
-## dbg(indent=0)
- return
- self._applyFormatting()
-## dbg(indent=0)
- return
-
- # Else... adjust the position as necessary for next input key,
- # and determine resulting selection:
- pos = self._adjustPos( orig_pos, key ) ## get insertion position, adjusted as needed
- sel_start, sel_to = self._GetSelection() ## check for a range of selected text
-## dbg("pos, sel_start, sel_to:", pos, sel_start, sel_to)
-
- keep_processing = True
- # Capture user past end of format field
- if pos > len(self.maskdict):
-## dbg("field length exceeded:",pos)
- keep_processing = False
-
- if keep_processing:
- if self._isMaskChar(pos): ## Get string of allowed characters for validation
- okchars = self._getAllowedChars(pos)
- else:
-## dbg('Not a valid position: pos = ', pos,"chars=",maskchars)
- okchars = ""
-
- key = self._adjustKey(pos, key) # apply formatting constraints to key:
-
- if self._keyhandlers.has_key(key):
- # there's an override for default behavior; use override function instead
-## dbg('using supplied key handler:', self._keyhandlers[key])
- keep_processing = self._keyhandlers[key](event)
- if self._GetValue() != orig_value:
- self.modified = True
- if not keep_processing:
-## dbg(indent=0)
- return
- # else skip default processing, but do final formatting
- if key < wx.WXK_SPACE or key > 255:
-## dbg('key < WXK_SPACE or key > 255')
- event.Skip() # non alphanumeric
- keep_processing = False
- else:
- field = self._FindField(pos)
-## dbg("key ='%s'" % chr(key))
- if chr(key) == ' ':
-## dbg('okSpaces?', field._okSpaces)
- pass
-
-
- if chr(key) in field._excludeChars + self._ctrl_constraints._excludeChars:
- keep_processing = False
-
- if keep_processing and self._isCharAllowed( chr(key), pos, checkRegex = True ):
-## dbg("key allowed by mask")
- # insert key into candidate new value, but don't change control yet:
- oldstr = self._GetValue()
- newstr, newpos, new_select_to, match_field, match_index = self._insertKey(
- chr(key), pos, sel_start, sel_to, self._GetValue(), allowAutoSelect = True)
-## dbg("str with '%s' inserted:" % chr(key), '"%s"' % newstr)
- if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
-## dbg('not valid; checking to see if adjusted string is:')
- keep_processing = False
- if self._isFloat and newstr != self._template:
- newstr = self._adjustFloat(newstr)
-## dbg('adjusted str:', newstr)
- if self.IsValid(newstr):
-## dbg("it is!")
- keep_processing = True
- wx.CallAfter(self._SetInsertionPoint, self._decimalpos)
- if not keep_processing:
-## dbg("key disallowed by validation")
- if not wx.Validator_IsSilent() and orig_pos == pos:
- wx.Bell()
-
- if keep_processing:
- unadjusted = newstr
-
- # special case: adjust date value as necessary:
- if self._isDate and newstr != self._template:
- newstr = self._adjustDate(newstr)
-## dbg('adjusted newstr:', newstr)
-
- if newstr != orig_value:
- self.modified = True
-
- wx.CallAfter(self._SetValue, newstr)
-
- # Adjust insertion point on date if just entered 2 digit year, and there are now 4 digits:
- if not self.IsDefault() and self._isDate and self._4digityear:
- year2dig = self._dateExtent - 2
- if pos == year2dig and unadjusted[year2dig] != newstr[year2dig]:
- newpos = pos+2
-
- wx.CallAfter(self._SetInsertionPoint, newpos)
-
- if match_field is not None:
-## dbg('matched field')
- self._OnAutoSelect(match_field, match_index)
-
- if new_select_to != newpos:
-## dbg('queuing selection: (%d, %d)' % (newpos, new_select_to))
- wx.CallAfter(self._SetSelection, newpos, new_select_to)
- else:
- newfield = self._FindField(newpos)
- if newfield != field and newfield._selectOnFieldEntry:
-## dbg('queuing selection: (%d, %d)' % (newfield._extent[0], newfield._extent[1]))
- wx.CallAfter(self._SetSelection, newfield._extent[0], newfield._extent[1])
- keep_processing = False
-
- elif keep_processing:
-## dbg('char not allowed')
- keep_processing = False
- if (not wx.Validator_IsSilent()) and orig_pos == pos:
- wx.Bell()
-
- self._applyFormatting()
-
- # Move to next insertion point
- if keep_processing and key not in self._nav:
- pos = self._GetInsertionPoint()
- next_entry = self._findNextEntry( pos )
- if pos != next_entry:
-## dbg("moving from %(pos)d to next valid entry: %(next_entry)d" % locals())
- wx.CallAfter(self._SetInsertionPoint, next_entry )
-
- if self._isTemplateChar(pos):
- self._AdjustField(pos)
-## dbg(indent=0)
-
-
- def _FindFieldExtent(self, pos=None, getslice=False, value=None):
- """ returns editable extent of field corresponding to
- position pos, and, optionally, the contents of that field
- in the control or the value specified.
- Template chars are bound to the preceding field.
- For masks beginning with template chars, these chars are ignored
- when calculating the current field.
-
- Eg: with template (###) ###-####,
- >>> self._FindFieldExtent(pos=0)
- 1, 4
- >>> self._FindFieldExtent(pos=1)
- 1, 4
- >>> self._FindFieldExtent(pos=5)
- 1, 4
- >>> self._FindFieldExtent(pos=6)
- 6, 9
- >>> self._FindFieldExtent(pos=10)
- 10, 14
- etc.
- """
-## dbg('MaskedEditMixin::_FindFieldExtent(pos=%s, getslice=%s)' % (str(pos), str(getslice)) ,indent=1)
-
- field = self._FindField(pos)
- if not field:
- if getslice:
- return None, None, ""
- else:
- return None, None
- edit_start, edit_end = field._extent
- if getslice:
- if value is None: value = self._GetValue()
- slice = value[edit_start:edit_end]
-## dbg('edit_start:', edit_start, 'edit_end:', edit_end, 'slice: "%s"' % slice)
-## dbg(indent=0)
- return edit_start, edit_end, slice
- else:
-## dbg('edit_start:', edit_start, 'edit_end:', edit_end)
-## dbg(indent=0)
- return edit_start, edit_end
-
-
- def _FindField(self, pos=None):
- """
- Returns the field instance in which pos resides.
- Template chars are bound to the preceding field.
- For masks beginning with template chars, these chars are ignored
- when calculating the current field.
-
- """
-#### dbg('MaskedEditMixin::_FindField(pos=%s)' % str(pos) ,indent=1)
- if pos is None: pos = self._GetInsertionPoint()
- elif pos < 0 or pos > self._masklength:
- raise IndexError('position %s out of range of control' % str(pos))
-
- if len(self._fields) == 0:
-## dbg(indent=0)
- return None
-
- # else...
-#### dbg(indent=0)
- return self._fields[self._lookupField[pos]]
-
-
- def ClearValue(self):
- """ Blanks the current control value by replacing it with the default value."""
-## dbg("MaskedEditMixin::ClearValue - value reset to default value (template)")
- self._SetValue( self._template )
- self._SetInsertionPoint(0)
- self.Refresh()
-
-
- def _baseCtrlEventHandler(self, event):
- """
- This function is used whenever a key should be handled by the base control.
- """
- event.Skip()
- return False
-
-
- def _OnUpNumeric(self, event):
- """
- Makes up-arrow act like shift-tab should; ie. take you to start of
- previous field.
- """
-## dbg('MaskedEditMixin::_OnUpNumeric', indent=1)
- event.m_shiftDown = 1
-## dbg('event.ShiftDown()?', event.ShiftDown())
- self._OnChangeField(event)
-## dbg(indent=0)
-
-
- def _OnArrow(self, event):
- """
- Used in response to left/right navigation keys; makes these actions skip
- over mask template chars.
- """
-## dbg("MaskedEditMixin::_OnArrow", indent=1)
- pos = self._GetInsertionPoint()
- keycode = event.GetKeyCode()
- sel_start, sel_to = self._GetSelection()
- entry_end = self._goEnd(getPosOnly=True)
- if keycode in (wx.WXK_RIGHT, wx.WXK_DOWN):
- if( ( not self._isTemplateChar(pos) and pos+1 > entry_end)
- or ( self._isTemplateChar(pos) and pos >= entry_end) ):
-## dbg("can't advance", indent=0)
- return False
- elif self._isTemplateChar(pos):
- self._AdjustField(pos)
- elif keycode in (wx.WXK_LEFT,wx.WXK_UP) and sel_start == sel_to and pos > 0 and self._isTemplateChar(pos-1):
-## dbg('adjusting field')
- self._AdjustField(pos)
-
- # treat as shifted up/down arrows as tab/reverse tab:
- if event.ShiftDown() and keycode in (wx.WXK_UP, wx.WXK_DOWN):
- # remove "shifting" and treat as (forward) tab:
- event.m_shiftDown = False
- keep_processing = self._OnChangeField(event)
-
- elif self._FindField(pos)._selectOnFieldEntry:
- if( keycode in (wx.WXK_UP, wx.WXK_LEFT)
- and sel_start != 0
- and self._isTemplateChar(sel_start-1)
- and sel_start != self._masklength
- and not self._signOk and not self._useParens):
-
- # call _OnChangeField to handle "ctrl-shifted event"
- # (which moves to previous field and selects it.)
- event.m_shiftDown = True
- event.m_ControlDown = True
- keep_processing = self._OnChangeField(event)
- elif( keycode in (wx.WXK_DOWN, wx.WXK_RIGHT)
- and sel_to != self._masklength
- and self._isTemplateChar(sel_to)):
-
- # when changing field to the right, ensure don't accidentally go left instead
- event.m_shiftDown = False
- keep_processing = self._OnChangeField(event)
- else:
- # treat arrows as normal, allowing selection
- # as appropriate:
-## dbg('using base ctrl event processing')
- event.Skip()
- else:
- if( (sel_to == self._fields[0]._extent[0] and keycode == wx.WXK_LEFT)
- or (sel_to == self._masklength and keycode == wx.WXK_RIGHT) ):
- if not wx.Validator_IsSilent():
- wx.Bell()
- else:
- # treat arrows as normal, allowing selection
- # as appropriate:
-## dbg('using base event processing')
- event.Skip()
-
- keep_processing = False
-## dbg(indent=0)
- return keep_processing
-
-
- def _OnCtrl_S(self, event):
- """ Default Ctrl-S handler; prints value information if demo enabled. """
-## dbg("MaskedEditMixin::_OnCtrl_S")
- if self._demo:
- print 'MaskedEditMixin.GetValue() = "%s"\nMaskedEditMixin.GetPlainValue() = "%s"' % (self.GetValue(), self.GetPlainValue())
- print "Valid? => " + str(self.IsValid())
- print "Current field, start, end, value =", str( self._FindFieldExtent(getslice=True))
- return False
-
-
- def _OnCtrl_X(self, event=None):
- """ Handles ctrl-x keypress in control and Cut operation on context menu.
- Should return False to skip other processing. """
-## dbg("MaskedEditMixin::_OnCtrl_X", indent=1)
- self.Cut()
-## dbg(indent=0)
- return False
-
- def _OnCtrl_C(self, event=None):
- """ Handles ctrl-C keypress in control and Copy operation on context menu.
- Uses base control handling. Should return False to skip other processing."""
- self.Copy()
- return False
-
- def _OnCtrl_V(self, event=None):
- """ Handles ctrl-V keypress in control and Paste operation on context menu.
- Should return False to skip other processing. """
-## dbg("MaskedEditMixin::_OnCtrl_V", indent=1)
- self.Paste()
-## dbg(indent=0)
- return False
-
- def _OnCtrl_Z(self, event=None):
- """ Handles ctrl-Z keypress in control and Undo operation on context menu.
- Should return False to skip other processing. """
-## dbg("MaskedEditMixin::_OnCtrl_Z", indent=1)
- self.Undo()
-## dbg(indent=0)
- return False
-
- def _OnCtrl_A(self,event=None):
- """ Handles ctrl-a keypress in control. Should return False to skip other processing. """
- end = self._goEnd(getPosOnly=True)
- if not event or event.ShiftDown():
- wx.CallAfter(self._SetInsertionPoint, 0)
- wx.CallAfter(self._SetSelection, 0, self._masklength)
- else:
- wx.CallAfter(self._SetInsertionPoint, 0)
- wx.CallAfter(self._SetSelection, 0, end)
- return False
-
-
- def _OnErase(self, event=None):
- """ Handles backspace and delete keypress in control. Should return False to skip other processing."""
-## dbg("MaskedEditMixin::_OnErase", indent=1)
- sel_start, sel_to = self._GetSelection() ## check for a range of selected text
-
- if event is None: # called as action routine from Cut() operation.
- key = wx.WXK_DELETE
- else:
- key = event.GetKeyCode()
-
- field = self._FindField(sel_to)
- start, end = field._extent
- value = self._GetValue()
- oldstart = sel_start
-
- # If trying to erase beyond "legal" bounds, disallow operation:
- if( (sel_to == 0 and key == wx.WXK_BACK)
- or (self._signOk and sel_to == 1 and value[0] == ' ' and key == wx.WXK_BACK)
- or (sel_to == self._masklength and sel_start == sel_to and key == wx.WXK_DELETE and not field._insertRight)
- or (self._signOk and self._useParens
- and sel_start == sel_to
- and sel_to == self._masklength - 1
- and value[sel_to] == ' ' and key == wx.WXK_DELETE and not field._insertRight) ):
- if not wx.Validator_IsSilent():
- wx.Bell()
-## dbg(indent=0)
- return False
-
-
- if( field._insertRight # an insert-right field
- and value[start:end] != self._template[start:end] # and field not empty
- and sel_start >= start # and selection starts in field
- and ((sel_to == sel_start # and no selection
- and sel_to == end # and cursor at right edge
- and key in (wx.WXK_BACK, wx.WXK_DELETE)) # and either delete or backspace key
- or # or
- (key == wx.WXK_BACK # backspacing
- and (sel_to == end # and selection ends at right edge
- or sel_to < end and field._allowInsert)) ) ): # or allow right insert at any point in field
-
-## dbg('delete left')
- # if backspace but left of cursor is empty, adjust cursor right before deleting
- while( key == wx.WXK_BACK
- and sel_start == sel_to
- and sel_start < end
- and value[start:sel_start] == self._template[start:sel_start]):
- sel_start += 1
- sel_to = sel_start
-
-## dbg('sel_start, start:', sel_start, start)
-
- if sel_start == sel_to:
- keep = sel_start -1
- else:
- keep = sel_start
- newfield = value[start:keep] + value[sel_to:end]
-
- # handle sign char moving from outside field into the field:
- move_sign_into_field = False
- if not field._padZero and self._signOk and self._isNeg and value[0] in ('-', '('):
- signchar = value[0]
- newfield = signchar + newfield
- move_sign_into_field = True
-## dbg('cut newfield: "%s"' % newfield)
-
- # handle what should fill in from the left:
- left = ""
- for i in range(start, end - len(newfield)):
- if field._padZero:
- left += '0'
- elif( self._signOk and self._isNeg and i == 1
- and ((self._useParens and newfield.find('(') == -1)
- or (not self._useParens and newfield.find('-') == -1)) ):
- left += ' '
- else:
- left += self._template[i] # this can produce strange results in combination with default values...
- newfield = left + newfield
-## dbg('filled newfield: "%s"' % newfield)
-
- newstr = value[:start] + newfield + value[end:]
-
- # (handle sign located in "mask position" in front of field prior to delete)
- if move_sign_into_field:
- newstr = ' ' + newstr[1:]
- pos = sel_to
- else:
- # handle erasure of (left) sign, moving selection accordingly...
- if self._signOk and sel_start == 0:
- newstr = value = ' ' + value[1:]
- sel_start += 1
-
- if field._allowInsert and sel_start >= start:
- # selection (if any) falls within current insert-capable field:
- select_len = sel_to - sel_start
- # determine where cursor should end up:
- if key == wx.WXK_BACK:
- if select_len == 0:
- newpos = sel_start -1
- else:
- newpos = sel_start
- erase_to = sel_to
- else:
- newpos = sel_start
- if sel_to == sel_start:
- erase_to = sel_to + 1
- else:
- erase_to = sel_to
-
- if self._isTemplateChar(newpos) and select_len == 0:
- if self._signOk:
- if value[newpos] in ('(', '-'):
- newpos += 1 # don't move cusor
- newstr = ' ' + value[newpos:]
- elif value[newpos] == ')':
- # erase right sign, but don't move cursor; (matching left sign handled later)
- newstr = value[:newpos] + ' '
- else:
- # no deletion; just move cursor
- newstr = value
- else:
- # no deletion; just move cursor
- newstr = value
- else:
- if erase_to > end: erase_to = end
- erase_len = erase_to - newpos
-
- left = value[start:newpos]
-## dbg("retained ='%s'" % value[erase_to:end], 'sel_to:', sel_to, "fill: '%s'" % self._template[end - erase_len:end])
- right = value[erase_to:end] + self._template[end-erase_len:end]
- pos_adjust = 0
- if field._alignRight:
- rstripped = right.rstrip()
- if rstripped != right:
- pos_adjust = len(right) - len(rstripped)
- right = rstripped
-
- if not field._insertRight and value[-1] == ')' and end == self._masklength - 1:
- # need to shift ) into the field:
- right = right[:-1] + ')'
- value = value[:-1] + ' '
-
- newfield = left+right
- if pos_adjust:
- newfield = newfield.rjust(end-start)
- newpos += pos_adjust
-## dbg("left='%s', right ='%s', newfield='%s'" %(left, right, newfield))
- newstr = value[:start] + newfield + value[end:]
-
- pos = newpos
-
- else:
- if sel_start == sel_to:
-## dbg("current sel_start, sel_to:", sel_start, sel_to)
- if key == wx.WXK_BACK:
- sel_start, sel_to = sel_to-1, sel_to-1
-## dbg("new sel_start, sel_to:", sel_start, sel_to)
-
- if field._padZero and not value[start:sel_to].replace('0', '').replace(' ','').replace(field._fillChar, ''):
- # preceding chars (if any) are zeros, blanks or fillchar; new char should be 0:
- newchar = '0'
- else:
- newchar = self._template[sel_to] ## get an original template character to "clear" the current char
-## dbg('value = "%s"' % value, 'value[%d] = "%s"' %(sel_start, value[sel_start]))
-
- if self._isTemplateChar(sel_to):
- if sel_to == 0 and self._signOk and value[sel_to] == '-': # erasing "template" sign char
- newstr = ' ' + value[1:]
- sel_to += 1
- elif self._signOk and self._useParens and (value[sel_to] == ')' or value[sel_to] == '('):
- # allow "change sign" by removing both parens:
- newstr = value[:self._signpos] + ' ' + value[self._signpos+1:-1] + ' '
- else:
- newstr = value
- newpos = sel_to
- else:
- if field._insertRight and sel_start == sel_to:
- # force non-insert-right behavior, by selecting char to be replaced:
- sel_to += 1
- newstr, ignore = self._insertKey(newchar, sel_start, sel_start, sel_to, value)
-
- else:
- # selection made
- newstr = self._eraseSelection(value, sel_start, sel_to)
-
- pos = sel_start # put cursor back at beginning of selection
-
- if self._signOk and self._useParens:
- # account for resultant unbalanced parentheses:
- left_signpos = newstr.find('(')
- right_signpos = newstr.find(')')
-
- if left_signpos == -1 and right_signpos != -1:
- # erased left-sign marker; get rid of right sign marker:
- newstr = newstr[:right_signpos] + ' ' + newstr[right_signpos+1:]
-
- elif left_signpos != -1 and right_signpos == -1:
- # erased right-sign marker; get rid of left-sign marker:
- newstr = newstr[:left_signpos] + ' ' + newstr[left_signpos+1:]
-
-## dbg("oldstr:'%s'" % value, 'oldpos:', oldstart)
-## dbg("newstr:'%s'" % newstr, 'pos:', pos)
-
- # if erasure results in an invalid field, disallow it:
-## dbg('field._validRequired?', field._validRequired)
-## dbg('field.IsValid("%s")?' % newstr[start:end], field.IsValid(newstr[start:end]))
- if field._validRequired and not field.IsValid(newstr[start:end]):
- if not wx.Validator_IsSilent():
- wx.Bell()
-## dbg(indent=0)
- return False
-
- # if erasure results in an invalid value, disallow it:
- if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
- if not wx.Validator_IsSilent():
- wx.Bell()
-## dbg(indent=0)
- return False
-
-## dbg('setting value (later) to', newstr)
- wx.CallAfter(self._SetValue, newstr)
-## dbg('setting insertion point (later) to', pos)
- wx.CallAfter(self._SetInsertionPoint, pos)
-## dbg(indent=0)
- if newstr != value:
- self.modified = True
- return False
-
-
- def _OnEnd(self,event):
- """ Handles End keypress in control. Should return False to skip other processing. """
-## dbg("MaskedEditMixin::_OnEnd", indent=1)
- pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
- if not event.ControlDown():
- end = self._masklength # go to end of control
- if self._signOk and self._useParens:
- end = end - 1 # account for reserved char at end
- else:
- end_of_input = self._goEnd(getPosOnly=True)
- sel_start, sel_to = self._GetSelection()
- if sel_to < pos: sel_to = pos
- field = self._FindField(sel_to)
- field_end = self._FindField(end_of_input)
-
- # pick different end point if either:
- # - cursor not in same field
- # - or at or past last input already
- # - or current selection = end of current field:
-#### dbg('field != field_end?', field != field_end)
-#### dbg('sel_to >= end_of_input?', sel_to >= end_of_input)
- if field != field_end or sel_to >= end_of_input:
- edit_start, edit_end = field._extent
-#### dbg('edit_end:', edit_end)
-#### dbg('sel_to:', sel_to)
-#### dbg('sel_to == edit_end?', sel_to == edit_end)
-#### dbg('field._index < self._field_indices[-1]?', field._index < self._field_indices[-1])
-
- if sel_to == edit_end and field._index < self._field_indices[-1]:
- edit_start, edit_end = self._FindFieldExtent(self._findNextEntry(edit_end)) # go to end of next field:
- end = edit_end
-## dbg('end moved to', end)
-
- elif sel_to == edit_end and field._index == self._field_indices[-1]:
- # already at edit end of last field; select to end of control:
- end = self._masklength
-## dbg('end moved to', end)
- else:
- end = edit_end # select to end of current field
-## dbg('end moved to ', end)
- else:
- # select to current end of input
- end = end_of_input
-
-
-#### dbg('pos:', pos, 'end:', end)
-
- if event.ShiftDown():
- if not event.ControlDown():
-## dbg("shift-end; select to end of control")
- pass
- else:
-## dbg("shift-ctrl-end; select to end of non-whitespace")
- pass
- wx.CallAfter(self._SetInsertionPoint, pos)
- wx.CallAfter(self._SetSelection, pos, end)
- else:
- if not event.ControlDown():
-## dbg('go to end of control:')
- pass
- wx.CallAfter(self._SetInsertionPoint, end)
- wx.CallAfter(self._SetSelection, end, end)
-
-## dbg(indent=0)
- return False
-
-
- def _OnReturn(self, event):
- """
- Changes the event to look like a tab event, so we can then call
- event.Skip() on it, and have the parent form "do the right thing."
- """
-## dbg('MaskedEditMixin::OnReturn')
- event.m_keyCode = wx.WXK_TAB
- event.Skip()
-
-
- def _OnHome(self,event):
- """ Handles Home keypress in control. Should return False to skip other processing."""
-## dbg("MaskedEditMixin::_OnHome", indent=1)
- pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
- sel_start, sel_to = self._GetSelection()
-
- # There are 5 cases here:
-
- # 1) shift: select from start of control to end of current
- # selection.
- if event.ShiftDown() and not event.ControlDown():
-## dbg("shift-home; select to start of control")
- start = 0
- end = sel_start
-
- # 2) no shift, no control: move cursor to beginning of control.
- elif not event.ControlDown():
-## dbg("home; move to start of control")
- start = 0
- end = 0
-
- # 3) No shift, control: move cursor back to beginning of field; if
- # there already, go to beginning of previous field.
- # 4) shift, control, start of selection not at beginning of control:
- # move sel_start back to start of field; if already there, go to
- # start of previous field.
- elif( event.ControlDown()
- and (not event.ShiftDown()
- or (event.ShiftDown() and sel_start > 0) ) ):
- if len(self._field_indices) > 1:
- field = self._FindField(sel_start)
- start, ignore = field._extent
- if sel_start == start and field._index != self._field_indices[0]: # go to start of previous field:
- start, ignore = self._FindFieldExtent(sel_start-1)
- elif sel_start == start:
- start = 0 # go to literal beginning if edit start
- # not at that point
- end_of_field = True
-
- else:
- start = 0
-
- if not event.ShiftDown():
-## dbg("ctrl-home; move to beginning of field")
- end = start
- else:
-## dbg("shift-ctrl-home; select to beginning of field")
- end = sel_to
-
- else:
- # 5) shift, control, start of selection at beginning of control:
- # unselect by moving sel_to backward to beginning of current field;
- # if already there, move to start of previous field.
- start = sel_start
- if len(self._field_indices) > 1:
- # find end of previous field:
- field = self._FindField(sel_to)
- if sel_to > start and field._index != self._field_indices[0]:
- ignore, end = self._FindFieldExtent(field._extent[0]-1)
- else:
- end = start
- end_of_field = True
- else:
- end = start
- end_of_field = False
-## dbg("shift-ctrl-home; unselect to beginning of field")
-
-## dbg('queuing new sel_start, sel_to:', (start, end))
- wx.CallAfter(self._SetInsertionPoint, start)
- wx.CallAfter(self._SetSelection, start, end)
-## dbg(indent=0)
- return False
-
-
- def _OnChangeField(self, event):
- """
- Primarily handles TAB events, but can be used for any key that
- designer wants to change fields within a masked edit control.
- NOTE: at the moment, although coded to handle shift-TAB and
- control-shift-TAB, these events are not sent to the controls
- by the framework.
- """
-## dbg('MaskedEditMixin::_OnChangeField', indent = 1)
- # determine end of current field:
- pos = self._GetInsertionPoint()
-## dbg('current pos:', pos)
- sel_start, sel_to = self._GetSelection()
-
- if self._masklength < 0: # no fields; process tab normally
- self._AdjustField(pos)
- if event.GetKeyCode() == wx.WXK_TAB:
-## dbg('tab to next ctrl')
- event.Skip()
- #else: do nothing
-## dbg(indent=0)
- return False
-
-
- if event.ShiftDown():
-
- # "Go backward"
-
- # NOTE: doesn't yet work with SHIFT-tab under wx; the control
- # never sees this event! (But I've coded for it should it ever work,
- # and it *does* work for '.' in IpAddrCtrl.)
- field = self._FindField(pos)
- index = field._index
- field_start = field._extent[0]
- if pos < field_start:
-## dbg('cursor before 1st field; cannot change to a previous field')
- if not wx.Validator_IsSilent():
- wx.Bell()
- return False
-
- if event.ControlDown():
-## dbg('queuing select to beginning of field:', field_start, pos)
- wx.CallAfter(self._SetInsertionPoint, field_start)
- wx.CallAfter(self._SetSelection, field_start, pos)
-## dbg(indent=0)
- return False
-
- elif index == 0:
- # We're already in the 1st field; process shift-tab normally:
- self._AdjustField(pos)
- if event.GetKeyCode() == wx.WXK_TAB:
-## dbg('tab to previous ctrl')
- event.Skip()
- else:
-## dbg('position at beginning')
- wx.CallAfter(self._SetInsertionPoint, field_start)
-## dbg(indent=0)
- return False
- else:
- # find beginning of previous field:
- begin_prev = self._FindField(field_start-1)._extent[0]
- self._AdjustField(pos)
-## dbg('repositioning to', begin_prev)
- wx.CallAfter(self._SetInsertionPoint, begin_prev)
- if self._FindField(begin_prev)._selectOnFieldEntry:
- edit_start, edit_end = self._FindFieldExtent(begin_prev)
-## dbg('queuing selection to (%d, %d)' % (edit_start, edit_end))
- wx.CallAfter(self._SetInsertionPoint, edit_start)
- wx.CallAfter(self._SetSelection, edit_start, edit_end)
-## dbg(indent=0)
- return False
-
- else:
- # "Go forward"
- field = self._FindField(sel_to)
- field_start, field_end = field._extent
- if event.ControlDown():
-## dbg('queuing select to end of field:', pos, field_end)
- wx.CallAfter(self._SetInsertionPoint, pos)
- wx.CallAfter(self._SetSelection, pos, field_end)
-## dbg(indent=0)
- return False
- else:
- if pos < field_start:
-## dbg('cursor before 1st field; go to start of field')
- wx.CallAfter(self._SetInsertionPoint, field_start)
- if field._selectOnFieldEntry:
- wx.CallAfter(self._SetSelection, field_start, field_end)
- else:
- wx.CallAfter(self._SetSelection, field_start, field_start)
- return False
- # else...
-## dbg('end of current field:', field_end)
-## dbg('go to next field')
- if field_end == self._fields[self._field_indices[-1]]._extent[1]:
- self._AdjustField(pos)
- if event.GetKeyCode() == wx.WXK_TAB:
-## dbg('tab to next ctrl')
- event.Skip()
- else:
-## dbg('position at end')
- wx.CallAfter(self._SetInsertionPoint, field_end)
-## dbg(indent=0)
- return False
- else:
- # we have to find the start of the next field
- next_pos = self._findNextEntry(field_end)
- if next_pos == field_end:
-## dbg('already in last field')
- self._AdjustField(pos)
- if event.GetKeyCode() == wx.WXK_TAB:
-## dbg('tab to next ctrl')
- event.Skip()
- #else: do nothing
-## dbg(indent=0)
- return False
- else:
- self._AdjustField( pos )
-
- # move cursor to appropriate point in the next field and select as necessary:
- field = self._FindField(next_pos)
- edit_start, edit_end = field._extent
- if field._selectOnFieldEntry:
-## dbg('move to ', next_pos)
- wx.CallAfter(self._SetInsertionPoint, next_pos)
- edit_start, edit_end = self._FindFieldExtent(next_pos)
-## dbg('queuing select', edit_start, edit_end)
- wx.CallAfter(self._SetSelection, edit_start, edit_end)
- else:
- if field._insertRight:
- next_pos = field._extent[1]
-## dbg('move to ', next_pos)
- wx.CallAfter(self._SetInsertionPoint, next_pos)
-## dbg(indent=0)
- return False
-
-
- def _OnDecimalPoint(self, event):
-## dbg('MaskedEditMixin::_OnDecimalPoint', indent=1)
-
- pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
-
- if self._isFloat: ## handle float value, move to decimal place
-## dbg('key == Decimal tab; decimal pos:', self._decimalpos)
- value = self._GetValue()
- if pos < self._decimalpos:
- clipped_text = value[0:pos] + self._decimalChar + value[self._decimalpos+1:]
-## dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text)
- newstr = self._adjustFloat(clipped_text)
- else:
- newstr = self._adjustFloat(value)
- wx.CallAfter(self._SetValue, newstr)
- fraction = self._fields[1]
- start, end = fraction._extent
- wx.CallAfter(self._SetInsertionPoint, start)
- if fraction._selectOnFieldEntry:
-## dbg('queuing selection after decimal point to:', (start, end))
- wx.CallAfter(self._SetSelection, start, end)
- keep_processing = False
-
- if self._isInt: ## handle integer value, truncate from current position
-## dbg('key == Integer decimal event')
- value = self._GetValue()
- clipped_text = value[0:pos]
-## dbg('value: "%s"' % self._GetValue(), "clipped_text:'%s'" % clipped_text)
- newstr = self._adjustInt(clipped_text)
-## dbg('newstr: "%s"' % newstr)
- wx.CallAfter(self._SetValue, newstr)
- newpos = len(newstr.rstrip())
- if newstr.find(')') != -1:
- newpos -= 1 # (don't move past right paren)
- wx.CallAfter(self._SetInsertionPoint, newpos)
- keep_processing = False
-## dbg(indent=0)
-
-
- def _OnChangeSign(self, event):
-## dbg('MaskedEditMixin::_OnChangeSign', indent=1)
- key = event.GetKeyCode()
- pos = self._adjustPos(self._GetInsertionPoint(), key)
- value = self._eraseSelection()
- integer = self._fields[0]
- start, end = integer._extent
-
-#### dbg('adjusted pos:', pos)
- if chr(key) in ('-','+','(', ')') or (chr(key) == " " and pos == self._signpos):
- cursign = self._isNeg
-## dbg('cursign:', cursign)
- if chr(key) in ('-','(', ')'):
- self._isNeg = (not self._isNeg) ## flip value
- else:
- self._isNeg = False
-## dbg('isNeg?', self._isNeg)
-
- text, self._signpos, self._right_signpos = self._getSignedValue(candidate=value)
-## dbg('text:"%s"' % text, 'signpos:', self._signpos, 'right_signpos:', self._right_signpos)
- if text is None:
- text = value
-
- if self._isNeg and self._signpos is not None and self._signpos != -1:
- if self._useParens and self._right_signpos is not None:
- text = text[:self._signpos] + '(' + text[self._signpos+1:self._right_signpos] + ')' + text[self._right_signpos+1:]
- else:
- text = text[:self._signpos] + '-' + text[self._signpos+1:]
- else:
-#### dbg('self._isNeg?', self._isNeg, 'self.IsValid(%s)' % text, self.IsValid(text))
- if self._useParens:
- text = text[:self._signpos] + ' ' + text[self._signpos+1:self._right_signpos] + ' ' + text[self._right_signpos+1:]
- else:
- text = text[:self._signpos] + ' ' + text[self._signpos+1:]
-## dbg('clearing self._isNeg')
- self._isNeg = False
-
- wx.CallAfter(self._SetValue, text)
- wx.CallAfter(self._applyFormatting)
-## dbg('pos:', pos, 'signpos:', self._signpos)
- if pos == self._signpos or integer.IsEmpty(text[start:end]):
- wx.CallAfter(self._SetInsertionPoint, self._signpos+1)
- else:
- wx.CallAfter(self._SetInsertionPoint, pos)
-
- keep_processing = False
- else:
- keep_processing = True
-## dbg(indent=0)
- return keep_processing
-
-
- def _OnGroupChar(self, event):
- """
- This handler is only registered if the mask is a numeric mask.
- It allows the insertion of ',' or '.' if appropriate.
- """
-## dbg('MaskedEditMixin::_OnGroupChar', indent=1)
- keep_processing = True
- pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
- sel_start, sel_to = self._GetSelection()
- groupchar = self._fields[0]._groupChar
- if not self._isCharAllowed(groupchar, pos, checkRegex=True):
- keep_processing = False
- if not wx.Validator_IsSilent():
- wx.Bell()
-
- if keep_processing:
- newstr, newpos = self._insertKey(groupchar, pos, sel_start, sel_to, self._GetValue() )
-## dbg("str with '%s' inserted:" % groupchar, '"%s"' % newstr)
- if self._ctrl_constraints._validRequired and not self.IsValid(newstr):
- keep_processing = False
- if not wx.Validator_IsSilent():
- wx.Bell()
-
- if keep_processing:
- wx.CallAfter(self._SetValue, newstr)
- wx.CallAfter(self._SetInsertionPoint, newpos)
- keep_processing = False
-## dbg(indent=0)
- return keep_processing
-
-
- def _findNextEntry(self,pos, adjustInsert=True):
- """ Find the insertion point for the next valid entry character position."""
- if self._isTemplateChar(pos): # if changing fields, pay attn to flag
- adjustInsert = adjustInsert
- else: # else within a field; flag not relevant
- adjustInsert = False
-
- while self._isTemplateChar(pos) and pos < self._masklength:
- pos += 1
-
- # if changing fields, and we've been told to adjust insert point,
- # look at new field; if empty and right-insert field,
- # adjust to right edge:
- if adjustInsert and pos < self._masklength:
- field = self._FindField(pos)
- start, end = field._extent
- slice = self._GetValue()[start:end]
- if field._insertRight and field.IsEmpty(slice):
- pos = end
- return pos
-
-
- def _findNextTemplateChar(self, pos):
- """ Find the position of the next non-editable character in the mask."""
- while not self._isTemplateChar(pos) and pos < self._masklength:
- pos += 1
- return pos
-
-
- def _OnAutoCompleteField(self, event):
-## dbg('MaskedEditMixin::_OnAutoCompleteField', indent =1)
- pos = self._GetInsertionPoint()
- field = self._FindField(pos)
- edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True)
-
- match_index = None
- keycode = event.GetKeyCode()
-
- if field._fillChar != ' ':
- text = slice.replace(field._fillChar, '')
- else:
- text = slice
- text = text.strip()
- keep_processing = True # (assume True to start)
-## dbg('field._hasList?', field._hasList)
- if field._hasList:
-## dbg('choices:', field._choices)
-## dbg('compareChoices:', field._compareChoices)
- choices, choice_required = field._compareChoices, field._choiceRequired
- if keycode in (wx.WXK_PRIOR, wx.WXK_UP):
- direction = -1
- else:
- direction = 1
- match_index, partial_match = self._autoComplete(direction, choices, text, compareNoCase=field._compareNoCase, current_index = field._autoCompleteIndex)
- if( match_index is None
- and (keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT]
- or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown() ) ) ):
- # Select the 1st thing from the list:
- match_index = 0
-
- if( match_index is not None
- and ( keycode in self._autoCompleteKeycodes + [wx.WXK_PRIOR, wx.WXK_NEXT]
- or (keycode in [wx.WXK_UP, wx.WXK_DOWN] and event.ShiftDown())
- or (keycode == wx.WXK_DOWN and partial_match) ) ):
-
- # We're allowed to auto-complete:
-## dbg('match found')
- value = self._GetValue()
- newvalue = value[:edit_start] + field._choices[match_index] + value[edit_end:]
-## dbg('setting value to "%s"' % newvalue)
- self._SetValue(newvalue)
- self._SetInsertionPoint(min(edit_end, len(newvalue.rstrip())))
- self._OnAutoSelect(field, match_index)
- self._CheckValid() # recolor as appopriate
-
-
- if keycode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT):
- # treat as left right arrow if unshifted, tab/shift tab if shifted.
- if event.ShiftDown():
- if keycode in (wx.WXK_DOWN, wx.WXK_RIGHT):
- # remove "shifting" and treat as (forward) tab:
- event.m_shiftDown = False
- keep_processing = self._OnChangeField(event)
- else:
- keep_processing = self._OnArrow(event)
- # else some other key; keep processing the key
-
-## dbg('keep processing?', keep_processing, indent=0)
- return keep_processing
-
-
- def _OnAutoSelect(self, field, match_index = None):
- """
- Function called if autoselect feature is enabled and entire control
- is selected:
- """
-## dbg('MaskedEditMixin::OnAutoSelect', field._index)
- if match_index is not None:
- field._autoCompleteIndex = match_index
-
-
- def _autoComplete(self, direction, choices, value, compareNoCase, current_index):
- """
- This function gets called in response to Auto-complete events.
- It attempts to find a match to the specified value against the
- list of choices; if exact match, the index of then next
- appropriate value in the list, based on the given direction.
- If not an exact match, it will return the index of the 1st value from
- the choice list for which the partial value can be extended to match.
- If no match found, it will return None.
- The function returns a 2-tuple, with the 2nd element being a boolean
- that indicates if partial match was necessary.
- """
-## dbg('autoComplete(direction=', direction, 'choices=',choices, 'value=',value,'compareNoCase?', compareNoCase, 'current_index:', current_index, indent=1)
- if value is None:
-## dbg('nothing to match against', indent=0)
- return (None, False)
-
- partial_match = False
-
- if compareNoCase:
- value = value.lower()
-
- last_index = len(choices) - 1
- if value in choices:
-## dbg('"%s" in', choices)
- if current_index is not None and choices[current_index] == value:
- index = current_index
- else:
- index = choices.index(value)
-
-## dbg('matched "%s" (%d)' % (choices[index], index))
- if direction == -1:
-## dbg('going to previous')
- if index == 0: index = len(choices) - 1
- else: index -= 1
- else:
- if index == len(choices) - 1: index = 0
- else: index += 1
-## dbg('change value to "%s" (%d)' % (choices[index], index))
- match = index
- else:
- partial_match = True
- value = value.strip()
-## dbg('no match; try to auto-complete:')
- match = None
-## dbg('searching for "%s"' % value)
- if current_index is None:
- indices = range(len(choices))
- if direction == -1:
- indices.reverse()
- else:
- if direction == 1:
- indices = range(current_index +1, len(choices)) + range(current_index+1)
-## dbg('range(current_index+1 (%d), len(choices) (%d)) + range(%d):' % (current_index+1, len(choices), current_index+1), indices)
- else:
- indices = range(current_index-1, -1, -1) + range(len(choices)-1, current_index-1, -1)
-## dbg('range(current_index-1 (%d), -1) + range(len(choices)-1 (%d)), current_index-1 (%d):' % (current_index-1, len(choices)-1, current_index-1), indices)
-#### dbg('indices:', indices)
- for index in indices:
- choice = choices[index]
- if choice.find(value, 0) == 0:
-## dbg('match found:', choice)
- match = index
- break
- else: dbg('choice: "%s" - no match' % choice)
- if match is not None:
-## dbg('matched', match)
- pass
- else:
-## dbg('no match found')
- pass
-## dbg(indent=0)
- return (match, partial_match)
-
-
- def _AdjustField(self, pos):
- """
- This function gets called by default whenever the cursor leaves a field.
- The pos argument given is the char position before leaving that field.
- By default, floating point, integer and date values are adjusted to be
- legal in this function. Derived classes may override this function
- to modify the value of the control in a different way when changing fields.
-
- NOTE: these change the value immediately, and restore the cursor to
- the passed location, so that any subsequent code can then move it
- based on the operation being performed.
- """
- newvalue = value = self._GetValue()
- field = self._FindField(pos)
- start, end, slice = self._FindFieldExtent(getslice=True)
- newfield = field._AdjustField(slice)
- newvalue = value[:start] + newfield + value[end:]
-
- if self._isFloat and newvalue != self._template:
- newvalue = self._adjustFloat(newvalue)
-
- if self._ctrl_constraints._isInt and value != self._template:
- newvalue = self._adjustInt(value)
-
- if self._isDate and value != self._template:
- newvalue = self._adjustDate(value, fixcentury=True)
- if self._4digityear:
- year2dig = self._dateExtent - 2
- if pos == year2dig and value[year2dig] != newvalue[year2dig]:
- pos = pos+2
-
- if newvalue != value:
- self._SetValue(newvalue)
- self._SetInsertionPoint(pos)
-
-
- def _adjustKey(self, pos, key):
- """ Apply control formatting to the key (e.g. convert to upper etc). """
- field = self._FindField(pos)
- if field._forceupper and key in range(97,123):
- key = ord( chr(key).upper())
-
- if field._forcelower and key in range(97,123):
- key = ord( chr(key).lower())
-
- return key
-
-
- def _adjustPos(self, pos, key):
- """
- Checks the current insertion point position and adjusts it if
- necessary to skip over non-editable characters.
- """
-## dbg('_adjustPos', pos, key, indent=1)
- sel_start, sel_to = self._GetSelection()
- # If a numeric or decimal mask, and negatives allowed, reserve the
- # first space for sign, and last one if using parens.
- if( self._signOk
- and ((pos == self._signpos and key in (ord('-'), ord('+'), ord(' ')) )
- or self._useParens and pos == self._masklength -1)):
-## dbg('adjusted pos:', pos, indent=0)
- return pos
-
- if key not in self._nav:
- field = self._FindField(pos)
-
-## dbg('field._insertRight?', field._insertRight)
- if field._insertRight: # if allow right-insert
- start, end = field._extent
- slice = self._GetValue()[start:end].strip()
- field_len = end - start
- if pos == end: # if cursor at right edge of field
- # if not filled or supposed to stay in field, keep current position
-#### dbg('pos==end')
-#### dbg('len (slice):', len(slice))
-#### dbg('field_len?', field_len)
-#### dbg('pos==end; len (slice) < field_len?', len(slice) < field_len)
-#### dbg('not field._moveOnFieldFull?', not field._moveOnFieldFull)
- if len(slice) == field_len and field._moveOnFieldFull:
- # move cursor to next field:
- pos = self._findNextEntry(pos)
- self._SetInsertionPoint(pos)
- if pos < sel_to:
- self._SetSelection(pos, sel_to) # restore selection
- else:
- self._SetSelection(pos, pos) # remove selection
- else: # leave cursor alone
- pass
- else:
- # if at start of control, move to right edge
- if sel_to == sel_start and self._isTemplateChar(pos) and pos != end:
- pos = end # move to right edge
-## elif sel_start <= start and sel_to == end:
-## # select to right edge of field - 1 (to replace char)
-## pos = end - 1
-## self._SetInsertionPoint(pos)
-## # restore selection
-## self._SetSelection(sel_start, pos)
-
- elif self._signOk and sel_start == 0: # if selected to beginning and signed,
- # adjust to past reserved sign position:
- pos = self._fields[0]._extent[0]
- self._SetInsertionPoint(pos)
- # restore selection
- self._SetSelection(pos, sel_to)
- else:
- pass # leave position/selection alone
-
- # else make sure the user is not trying to type over a template character
- # If they are, move them to the next valid entry position
- elif self._isTemplateChar(pos):
- if( not field._moveOnFieldFull
- and (not self._signOk
- or (self._signOk
- and field._index == 0
- and pos > 0) ) ): # don't move to next field without explicit cursor movement
- pass
- else:
- # find next valid position
- pos = self._findNextEntry(pos)
- self._SetInsertionPoint(pos)
- if pos < sel_to: # restore selection
- self._SetSelection(pos, sel_to)
-## dbg('adjusted pos:', pos, indent=0)
- return pos
-
-
- def _adjustFloat(self, candidate=None):
- """
- 'Fixes' an floating point control. Collapses spaces, right-justifies, etc.
- """
-## dbg('MaskedEditMixin::_adjustFloat, candidate = "%s"' % candidate, indent=1)
- lenInt,lenFraction = [len(s) for s in self._mask.split('.')] ## Get integer, fraction lengths
-
- if candidate is None: value = self._GetValue()
- else: value = candidate
-## dbg('value = "%(value)s"' % locals(), 'len(value):', len(value))
- intStr, fracStr = value.split(self._decimalChar)
-
- intStr = self._fields[0]._AdjustField(intStr)
-## dbg('adjusted intStr: "%s"' % intStr)
- lenInt = len(intStr)
- fracStr = fracStr + ('0'*(lenFraction-len(fracStr))) # add trailing spaces to decimal
-
-## dbg('intStr "%(intStr)s"' % locals())
-## dbg('lenInt:', lenInt)
-
- intStr = string.rjust( intStr[-lenInt:], lenInt)
-## dbg('right-justifed intStr = "%(intStr)s"' % locals())
- newvalue = intStr + self._decimalChar + fracStr
-
- if self._signOk:
- if len(newvalue) < self._masklength:
- newvalue = ' ' + newvalue
- signedvalue = self._getSignedValue(newvalue)[0]
- if signedvalue is not None: newvalue = signedvalue
-
- # Finally, align string with decimal position, left-padding with
- # fillChar:
- newdecpos = newvalue.find(self._decimalChar)
- if newdecpos < self._decimalpos:
- padlen = self._decimalpos - newdecpos
- newvalue = string.join([' ' * padlen] + [newvalue] ,'')
-
- if self._signOk and self._useParens:
- if newvalue.find('(') != -1:
- newvalue = newvalue[:-1] + ')'
- else:
- newvalue = newvalue[:-1] + ' '
-
-## dbg('newvalue = "%s"' % newvalue)
- if candidate is None:
- wx.CallAfter(self._SetValue, newvalue)
-## dbg(indent=0)
- return newvalue
-
-
- def _adjustInt(self, candidate=None):
- """ 'Fixes' an integer control. Collapses spaces, right or left-justifies."""
-## dbg("MaskedEditMixin::_adjustInt", candidate)
- lenInt = self._masklength
- if candidate is None: value = self._GetValue()
- else: value = candidate
-
- intStr = self._fields[0]._AdjustField(value)
- intStr = intStr.strip() # drop extra spaces
-## dbg('adjusted field: "%s"' % intStr)
-
- if self._isNeg and intStr.find('-') == -1 and intStr.find('(') == -1:
- if self._useParens:
- intStr = '(' + intStr + ')'
- else:
- intStr = '-' + intStr
- elif self._isNeg and intStr.find('-') != -1 and self._useParens:
- intStr = intStr.replace('-', '(')
-
- if( self._signOk and ((self._useParens and intStr.find('(') == -1)
- or (not self._useParens and intStr.find('-') == -1))):
- intStr = ' ' + intStr
- if self._useParens:
- intStr += ' ' # space for right paren position
-
- elif self._signOk and self._useParens and intStr.find('(') != -1 and intStr.find(')') == -1:
- # ensure closing right paren:
- intStr += ')'
-
- if self._fields[0]._alignRight: ## Only if right-alignment is enabled
- intStr = intStr.rjust( lenInt )
- else:
- intStr = intStr.ljust( lenInt )
-
- if candidate is None:
- wx.CallAfter(self._SetValue, intStr )
- return intStr
-
-
- def _adjustDate(self, candidate=None, fixcentury=False, force4digit_year=False):
- """
- 'Fixes' a date control, expanding the year if it can.
- Applies various self-formatting options.
- """
-## dbg("MaskedEditMixin::_adjustDate", indent=1)
- if candidate is None: text = self._GetValue()
- else: text = candidate
-## dbg('text=', text)
- if self._datestyle == "YMD":
- year_field = 0
- else:
- year_field = 2
-
-## dbg('getYear: "%s"' % getYear(text, self._datestyle))
- year = string.replace( getYear( text, self._datestyle),self._fields[year_field]._fillChar,"") # drop extra fillChars
- month = getMonth( text, self._datestyle)
- day = getDay( text, self._datestyle)
-## dbg('self._datestyle:', self._datestyle, 'year:', year, 'Month', month, 'day:', day)
-
- yearVal = None
- yearstart = self._dateExtent - 4
- if( len(year) < 4
- and (fixcentury
- or force4digit_year
- or (self._GetInsertionPoint() > yearstart+1 and text[yearstart+2] == ' ')
- or (self._GetInsertionPoint() > yearstart+2 and text[yearstart+3] == ' ') ) ):
- ## user entered less than four digits and changing fields or past point where we could
- ## enter another digit:
- try:
- yearVal = int(year)
- except:
-## dbg('bad year=', year)
- year = text[yearstart:self._dateExtent]
-
- if len(year) < 4 and yearVal:
- if len(year) == 2:
- # Fix year adjustment to be less "20th century" :-) and to adjust heuristic as the
- # years pass...
- now = wx.DateTime_Now()
- century = (now.GetYear() /100) * 100 # "this century"
- twodig_year = now.GetYear() - century # "this year" (2 digits)
- # if separation between today's 2-digit year and typed value > 50,
- # assume last century,
- # else assume this century.
- #
- # Eg: if 2003 and yearVal == 30, => 2030
- # if 2055 and yearVal == 80, => 2080
- # if 2010 and yearVal == 96, => 1996
- #
- if abs(yearVal - twodig_year) > 50:
- yearVal = (century - 100) + yearVal
- else:
- yearVal = century + yearVal
- year = str( yearVal )
- else: # pad with 0's to make a 4-digit year
- year = "%04d" % yearVal
- if self._4digityear or force4digit_year:
- text = makeDate(year, month, day, self._datestyle, text) + text[self._dateExtent:]
-## dbg('newdate: "%s"' % text, indent=0)
- return text
-
-
- def _goEnd(self, getPosOnly=False):
- """ Moves the insertion point to the end of user-entry """
-## dbg("MaskedEditMixin::_goEnd; getPosOnly:", getPosOnly, indent=1)
- text = self._GetValue()
-#### dbg('text: "%s"' % text)
- i = 0
- if len(text.rstrip()):
- for i in range( min( self._masklength-1, len(text.rstrip())), -1, -1):
-#### dbg('i:', i, 'self._isMaskChar(%d)' % i, self._isMaskChar(i))
- if self._isMaskChar(i):
- char = text[i]
-#### dbg("text[%d]: '%s'" % (i, char))
- if char != ' ':
- i += 1
- break
-
- if i == 0:
- pos = self._goHome(getPosOnly=True)
- else:
- pos = min(i,self._masklength)
-
- field = self._FindField(pos)
- start, end = field._extent
- if field._insertRight and pos < end:
- pos = end
-## dbg('next pos:', pos)
-## dbg(indent=0)
- if getPosOnly:
- return pos
- else:
- self._SetInsertionPoint(pos)
-
-
- def _goHome(self, getPosOnly=False):
- """ Moves the insertion point to the beginning of user-entry """
-## dbg("MaskedEditMixin::_goHome; getPosOnly:", getPosOnly, indent=1)
- text = self._GetValue()
- for i in range(self._masklength):
- if self._isMaskChar(i):
- break
- pos = max(i, 0)
-## dbg(indent=0)
- if getPosOnly:
- return pos
- else:
- self._SetInsertionPoint(max(i,0))
-
-
-
- def _getAllowedChars(self, pos):
- """ Returns a string of all allowed user input characters for the provided
- mask character plus control options
- """
- maskChar = self.maskdict[pos]
- okchars = self.maskchardict[maskChar] ## entry, get mask approved characters
- field = self._FindField(pos)
- if okchars and field._okSpaces: ## Allow spaces?
- okchars += " "
- if okchars and field._includeChars: ## any additional included characters?
- okchars += field._includeChars
-#### dbg('okchars[%d]:' % pos, okchars)
- return okchars
-
-
- def _isMaskChar(self, pos):
- """ Returns True if the char at position pos is a special mask character (e.g. NCXaA#)
- """
- if pos < self._masklength:
- return self.ismasked[pos]
- else:
- return False
-
-
- def _isTemplateChar(self,Pos):
- """ Returns True if the char at position pos is a template character (e.g. -not- NCXaA#)
- """
- if Pos < self._masklength:
- return not self._isMaskChar(Pos)
- else:
- return False
-
-
- def _isCharAllowed(self, char, pos, checkRegex=False, allowAutoSelect=True, ignoreInsertRight=False):
- """ Returns True if character is allowed at the specific position, otherwise False."""
-## dbg('_isCharAllowed', char, pos, checkRegex, indent=1)
- field = self._FindField(pos)
- right_insert = False
-
- if self.controlInitialized:
- sel_start, sel_to = self._GetSelection()
- else:
- sel_start, sel_to = pos, pos
-
- if (field._insertRight or self._ctrl_constraints._insertRight) and not ignoreInsertRight:
- start, end = field._extent
- field_len = end - start
- if self.controlInitialized:
- value = self._GetValue()
- fstr = value[start:end].strip()
- if field._padZero:
- while fstr and fstr[0] == '0':
- fstr = fstr[1:]
- input_len = len(fstr)
- if self._signOk and '-' in fstr or '(' in fstr:
- input_len -= 1 # sign can move out of field, so don't consider it in length
- else:
- value = self._template
- input_len = 0 # can't get the current "value", so use 0
-
-
- # if entire field is selected or position is at end and field is not full,
- # or if allowed to right-insert at any point in field and field is not full and cursor is not at a fillChar:
- if( (sel_start, sel_to) == field._extent
- or (pos == end and input_len < field_len)):
- pos = end - 1
-## dbg('pos = end - 1 = ', pos, 'right_insert? 1')
- right_insert = True
- elif( field._allowInsert and sel_start == sel_to
- and (sel_to == end or (sel_to < self._masklength and value[sel_start] != field._fillChar))
- and input_len < field_len ):
- pos = sel_to - 1 # where character will go
-## dbg('pos = sel_to - 1 = ', pos, 'right_insert? 1')
- right_insert = True
- # else leave pos alone...
- else:
-## dbg('pos stays ', pos, 'right_insert? 0')
- pass
-
- if self._isTemplateChar( pos ): ## if a template character, return empty
-## dbg('%d is a template character; returning False' % pos, indent=0)
- return False
-
- if self._isMaskChar( pos ):
- okChars = self._getAllowedChars(pos)
-
- if self._fields[0]._groupdigits and (self._isInt or (self._isFloat and pos < self._decimalpos)):
- okChars += self._fields[0]._groupChar
-
- if self._signOk:
- if self._isInt or (self._isFloat and pos < self._decimalpos):
- okChars += '-'
- if self._useParens:
- okChars += '('
- elif self._useParens and (self._isInt or (self._isFloat and pos > self._decimalpos)):
- okChars += ')'
-
-#### dbg('%s in %s?' % (char, okChars), char in okChars)
- approved = char in okChars
-
- if approved and checkRegex:
-## dbg("checking appropriate regex's")
- value = self._eraseSelection(self._GetValue())
- if right_insert:
- at = pos+1
- else:
- at = pos
- if allowAutoSelect:
- newvalue, ignore, ignore, ignore, ignore = self._insertKey(char, at, sel_start, sel_to, value, allowAutoSelect=True)
- else:
- newvalue, ignore = self._insertKey(char, at, sel_start, sel_to, value)
-## dbg('newvalue: "%s"' % newvalue)
-
- fields = [self._FindField(pos)] + [self._ctrl_constraints]
- for field in fields: # includes fields[-1] == "ctrl_constraints"
- if field._regexMask and field._filter:
-## dbg('checking vs. regex')
- start, end = field._extent
- slice = newvalue[start:end]
- approved = (re.match( field._filter, slice) is not None)
-## dbg('approved?', approved)
- if not approved: break
-## dbg(indent=0)
- return approved
- else:
-## dbg('%d is a !???! character; returning False', indent=0)
- return False
-
-
- def _applyFormatting(self):
- """ Apply formatting depending on the control's state.
- Need to find a way to call this whenever the value changes, in case the control's
- value has been changed or set programatically.
- """
-## dbg(suspend=1)
-## dbg('MaskedEditMixin::_applyFormatting', indent=1)
-
- # Handle negative numbers
- if self._signOk:
- text, signpos, right_signpos = self._getSignedValue()
-## dbg('text: "%s", signpos:' % text, signpos)
- if not text or text[signpos] not in ('-','('):
- self._isNeg = False
-## dbg('no valid sign found; new sign:', self._isNeg)
- if text and signpos != self._signpos:
- self._signpos = signpos
- elif text and self._valid and not self._isNeg and text[signpos] in ('-', '('):
-## dbg('setting _isNeg to True')
- self._isNeg = True
-## dbg('self._isNeg:', self._isNeg)
-
- if self._signOk and self._isNeg:
- fc = self._signedForegroundColour
- else:
- fc = self._foregroundColour
-
- if hasattr(fc, '_name'):
- c =fc._name
- else:
- c = fc
-## dbg('setting foreground to', c)
- self.SetForegroundColour(fc)
-
- if self._valid:
-## dbg('valid')
- if self.IsEmpty():
- bc = self._emptyBackgroundColour
- else:
- bc = self._validBackgroundColour
- else:
-## dbg('invalid')
- bc = self._invalidBackgroundColour
- if hasattr(bc, '_name'):
- c =bc._name
- else:
- c = bc
-## dbg('setting background to', c)
- self.SetBackgroundColour(bc)
- self._Refresh()
-## dbg(indent=0, suspend=0)
-
-
- def _getAbsValue(self, candidate=None):
- """ Return an unsigned value (i.e. strip the '-' prefix if any), and sign position(s).
- """
-## dbg('MaskedEditMixin::_getAbsValue; candidate="%s"' % candidate, indent=1)
- if candidate is None: text = self._GetValue()
- else: text = candidate
- right_signpos = text.find(')')
-
- if self._isInt:
- if self._ctrl_constraints._alignRight and self._fields[0]._fillChar == ' ':
- signpos = text.find('-')
- if signpos == -1:
-## dbg('no - found; searching for (')
- signpos = text.find('(')
- elif signpos != -1:
-## dbg('- found at', signpos)
- pass
-
- if signpos == -1:
-## dbg('signpos still -1')
-## dbg('len(%s) (%d) < len(%s) (%d)?' % (text, len(text), self._mask, self._masklength), len(text) < self._masklength)
- if len(text) < self._masklength:
- text = ' ' + text
- if len(text) < self._masklength:
- text += ' '
- if len(text) > self._masklength and text[-1] in (')', ' '):
- text = text[:-1]
- else:
-## dbg('len(%s) (%d), len(%s) (%d)' % (text, len(text), self._mask, self._masklength))
-## dbg('len(%s) - (len(%s) + 1):' % (text, text.lstrip()) , len(text) - (len(text.lstrip()) + 1))
- signpos = len(text) - (len(text.lstrip()) + 1)
-
- if self._useParens and not text.strip():
- signpos -= 1 # empty value; use penultimate space
-## dbg('signpos:', signpos)
- if signpos >= 0:
- text = text[:signpos] + ' ' + text[signpos+1:]
-
- else:
- if self._signOk:
- signpos = 0
- text = self._template[0] + text[1:]
- else:
- signpos = -1
-
- if right_signpos != -1:
- if self._signOk:
- text = text[:right_signpos] + ' ' + text[right_signpos+1:]
- elif len(text) > self._masklength:
- text = text[:right_signpos] + text[right_signpos+1:]
- right_signpos = -1
-
-
- elif self._useParens and self._signOk:
- # figure out where it ought to go:
- right_signpos = self._masklength - 1 # initial guess
- if not self._ctrl_constraints._alignRight:
-## dbg('not right-aligned')
- if len(text.strip()) == 0:
- right_signpos = signpos + 1
- elif len(text.strip()) < self._masklength:
- right_signpos = len(text.rstrip())
-## dbg('right_signpos:', right_signpos)
-
- groupchar = self._fields[0]._groupChar
- try:
- value = long(text.replace(groupchar,'').replace('(','-').replace(')','').replace(' ', ''))
- except:
-## dbg('invalid number', indent=0)
- return None, signpos, right_signpos
-
- else: # float value
- try:
- groupchar = self._fields[0]._groupChar
- value = float(text.replace(groupchar,'').replace(self._decimalChar, '.').replace('(', '-').replace(')','').replace(' ', ''))
-## dbg('value:', value)
- except:
- value = None
-
- if value < 0 and value is not None:
- signpos = text.find('-')
- if signpos == -1:
- signpos = text.find('(')
-
- text = text[:signpos] + self._template[signpos] + text[signpos+1:]
- else:
- # look forwards up to the decimal point for the 1st non-digit
-## dbg('decimal pos:', self._decimalpos)
-## dbg('text: "%s"' % text)
- if self._signOk:
- signpos = self._decimalpos - (len(text[:self._decimalpos].lstrip()) + 1)
- # prevent checking for empty string - Tomo - Wed 14 Jan 2004 03:19:09 PM CET
- if len(text) >= signpos+1 and text[signpos+1] in ('-','('):
- signpos += 1
- else:
- signpos = -1
-## dbg('signpos:', signpos)
-
- if self._useParens:
- if self._signOk:
- right_signpos = self._masklength - 1
- text = text[:right_signpos] + ' '
- if text[signpos] == '(':
- text = text[:signpos] + ' ' + text[signpos+1:]
- else:
- right_signpos = text.find(')')
- if right_signpos != -1:
- text = text[:-1]
- right_signpos = -1
-
- if value is None:
-## dbg('invalid number')
- text = None
-
-## dbg('abstext = "%s"' % text, 'signpos:', signpos, 'right_signpos:', right_signpos)
-## dbg(indent=0)
- return text, signpos, right_signpos
-
-
- def _getSignedValue(self, candidate=None):
- """ Return a signed value by adding a "-" prefix if the value
- is set to negative, or a space if positive.
- """
-## dbg('MaskedEditMixin::_getSignedValue; candidate="%s"' % candidate, indent=1)
- if candidate is None: text = self._GetValue()
- else: text = candidate
-
-
- abstext, signpos, right_signpos = self._getAbsValue(text)
- if self._signOk:
- if abstext is None:
-## dbg(indent=0)
- return abstext, signpos, right_signpos
-
- if self._isNeg or text[signpos] in ('-', '('):
- if self._useParens:
- sign = '('
- else:
- sign = '-'
- else:
- sign = ' '
- if abstext[signpos] not in string.digits:
- text = abstext[:signpos] + sign + abstext[signpos+1:]
- else:
- # this can happen if value passed is too big; sign assumed to be
- # in position 0, but if already filled with a digit, prepend sign...
- text = sign + abstext
- if self._useParens and text.find('(') != -1:
- text = text[:right_signpos] + ')' + text[right_signpos+1:]
- else:
- text = abstext
-## dbg('signedtext = "%s"' % text, 'signpos:', signpos, 'right_signpos', right_signpos)
-## dbg(indent=0)
- return text, signpos, right_signpos
-
-
- def GetPlainValue(self, candidate=None):
- """ Returns control's value stripped of the template text.
- plainvalue = MaskedEditMixin.GetPlainValue()
- """
-## dbg('MaskedEditMixin::GetPlainValue; candidate="%s"' % candidate, indent=1)
-
- if candidate is None: text = self._GetValue()
- else: text = candidate
-
- if self.IsEmpty():
-## dbg('returned ""', indent=0)
- return ""
- else:
- plain = ""
- for idx in range( min(len(self._template), len(text)) ):
- if self._mask[idx] in maskchars:
- plain += text[idx]
-
- if self._isFloat or self._isInt:
-## dbg('plain so far: "%s"' % plain)
- plain = plain.replace('(', '-').replace(')', ' ')
-## dbg('plain after sign regularization: "%s"' % plain)
-
- if self._signOk and self._isNeg and plain.count('-') == 0:
- # must be in reserved position; add to "plain value"
- plain = '-' + plain.strip()
-
- if self._fields[0]._alignRight:
- lpad = plain.count(',')
- plain = ' ' * lpad + plain.replace(',','')
- else:
- plain = plain.replace(',','')
-## dbg('plain after pad and group:"%s"' % plain)
-
-## dbg('returned "%s"' % plain.rstrip(), indent=0)
- return plain.rstrip()
-
-
- def IsEmpty(self, value=None):
- """
- Returns True if control is equal to an empty value.
- (Empty means all editable positions in the template == fillChar.)
- """
- if value is None: value = self._GetValue()
- if value == self._template and not self._defaultValue:
-#### dbg("IsEmpty? 1 (value == self._template and not self._defaultValue)")
- return True # (all mask chars == fillChar by defn)
- elif value == self._template:
- empty = True
- for pos in range(len(self._template)):
-#### dbg('isMaskChar(%(pos)d)?' % locals(), self._isMaskChar(pos))
-#### dbg('value[%(pos)d] != self._fillChar?' %locals(), value[pos] != self._fillChar[pos])
- if self._isMaskChar(pos) and value[pos] not in (' ', self._fillChar[pos]):
- empty = False
-#### dbg("IsEmpty? %(empty)d (do all mask chars == fillChar?)" % locals())
- return empty
- else:
-#### dbg("IsEmpty? 0 (value doesn't match template)")
- return False
-
-
- def IsDefault(self, value=None):
- """
- Returns True if the value specified (or the value of the control if not specified)
- is equal to the default value.
- """
- if value is None: value = self._GetValue()
- return value == self._template
-
-
- def IsValid(self, value=None):
- """ Indicates whether the value specified (or the current value of the control
- if not specified) is considered valid."""
-#### dbg('MaskedEditMixin::IsValid("%s")' % value, indent=1)
- if value is None: value = self._GetValue()
- ret = self._CheckValid(value)
-#### dbg(indent=0)
- return ret
-
-
- def _eraseSelection(self, value=None, sel_start=None, sel_to=None):
- """ Used to blank the selection when inserting a new character. """
-## dbg("MaskedEditMixin::_eraseSelection", indent=1)
- if value is None: value = self._GetValue()
- if sel_start is None or sel_to is None:
- sel_start, sel_to = self._GetSelection() ## check for a range of selected text
-## dbg('value: "%s"' % value)
-## dbg("current sel_start, sel_to:", sel_start, sel_to)
-
- newvalue = list(value)
- for i in range(sel_start, sel_to):
- if self._signOk and newvalue[i] in ('-', '(', ')'):
-## dbg('found sign (%s) at' % newvalue[i], i)
-
- # balance parentheses:
- if newvalue[i] == '(':
- right_signpos = value.find(')')
- if right_signpos != -1:
- newvalue[right_signpos] = ' '
-
- elif newvalue[i] == ')':
- left_signpos = value.find('(')
- if left_signpos != -1:
- newvalue[left_signpos] = ' '
-
- newvalue[i] = ' '
-
- elif self._isMaskChar(i):
- field = self._FindField(i)
- if field._padZero:
- newvalue[i] = '0'
- else:
- newvalue[i] = self._template[i]
-
- value = string.join(newvalue,"")
-## dbg('new value: "%s"' % value)
-## dbg(indent=0)
- return value
-
-
- def _insertKey(self, char, pos, sel_start, sel_to, value, allowAutoSelect=False):
- """ Handles replacement of the character at the current insertion point."""
-## dbg('MaskedEditMixin::_insertKey', "\'" + char + "\'", pos, sel_start, sel_to, '"%s"' % value, indent=1)
-
- text = self._eraseSelection(value)
- field = self._FindField(pos)
- start, end = field._extent
- newtext = ""
- newpos = pos
-
- if pos != sel_start and sel_start == sel_to:
- # adjustpos must have moved the position; make selection match:
- sel_start = sel_to = pos
-
-## dbg('field._insertRight?', field._insertRight)
- if( field._insertRight # field allows right insert
- and ((sel_start, sel_to) == field._extent # and whole field selected
- or (sel_start == sel_to # or nothing selected
- and (sel_start == end # and cursor at right edge
- or (field._allowInsert # or field allows right-insert
- and sel_start < end # next to other char in field:
- and text[sel_start] != field._fillChar) ) ) ) ):
-## dbg('insertRight')
- fstr = text[start:end]
- erasable_chars = [field._fillChar, ' ']
-
- if field._padZero:
- erasable_chars.append('0')
-
- erased = ''
-#### dbg("fstr[0]:'%s'" % fstr[0])
-#### dbg('field_index:', field._index)
-#### dbg("fstr[0] in erasable_chars?", fstr[0] in erasable_chars)
-#### dbg("self._signOk and field._index == 0 and fstr[0] in ('-','(')?",
-## self._signOk and field._index == 0 and fstr[0] in ('-','('))
- if fstr[0] in erasable_chars or (self._signOk and field._index == 0 and fstr[0] in ('-','(')):
- erased = fstr[0]
-#### dbg('value: "%s"' % text)
-#### dbg('fstr: "%s"' % fstr)
-#### dbg("erased: '%s'" % erased)
- field_sel_start = sel_start - start
- field_sel_to = sel_to - start
-## dbg('left fstr: "%s"' % fstr[1:field_sel_start])
-## dbg('right fstr: "%s"' % fstr[field_sel_to:end])
- fstr = fstr[1:field_sel_start] + char + fstr[field_sel_to:end]
- if field._alignRight and sel_start != sel_to:
- field_len = end - start
-## pos += (field_len - len(fstr)) # move cursor right by deleted amount
- pos = sel_to
-## dbg('setting pos to:', pos)
- if field._padZero:
- fstr = '0' * (field_len - len(fstr)) + fstr
- else:
- fstr = fstr.rjust(field_len) # adjust the field accordingly
-## dbg('field str: "%s"' % fstr)
-
- newtext = text[:start] + fstr + text[end:]
- if erased in ('-', '(') and self._signOk:
- newtext = erased + newtext[1:]
-## dbg('newtext: "%s"' % newtext)
-
- if self._signOk and field._index == 0:
- start -= 1 # account for sign position
-
-#### dbg('field._moveOnFieldFull?', field._moveOnFieldFull)
-#### dbg('len(fstr.lstrip()) == end-start?', len(fstr.lstrip()) == end-start)
- if( field._moveOnFieldFull and pos == end
- and len(fstr.lstrip()) == end-start): # if field now full
- newpos = self._findNextEntry(end) # go to next field
- else:
- newpos = pos # else keep cursor at current position
-
- if not newtext:
-## dbg('not newtext')
- if newpos != pos:
-## dbg('newpos:', newpos)
- pass
- if self._signOk and self._useParens:
- old_right_signpos = text.find(')')
-
- if field._allowInsert and not field._insertRight and sel_to <= end and sel_start >= start:
- # inserting within a left-insert-capable field
- field_len = end - start
- before = text[start:sel_start]
- after = text[sel_to:end].strip()
-#### dbg("current field:'%s'" % text[start:end])
-#### dbg("before:'%s'" % before, "after:'%s'" % after)
- new_len = len(before) + len(after) + 1 # (for inserted char)
-#### dbg('new_len:', new_len)
-
- if new_len < field_len:
- retained = after + self._template[end-(field_len-new_len):end]
- elif new_len > end-start:
- retained = after[1:]
- else:
- retained = after
-
- left = text[0:start] + before
-#### dbg("left:'%s'" % left, "retained:'%s'" % retained)
- right = retained + text[end:]
- else:
- left = text[0:pos]
- right = text[pos+1:]
-
- newtext = left + char + right
-
- if self._signOk and self._useParens:
- # Balance parentheses:
- left_signpos = newtext.find('(')
-
- if left_signpos == -1: # erased '('; remove ')'
- right_signpos = newtext.find(')')
- if right_signpos != -1:
- newtext = newtext[:right_signpos] + ' ' + newtext[right_signpos+1:]
-
- elif old_right_signpos != -1:
- right_signpos = newtext.find(')')
-
- if right_signpos == -1: # just replaced right-paren
- if newtext[pos] == ' ': # we just erased '); erase '('
- newtext = newtext[:left_signpos] + ' ' + newtext[left_signpos+1:]
- else: # replaced with digit; move ') over
- if self._ctrl_constraints._alignRight or self._isFloat:
- newtext = newtext[:-1] + ')'
- else:
- rstripped_text = newtext.rstrip()
- right_signpos = len(rstripped_text)
-## dbg('old_right_signpos:', old_right_signpos, 'right signpos now:', right_signpos)
- newtext = newtext[:right_signpos] + ')' + newtext[right_signpos+1:]
-
- if( field._insertRight # if insert-right field (but we didn't start at right edge)
- and field._moveOnFieldFull # and should move cursor when full
- and len(newtext[start:end].strip()) == end-start): # and field now full
- newpos = self._findNextEntry(end) # go to next field
-## dbg('newpos = nextentry =', newpos)
- else:
-## dbg('pos:', pos, 'newpos:', pos+1)
- newpos = pos+1
-
-
- if allowAutoSelect:
- new_select_to = newpos # (default return values)
- match_field = None
- match_index = None
-
- if field._autoSelect:
- match_index, partial_match = self._autoComplete(1, # (always forward)
- field._compareChoices,
- newtext[start:end],
- compareNoCase=field._compareNoCase,
- current_index = field._autoCompleteIndex-1)
- if match_index is not None and partial_match:
- matched_str = newtext[start:end]
- newtext = newtext[:start] + field._choices[match_index] + newtext[end:]
- new_select_to = end
- match_field = field
- if field._insertRight:
- # adjust position to just after partial match in field
- newpos = end - (len(field._choices[match_index].strip()) - len(matched_str.strip()))
-
- elif self._ctrl_constraints._autoSelect:
- match_index, partial_match = self._autoComplete(
- 1, # (always forward)
- self._ctrl_constraints._compareChoices,
- newtext,
- self._ctrl_constraints._compareNoCase,
- current_index = self._ctrl_constraints._autoCompleteIndex - 1)
- if match_index is not None and partial_match:
- matched_str = newtext
- newtext = self._ctrl_constraints._choices[match_index]
- new_select_to = self._ctrl_constraints._extent[1]
- match_field = self._ctrl_constraints
- if self._ctrl_constraints._insertRight:
- # adjust position to just after partial match in control:
- newpos = self._masklength - (len(self._ctrl_constraints._choices[match_index].strip()) - len(matched_str.strip()))
-
-## dbg('newtext: "%s"' % newtext, 'newpos:', newpos, 'new_select_to:', new_select_to)
-## dbg(indent=0)
- return newtext, newpos, new_select_to, match_field, match_index
- else:
-## dbg('newtext: "%s"' % newtext, 'newpos:', newpos)
-## dbg(indent=0)
- return newtext, newpos
-
-
- def _OnFocus(self,event):
- """
- This event handler is currently necessary to work around new default
- behavior as of wxPython2.3.3;
- The TAB key auto selects the entire contents of the wxTextCtrl *after*
- the EVT_SET_FOCUS event occurs; therefore we can't query/adjust the selection
- *here*, because it hasn't happened yet. So to prevent this behavior, and
- preserve the correct selection when the focus event is not due to tab,
- we need to pull the following trick:
- """
-## dbg('MaskedEditMixin::_OnFocus')
- wx.CallAfter(self._fixSelection)
- event.Skip()
- self.Refresh()
-
-
- def _CheckValid(self, candidate=None):
- """
- This is the default validation checking routine; It verifies that the
- current value of the control is a "valid value," and has the side
- effect of coloring the control appropriately.
- """
-## dbg(suspend=1)
-## dbg('MaskedEditMixin::_CheckValid: candidate="%s"' % candidate, indent=1)
- oldValid = self._valid
- if candidate is None: value = self._GetValue()
- else: value = candidate
-## dbg('value: "%s"' % value)
- oldvalue = value
- valid = True # assume True
-
- if not self.IsDefault(value) and self._isDate: ## Date type validation
- valid = self._validateDate(value)
-## dbg("valid date?", valid)
-
- elif not self.IsDefault(value) and self._isTime:
- valid = self._validateTime(value)
-## dbg("valid time?", valid)
-
- elif not self.IsDefault(value) and (self._isInt or self._isFloat): ## Numeric type
- valid = self._validateNumeric(value)
-## dbg("valid Number?", valid)
-
- if valid: # and not self.IsDefault(value): ## generic validation accounts for IsDefault()
- ## valid so far; ensure also allowed by any list or regex provided:
- valid = self._validateGeneric(value)
-## dbg("valid value?", valid)
-
-## dbg('valid?', valid)
-
- if not candidate:
- self._valid = valid
- self._applyFormatting()
- if self._valid != oldValid:
-## dbg('validity changed: oldValid =',oldValid,'newvalid =', self._valid)
-## dbg('oldvalue: "%s"' % oldvalue, 'newvalue: "%s"' % self._GetValue())
- pass
-## dbg(indent=0, suspend=0)
- return valid
-
-
- def _validateGeneric(self, candidate=None):
- """ Validate the current value using the provided list or Regex filter (if any).
- """
- if candidate is None:
- text = self._GetValue()
- else:
- text = candidate
-
- valid = True # assume True
- for i in [-1] + self._field_indices: # process global constraints first:
- field = self._fields[i]
- start, end = field._extent
- slice = text[start:end]
- valid = field.IsValid(slice)
- if not valid:
- break
-
- return valid
-
-
- def _validateNumeric(self, candidate=None):
- """ Validate that the value is within the specified range (if specified.)"""
- if candidate is None: value = self._GetValue()
- else: value = candidate
- try:
- groupchar = self._fields[0]._groupChar
- if self._isFloat:
- number = float(value.replace(groupchar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')', ''))
- else:
- number = long( value.replace(groupchar, '').replace('(', '-').replace(')', ''))
- if value.strip():
- if self._fields[0]._alignRight:
- require_digit_at = self._fields[0]._extent[1]-1
- else:
- require_digit_at = self._fields[0]._extent[0]
-## dbg('require_digit_at:', require_digit_at)
-## dbg("value[rda]: '%s'" % value[require_digit_at])
- if value[require_digit_at] not in list(string.digits):
- valid = False
- return valid
- # else...
-## dbg('number:', number)
- if self._ctrl_constraints._hasRange:
- valid = self._ctrl_constraints._rangeLow <= number <= self._ctrl_constraints._rangeHigh
- else:
- valid = True
- groupcharpos = value.rfind(groupchar)
- if groupcharpos != -1: # group char present
-## dbg('groupchar found at', groupcharpos)
- if self._isFloat and groupcharpos > self._decimalpos:
- # 1st one found on right-hand side is past decimal point
-## dbg('groupchar in fraction; illegal')
- valid = False
- elif self._isFloat:
- integer = value[:self._decimalpos].strip()
- else:
- integer = value.strip()
-## dbg("integer:'%s'" % integer)
- if integer[0] in ('-', '('):
- integer = integer[1:]
- if integer[-1] == ')':
- integer = integer[:-1]
-
- parts = integer.split(groupchar)
-## dbg('parts:', parts)
- for i in range(len(parts)):
- if i == 0 and abs(int(parts[0])) > 999:
-## dbg('group 0 too long; illegal')
- valid = False
- break
- elif i > 0 and (len(parts[i]) != 3 or ' ' in parts[i]):
-## dbg('group %i (%s) not right size; illegal' % (i, parts[i]))
- valid = False
- break
- except ValueError:
-## dbg('value not a valid number')
- valid = False
- return valid
-
-
- def _validateDate(self, candidate=None):
- """ Validate the current date value using the provided Regex filter.
- Generally used for character types.BufferType
- """
-## dbg('MaskedEditMixin::_validateDate', indent=1)
- if candidate is None: value = self._GetValue()
- else: value = candidate
-## dbg('value = "%s"' % value)
- text = self._adjustDate(value, force4digit_year=True) ## Fix the date up before validating it
-## dbg('text =', text)
- valid = True # assume True until proven otherwise
-
- try:
- # replace fillChar in each field with space:
- datestr = text[0:self._dateExtent]
- for i in range(3):
- field = self._fields[i]
- start, end = field._extent
- fstr = datestr[start:end]
- fstr.replace(field._fillChar, ' ')
- datestr = datestr[:start] + fstr + datestr[end:]
-
- year, month, day = getDateParts( datestr, self._datestyle)
- year = int(year)
-## dbg('self._dateExtent:', self._dateExtent)
- if self._dateExtent == 11:
- month = charmonths_dict[month.lower()]
- else:
- month = int(month)
- day = int(day)
-## dbg('year, month, day:', year, month, day)
-
- except ValueError:
-## dbg('cannot convert string to integer parts')
- valid = False
- except KeyError:
-## dbg('cannot convert string to integer month')
- valid = False
-
- if valid:
- # use wxDateTime to unambiguously try to parse the date:
- # ### Note: because wxDateTime is *brain-dead* and expects months 0-11,
- # rather than 1-12, so handle accordingly:
- if month > 12:
- valid = False
- else:
- month -= 1
- try:
-## dbg("trying to create date from values day=%d, month=%d, year=%d" % (day,month,year))
- dateHandler = wx.DateTimeFromDMY(day,month,year)
-## dbg("succeeded")
- dateOk = True
- except:
-## dbg('cannot convert string to valid date')
- dateOk = False
- if not dateOk:
- valid = False
-
- if valid:
- # wxDateTime doesn't take kindly to leading/trailing spaces when parsing,
- # so we eliminate them here:
- timeStr = text[self._dateExtent+1:].strip() ## time portion of the string
- if timeStr:
-## dbg('timeStr: "%s"' % timeStr)
- try:
- checkTime = dateHandler.ParseTime(timeStr)
- valid = checkTime == len(timeStr)
- except:
- valid = False
- if not valid:
-## dbg('cannot convert string to valid time')
- pass
- if valid: dbg('valid date')
-## dbg(indent=0)
- return valid
-
-
- def _validateTime(self, candidate=None):
- """ Validate the current time value using the provided Regex filter.
- Generally used for character types.BufferType
- """
-## dbg('MaskedEditMixin::_validateTime', indent=1)
- # wxDateTime doesn't take kindly to leading/trailing spaces when parsing,
- # so we eliminate them here:
- if candidate is None: value = self._GetValue().strip()
- else: value = candidate.strip()
-## dbg('value = "%s"' % value)
- valid = True # assume True until proven otherwise
-
- dateHandler = wx.DateTime_Today()
- try:
- checkTime = dateHandler.ParseTime(value)
-## dbg('checkTime:', checkTime, 'len(value)', len(value))
- valid = checkTime == len(value)
- except:
- valid = False
-
- if not valid:
-## dbg('cannot convert string to valid time')
- pass
- if valid: dbg('valid time')
-## dbg(indent=0)
- return valid
-
-
- def _OnKillFocus(self,event):
- """ Handler for EVT_KILL_FOCUS event.
- """
-## dbg('MaskedEditMixin::_OnKillFocus', 'isDate=',self._isDate, indent=1)
- if self._mask and self._IsEditable():
- self._AdjustField(self._GetInsertionPoint())
- self._CheckValid() ## Call valid handler
-
- self._LostFocus() ## Provided for subclass use
- event.Skip()
-## dbg(indent=0)
-
-
- def _fixSelection(self):
- """
- This gets called after the TAB traversal selection is made, if the
- focus event was due to this, but before the EVT_LEFT_* events if
- the focus shift was due to a mouse event.
-
- The trouble is that, a priori, there's no explicit notification of
- why the focus event we received. However, the whole reason we need to
- do this is because the default behavior on TAB traveral in a wxTextCtrl is
- now to select the entire contents of the window, something we don't want.
- So we can *now* test the selection range, and if it's "the whole text"
- we can assume the cause, change the insertion point to the start of
- the control, and deselect.
- """
-## dbg('MaskedEditMixin::_fixSelection', indent=1)
- if not self._mask or not self._IsEditable():
-## dbg(indent=0)
- return
-
- sel_start, sel_to = self._GetSelection()
-## dbg('sel_start, sel_to:', sel_start, sel_to, 'self.IsEmpty()?', self.IsEmpty())
-
- if( sel_start == 0 and sel_to >= len( self._mask ) #(can be greater in numeric controls because of reserved space)
- and (not self._ctrl_constraints._autoSelect or self.IsEmpty() or self.IsDefault() ) ):
- # This isn't normally allowed, and so assume we got here by the new
- # "tab traversal" behavior, so we need to reset the selection
- # and insertion point:
-## dbg('entire text selected; resetting selection to start of control')
- self._goHome()
- field = self._FindField(self._GetInsertionPoint())
- edit_start, edit_end = field._extent
- if field._selectOnFieldEntry:
- self._SetInsertionPoint(edit_start)
- self._SetSelection(edit_start, edit_end)
-
- elif field._insertRight:
- self._SetInsertionPoint(edit_end)
- self._SetSelection(edit_end, edit_end)
-
- elif (self._isFloat or self._isInt):
-
- text, signpos, right_signpos = self._getAbsValue()
- if text is None or text == self._template:
- integer = self._fields[0]
- edit_start, edit_end = integer._extent
-
- if integer._selectOnFieldEntry:
-## dbg('select on field entry:')
- self._SetInsertionPoint(edit_start)
- self._SetSelection(edit_start, edit_end)
-
- elif integer._insertRight:
-## dbg('moving insertion point to end')
- self._SetInsertionPoint(edit_end)
- self._SetSelection(edit_end, edit_end)
- else:
-## dbg('numeric ctrl is empty; start at beginning after sign')
- self._SetInsertionPoint(signpos+1) ## Move past minus sign space if signed
- self._SetSelection(signpos+1, signpos+1)
-
- elif sel_start > self._goEnd(getPosOnly=True):
-## dbg('cursor beyond the end of the user input; go to end of it')
- self._goEnd()
- else:
-## dbg('sel_start, sel_to:', sel_start, sel_to, 'self._masklength:', self._masklength)
- pass
-## dbg(indent=0)
-
-
- def _Keypress(self,key):
- """ Method provided to override OnChar routine. Return False to force
- a skip of the 'normal' OnChar process. Called before class OnChar.
- """
- return True
-
-
- def _LostFocus(self):
- """ Method provided for subclasses. _LostFocus() is called after
- the class processes its EVT_KILL_FOCUS event code.
- """
- pass
-
-
- def _OnDoubleClick(self, event):
- """ selects field under cursor on dclick."""
- pos = self._GetInsertionPoint()
- field = self._FindField(pos)
- start, end = field._extent
- self._SetInsertionPoint(start)
- self._SetSelection(start, end)
-
-
- def _Change(self):
- """ Method provided for subclasses. Called by internal EVT_TEXT
- handler. Return False to override the class handler, True otherwise.
- """
- return True
-
-
- def _Cut(self):
- """
- Used to override the default Cut() method in base controls, instead
- copying the selection to the clipboard and then blanking the selection,
- leaving only the mask in the selected area behind.
- Note: _Cut (read "undercut" ;-) must be called from a Cut() override in the
- derived control because the mixin functions can't override a method of
- a sibling class.
- """
-## dbg("MaskedEditMixin::_Cut", indent=1)
- value = self._GetValue()
-## dbg('current value: "%s"' % value)
- sel_start, sel_to = self._GetSelection() ## check for a range of selected text
-## dbg('selected text: "%s"' % value[sel_start:sel_to].strip())
- do = wxTextDataObject()
- do.SetText(value[sel_start:sel_to].strip())
- wxTheClipboard.Open()
- wxTheClipboard.SetData(do)
- wxTheClipboard.Close()
-
- if sel_to - sel_start != 0:
- self._OnErase()
-## dbg(indent=0)
-
-
-# WS Note: overriding Copy is no longer necessary given that you
-# can no longer select beyond the last non-empty char in the control.
-#
-## def _Copy( self ):
-## """
-## Override the wxTextCtrl's .Copy function, with our own
-## that does validation. Need to strip trailing spaces.
-## """
-## sel_start, sel_to = self._GetSelection()
-## select_len = sel_to - sel_start
-## textval = wxTextCtrl._GetValue(self)
-##
-## do = wxTextDataObject()
-## do.SetText(textval[sel_start:sel_to].strip())
-## wxTheClipboard.Open()
-## wxTheClipboard.SetData(do)
-## wxTheClipboard.Close()
-
-
- def _getClipboardContents( self ):
- """ Subroutine for getting the current contents of the clipboard.
- """
- do = wxTextDataObject()
- wxTheClipboard.Open()
- success = wxTheClipboard.GetData(do)
- wxTheClipboard.Close()
-
- if not success:
- return None
- else:
- # Remove leading and trailing spaces before evaluating contents
- return do.GetText().strip()
-
-
- def _validatePaste(self, paste_text, sel_start, sel_to, raise_on_invalid=False):
- """
- Used by paste routine and field choice validation to see
- if a given slice of paste text is legal for the area in question:
- returns validity, replacement text, and extent of paste in
- template.
- """
-## dbg(suspend=1)
-## dbg('MaskedEditMixin::_validatePaste("%(paste_text)s", %(sel_start)d, %(sel_to)d), raise_on_invalid? %(raise_on_invalid)d' % locals(), indent=1)
- select_length = sel_to - sel_start
- maxlength = select_length
-## dbg('sel_to - sel_start:', maxlength)
- if maxlength == 0:
- maxlength = self._masklength - sel_start
- item = 'control'
- else:
- item = 'selection'
-## dbg('maxlength:', maxlength)
- length_considered = len(paste_text)
- if length_considered > maxlength:
-## dbg('paste text will not fit into the %s:' % item, indent=0)
- if raise_on_invalid:
-## dbg(indent=0, suspend=0)
- if item == 'control':
- raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
- else:
- raise ValueError('"%s" will not fit into the selection' % paste_text)
- else:
-## dbg(indent=0, suspend=0)
- return False, None, None
-
- text = self._template
-## dbg('length_considered:', length_considered)
-
- valid_paste = True
- replacement_text = ""
- replace_to = sel_start
- i = 0
- while valid_paste and i < length_considered and replace_to < self._masklength:
- if paste_text[i:] == self._template[replace_to:length_considered]:
- # remainder of paste matches template; skip char-by-char analysis
-## dbg('remainder paste_text[%d:] (%s) matches template[%d:%d]' % (i, paste_text[i:], replace_to, length_considered))
- replacement_text += paste_text[i:]
- replace_to = i = length_considered
- continue
- # else:
- char = paste_text[i]
- field = self._FindField(replace_to)
- if not field._compareNoCase:
- if field._forceupper: char = char.upper()
- elif field._forcelower: char = char.lower()
-
-## dbg('char:', "'"+char+"'", 'i =', i, 'replace_to =', replace_to)
-## dbg('self._isTemplateChar(%d)?' % replace_to, self._isTemplateChar(replace_to))
- if not self._isTemplateChar(replace_to) and self._isCharAllowed( char, replace_to, allowAutoSelect=False, ignoreInsertRight=True):
- replacement_text += char
-## dbg("not template(%(replace_to)d) and charAllowed('%(char)s',%(replace_to)d)" % locals())
-## dbg("replacement_text:", '"'+replacement_text+'"')
- i += 1
- replace_to += 1
- elif( char == self._template[replace_to]
- or (self._signOk and
- ( (i == 0 and (char == '-' or (self._useParens and char == '(')))
- or (i == self._masklength - 1 and self._useParens and char == ')') ) ) ):
- replacement_text += char
-## dbg("'%(char)s' == template(%(replace_to)d)" % locals())
-## dbg("replacement_text:", '"'+replacement_text+'"')
- i += 1
- replace_to += 1
- else:
- next_entry = self._findNextEntry(replace_to, adjustInsert=False)
- if next_entry == replace_to:
- valid_paste = False
- else:
- replacement_text += self._template[replace_to:next_entry]
-## dbg("skipping template; next_entry =", next_entry)
-## dbg("replacement_text:", '"'+replacement_text+'"')
- replace_to = next_entry # so next_entry will be considered on next loop
-
- if not valid_paste and raise_on_invalid:
-## dbg('raising exception', indent=0, suspend=0)
- raise ValueError('"%s" cannot be inserted into the control "%s"' % (paste_text, self.name))
-
- elif i < len(paste_text):
- valid_paste = False
- if raise_on_invalid:
-## dbg('raising exception', indent=0, suspend=0)
- raise ValueError('"%s" will not fit into the control "%s"' % (paste_text, self.name))
-
-## dbg('valid_paste?', valid_paste)
- if valid_paste:
-## dbg('replacement_text: "%s"' % replacement_text, 'replace to:', replace_to)
- pass
-## dbg(indent=0, suspend=0)
- return valid_paste, replacement_text, replace_to
-
-
- def _Paste( self, value=None, raise_on_invalid=False, just_return_value=False ):
- """
- Used to override the base control's .Paste() function,
- with our own that does validation.
- Note: _Paste must be called from a Paste() override in the
- derived control because the mixin functions can't override a
- method of a sibling class.
- """
-## dbg('MaskedEditMixin::_Paste (value = "%s")' % value, indent=1)
- if value is None:
- paste_text = self._getClipboardContents()
- else:
- paste_text = value
-
- if paste_text is not None:
-## dbg('paste text: "%s"' % paste_text)
- # (conversion will raise ValueError if paste isn't legal)
- sel_start, sel_to = self._GetSelection()
-## dbg('selection:', (sel_start, sel_to))
-
- # special case: handle allowInsert fields properly
- field = self._FindField(sel_start)
- edit_start, edit_end = field._extent
- new_pos = None
- if field._allowInsert and sel_to <= edit_end and sel_start + len(paste_text) < edit_end:
- new_pos = sel_start + len(paste_text) # store for subsequent positioning
- paste_text = paste_text + self._GetValue()[sel_to:edit_end].rstrip()
-## dbg('paste within insertable field; adjusted paste_text: "%s"' % paste_text, 'end:', edit_end)
- sel_to = sel_start + len(paste_text)
-
- # Another special case: paste won't fit, but it's a right-insert field where entire
- # non-empty value is selected, and there's room if the selection is expanded leftward:
- if( len(paste_text) > sel_to - sel_start
- and field._insertRight
- and sel_start > edit_start
- and sel_to >= edit_end
- and not self._GetValue()[edit_start:sel_start].strip() ):
- # text won't fit within selection, but left of selection is empty;
- # check to see if we can expand selection to accomodate the value:
- empty_space = sel_start - edit_start
- amount_needed = len(paste_text) - (sel_to - sel_start)
- if amount_needed <= empty_space:
- sel_start -= amount_needed
-## dbg('expanded selection to:', (sel_start, sel_to))
-
-
- # another special case: deal with signed values properly:
- if self._signOk:
- signedvalue, signpos, right_signpos = self._getSignedValue()
- paste_signpos = paste_text.find('-')
- if paste_signpos == -1:
- paste_signpos = paste_text.find('(')
-
- # if paste text will result in signed value:
-#### dbg('paste_signpos != -1?', paste_signpos != -1)
-#### dbg('sel_start:', sel_start, 'signpos:', signpos)
-#### dbg('field._insertRight?', field._insertRight)
-#### dbg('sel_start - len(paste_text) >= signpos?', sel_start - len(paste_text) <= signpos)
- if paste_signpos != -1 and (sel_start <= signpos
- or (field._insertRight and sel_start - len(paste_text) <= signpos)):
- signed = True
- else:
- signed = False
- # remove "sign" from paste text, so we can auto-adjust for sign type after paste:
- paste_text = paste_text.replace('-', ' ').replace('(',' ').replace(')','')
-## dbg('unsigned paste text: "%s"' % paste_text)
- else:
- signed = False
-
- # another special case: deal with insert-right fields when selection is empty and
- # cursor is at end of field:
-#### dbg('field._insertRight?', field._insertRight)
-#### dbg('sel_start == edit_end?', sel_start == edit_end)
-#### dbg('sel_start', sel_start, 'sel_to', sel_to)
- if field._insertRight and sel_start == edit_end and sel_start == sel_to:
- sel_start -= len(paste_text)
- if sel_start < 0:
- sel_start = 0
-## dbg('adjusted selection:', (sel_start, sel_to))
-
- try:
- valid_paste, replacement_text, replace_to = self._validatePaste(paste_text, sel_start, sel_to, raise_on_invalid)
- except:
-## dbg('exception thrown', indent=0)
- raise
-
- if not valid_paste:
-## dbg('paste text not legal for the selection or portion of the control following the cursor;')
- if not wx.Validator_IsSilent():
- wx.Bell()
-## dbg(indent=0)
- return False
- # else...
- text = self._eraseSelection()
-
- new_text = text[:sel_start] + replacement_text + text[replace_to:]
- if new_text:
- new_text = string.ljust(new_text,self._masklength)
- if signed:
- new_text, signpos, right_signpos = self._getSignedValue(candidate=new_text)
- if new_text:
- if self._useParens:
- new_text = new_text[:signpos] + '(' + new_text[signpos+1:right_signpos] + ')' + new_text[right_signpos+1:]
- else:
- new_text = new_text[:signpos] + '-' + new_text[signpos+1:]
- if not self._isNeg:
- self._isNeg = 1
-
-## dbg("new_text:", '"'+new_text+'"')
-
- if not just_return_value:
- if new_text != self._GetValue():
- self.modified = True
- if new_text == '':
- self.ClearValue()
- else:
- wx.CallAfter(self._SetValue, new_text)
- if new_pos is None:
- new_pos = sel_start + len(replacement_text)
- wx.CallAfter(self._SetInsertionPoint, new_pos)
- else:
-## dbg(indent=0)
- return new_text
- elif just_return_value:
-## dbg(indent=0)
- return self._GetValue()
-## dbg(indent=0)
-
- def _Undo(self):
- """ Provides an Undo() method in base controls. """
-## dbg("MaskedEditMixin::_Undo", indent=1)
- value = self._GetValue()
- prev = self._prevValue
-## dbg('current value: "%s"' % value)
-## dbg('previous value: "%s"' % prev)
- if prev is None:
-## dbg('no previous value', indent=0)
- return
-
- elif value != prev:
- # Determine what to select: (relies on fixed-length strings)
- # (This is a lot harder than it would first appear, because
- # of mask chars that stay fixed, and so break up the "diff"...)
-
- # Determine where they start to differ:
- i = 0
- length = len(value) # (both are same length in masked control)
-
- while( value[:i] == prev[:i] ):
- i += 1
- sel_start = i - 1
-
-
- # handle signed values carefully, so undo from signed to unsigned or vice-versa
- # works properly:
- if self._signOk:
- text, signpos, right_signpos = self._getSignedValue(candidate=prev)
- if self._useParens:
- if prev[signpos] == '(' and prev[right_signpos] == ')':
- self._isNeg = True
- else:
- self._isNeg = False
- # eliminate source of "far-end" undo difference if using balanced parens:
- value = value.replace(')', ' ')
- prev = prev.replace(')', ' ')
- elif prev[signpos] == '-':
- self._isNeg = True
- else:
- self._isNeg = False
-
- # Determine where they stop differing in "undo" result:
- sm = difflib.SequenceMatcher(None, a=value, b=prev)
- i, j, k = sm.find_longest_match(sel_start, length, sel_start, length)
-## dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] )
-
- if k == 0: # no match found; select to end
- sel_to = length
- else:
- code_5tuples = sm.get_opcodes()
- for op, i1, i2, j1, j2 in code_5tuples:
-## dbg("%7s value[%d:%d] (%s) prev[%d:%d] (%s)" % (op, i1, i2, value[i1:i2], j1, j2, prev[j1:j2]))
- pass
-
- diff_found = False
- # look backward through operations needed to produce "previous" value;
- # first change wins:
- for next_op in range(len(code_5tuples)-1, -1, -1):
- op, i1, i2, j1, j2 = code_5tuples[next_op]
-## dbg('value[i1:i2]: "%s"' % value[i1:i2], 'template[i1:i2] "%s"' % self._template[i1:i2])
- if op == 'insert' and prev[j1:j2] != self._template[j1:j2]:
-## dbg('insert found: selection =>', (j1, j2))
- sel_start = j1
- sel_to = j2
- diff_found = True
- break
- elif op == 'delete' and value[i1:i2] != self._template[i1:i2]:
- field = self._FindField(i2)
- edit_start, edit_end = field._extent
- if field._insertRight and i2 == edit_end:
- sel_start = i2
- sel_to = i2
- else:
- sel_start = i1
- sel_to = j1
-## dbg('delete found: selection =>', (sel_start, sel_to))
- diff_found = True
- break
- elif op == 'replace':
-## dbg('replace found: selection =>', (j1, j2))
- sel_start = j1
- sel_to = j2
- diff_found = True
- break
-
-
- if diff_found:
- # now go forwards, looking for earlier changes:
- for next_op in range(len(code_5tuples)):
- op, i1, i2, j1, j2 = code_5tuples[next_op]
- field = self._FindField(i1)
- if op == 'equal':
- continue
- elif op == 'replace':
-## dbg('setting sel_start to', i1)
- sel_start = i1
- break
- elif op == 'insert' and not value[i1:i2]:
-## dbg('forward %s found' % op)
- if prev[j1:j2].strip():
-## dbg('item to insert non-empty; setting sel_start to', j1)
- sel_start = j1
- break
- elif not field._insertRight:
-## dbg('setting sel_start to inserted space:', j1)
- sel_start = j1
- break
- elif op == 'delete' and field._insertRight and not value[i1:i2].lstrip():
- continue
- else:
- # we've got what we need
- break
-
-
- if not diff_found:
-## dbg('no insert,delete or replace found (!)')
- # do "left-insert"-centric processing of difference based on l.c.s.:
- if i == j and j != sel_start: # match starts after start of selection
- sel_to = sel_start + (j-sel_start) # select to start of match
- else:
- sel_to = j # (change ends at j)
-
-
- # There are several situations where the calculated difference is
- # not what we want to select. If changing sign, or just adding
- # group characters, we really don't want to highlight the characters
- # changed, but instead leave the cursor where it is.
- # Also, there a situations in which the difference can be ambiguous;
- # Consider:
- #
- # current value: 11234
- # previous value: 1111234
- #
- # Where did the cursor actually lie and which 1s were selected on the delete
- # operation?
- #
- # Also, difflib can "get it wrong;" Consider:
- #
- # current value: " 128.66"
- # previous value: " 121.86"
- #
- # difflib produces the following opcodes, which are sub-optimal:
- # equal value[0:9] ( 12) prev[0:9] ( 12)
- # insert value[9:9] () prev[9:11] (1.)
- # equal value[9:10] (8) prev[11:12] (8)
- # delete value[10:11] (.) prev[12:12] ()
- # equal value[11:12] (6) prev[12:13] (6)
- # delete value[12:13] (6) prev[13:13] ()
- #
- # This should have been:
- # equal value[0:9] ( 12) prev[0:9] ( 12)
- # replace value[9:11] (8.6) prev[9:11] (1.8)
- # equal value[12:13] (6) prev[12:13] (6)
- #
- # But it didn't figure this out!
- #
- # To get all this right, we use the previous selection recorded to help us...
-
- if (sel_start, sel_to) != self._prevSelection:
-## dbg('calculated selection', (sel_start, sel_to), "doesn't match previous", self._prevSelection)
-
- prev_sel_start, prev_sel_to = self._prevSelection
- field = self._FindField(sel_start)
-
- if self._signOk and (self._prevValue[sel_start] in ('-', '(', ')')
- or self._curValue[sel_start] in ('-', '(', ')')):
- # change of sign; leave cursor alone...
- sel_start, sel_to = self._prevSelection
-
- elif field._groupdigits and (self._curValue[sel_start:sel_to] == field._groupChar
- or self._prevValue[sel_start:sel_to] == field._groupChar):
- # do not highlight grouping changes
- sel_start, sel_to = self._prevSelection
-
- else:
- calc_select_len = sel_to - sel_start
- prev_select_len = prev_sel_to - prev_sel_start
-
-## dbg('sel_start == prev_sel_start', sel_start == prev_sel_start)
-## dbg('sel_to > prev_sel_to', sel_to > prev_sel_to)
-
- if prev_select_len >= calc_select_len:
- # old selection was bigger; trust it:
- sel_start, sel_to = self._prevSelection
-
- elif( sel_to > prev_sel_to # calculated select past last selection
- and prev_sel_to < len(self._template) # and prev_sel_to not at end of control
- and sel_to == len(self._template) ): # and calculated selection goes to end of control
-
- i, j, k = sm.find_longest_match(prev_sel_to, length, prev_sel_to, length)
-## dbg('i,j,k = ', (i,j,k), 'value[i:i+k] = "%s"' % value[i:i+k], 'prev[j:j+k] = "%s"' % prev[j:j+k] )
- if k > 0:
- # difflib must not have optimized opcodes properly;
- sel_to = j
-
- else:
- # look for possible ambiguous diff:
-
- # if last change resulted in no selection, test from resulting cursor position:
- if prev_sel_start == prev_sel_to:
- calc_select_len = sel_to - sel_start
- field = self._FindField(prev_sel_start)
-
- # determine which way to search from last cursor position for ambiguous change:
- if field._insertRight:
- test_sel_start = prev_sel_start
- test_sel_to = prev_sel_start + calc_select_len
- else:
- test_sel_start = prev_sel_start - calc_select_len
- test_sel_to = prev_sel_start
- else:
- test_sel_start, test_sel_to = prev_sel_start, prev_sel_to
-
-## dbg('test selection:', (test_sel_start, test_sel_to))
-## dbg('calc change: "%s"' % self._prevValue[sel_start:sel_to])
-## dbg('test change: "%s"' % self._prevValue[test_sel_start:test_sel_to])
-
- # if calculated selection spans characters, and same characters
- # "before" the previous insertion point are present there as well,
- # select the ones related to the last known selection instead.
- if( sel_start != sel_to
- and test_sel_to < len(self._template)
- and self._prevValue[test_sel_start:test_sel_to] == self._prevValue[sel_start:sel_to] ):
-
- sel_start, sel_to = test_sel_start, test_sel_to
-
-## dbg('sel_start, sel_to:', sel_start, sel_to)
-## dbg('previous value: "%s"' % self._prevValue)
- self._SetValue(self._prevValue)
- self._SetInsertionPoint(sel_start)
- self._SetSelection(sel_start, sel_to)
- else:
-## dbg('no difference between previous value')
- pass
-## dbg(indent=0)
-
-
- def _OnClear(self, event):
- """ Provides an action for context menu delete operation """
- self.ClearValue()
-
-
- def _OnContextMenu(self, event):
-## dbg('MaskedEditMixin::OnContextMenu()', indent=1)
- menu = wxMenu()
- menu.Append(wxID_UNDO, "Undo", "")
- menu.AppendSeparator()
- menu.Append(wxID_CUT, "Cut", "")
- menu.Append(wxID_COPY, "Copy", "")
- menu.Append(wxID_PASTE, "Paste", "")
- menu.Append(wxID_CLEAR, "Delete", "")
- menu.AppendSeparator()
- menu.Append(wxID_SELECTALL, "Select All", "")
-
- EVT_MENU(menu, wxID_UNDO, self._OnCtrl_Z)
- EVT_MENU(menu, wxID_CUT, self._OnCtrl_X)
- EVT_MENU(menu, wxID_COPY, self._OnCtrl_C)
- EVT_MENU(menu, wxID_PASTE, self._OnCtrl_V)
- EVT_MENU(menu, wxID_CLEAR, self._OnClear)
- EVT_MENU(menu, wxID_SELECTALL, self._OnCtrl_A)
-
- # ## WSS: The base control apparently handles
- # enable/disable of wID_CUT, wxID_COPY, wxID_PASTE
- # and wxID_CLEAR menu items even if the menu is one
- # we created. However, it doesn't do undo properly,
- # so we're keeping track of previous values ourselves.
- # Therefore, we have to override the default update for
- # that item on the menu:
- EVT_UPDATE_UI(self, wxID_UNDO, self._UndoUpdateUI)
- self._contextMenu = menu
-
- self.PopupMenu(menu, event.GetPosition())
- menu.Destroy()
- self._contextMenu = None
-## dbg(indent=0)
-
- def _UndoUpdateUI(self, event):
- if self._prevValue is None or self._prevValue == self._curValue:
- self._contextMenu.Enable(wxID_UNDO, False)
- else:
- self._contextMenu.Enable(wxID_UNDO, True)
-
-
- def _OnCtrlParametersChanged(self):
- """
- Overridable function to allow derived classes to take action as a
- result of parameter changes prior to possibly changing the value
- of the control.
- """
- pass
-
- ## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-# ## TRICKY BIT: to avoid a ton of boiler-plate, and to
-# ## automate the getter/setter generation for each valid
-# ## control parameter so we never forget to add the
-# ## functions when adding parameters, this loop
-# ## programmatically adds them to the class:
-# ## (This makes it easier for Designers like Boa to
-# ## deal with masked controls.)
-#
-# ## To further complicate matters, this is done with an
-# ## extra level of inheritance, so that "general" classes like
-# ## MaskedTextCtrl can have all possible attributes,
-# ## while derived classes, like TimeCtrl and MaskedNumCtrl
-# ## can prevent exposure of those optional attributes of their base
-# ## class that do not make sense for their derivation. Therefore,
-# ## we define
-# ## BaseMaskedTextCtrl(TextCtrl, MaskedEditMixin)
-# ## and
-# ## MaskedTextCtrl(BaseMaskedTextCtrl, MaskedEditAccessorsMixin).
-# ##
-# ## This allows us to then derive:
-# ## MaskedNumCtrl( BaseMaskedTextCtrl )
-# ##
-# ## and not have to expose all the same accessor functions for the
-# ## derived control when they don't all make sense for it.
-# ##
-class MaskedEditAccessorsMixin:
-
- # Define the default set of attributes exposed by the most generic masked controls:
- exposed_basectrl_params = MaskedEditMixin.valid_ctrl_params.keys() + Field.valid_params.keys()
- exposed_basectrl_params.remove('index')
- exposed_basectrl_params.remove('extent')
- exposed_basectrl_params.remove('foregroundColour') # (base class already has this)
-
- 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 BaseMaskedTextCtrl( wx.TextCtrl, MaskedEditMixin ):
- """
- This is the primary derivation from MaskedEditMixin. It provides
- a general masked text control that can be configured with different
- masks. It's actually a "base masked textCtrl", so that the
- MaskedTextCtrl class can be derived from it, and add those
- accessor functions to it that are appropriate to the general class,
- whilst other classes can derive from BaseMaskedTextCtrl, and
- only define those accessor functions that are appropriate for
- those derivations.
- """
-
- def __init__( self, parent, id=-1, value = '',
- pos = wx.DefaultPosition,
- size = wx.DefaultSize,
- style = wx.TE_PROCESS_TAB,
- validator=wx.DefaultValidator, ## placeholder provided for data-transfer logic
- name = 'maskedTextCtrl',
- setupEventHandling = True, ## setup event handling by default
- **kwargs):
-
- wx.TextCtrl.__init__(self, parent, id, value='',
- pos=pos, size = size,
- style=style, validator=validator,
- name=name)
-
- self.controlInitialized = True
- MaskedEditMixin.__init__( self, name, **kwargs )
-
- self._SetInitialValue(value)
-
- if setupEventHandling:
- ## Setup event handlers
- 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_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick
- self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu
- self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## capture control events not normally seen, eg ctrl-tab.
- self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress
- self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep
- ## track of previous value for undo
-
-
- def __repr__(self):
- return "<BaseMaskedTextCtrl: %s>" % self.GetValue()
-
-
- def _GetSelection(self):
- """
- Allow mixin to get the text selection of this control.
- REQUIRED by any class derived from MaskedEditMixin.
- """
- return self.GetSelection()
-
- def _SetSelection(self, sel_start, sel_to):
- """
- Allow mixin to set the text selection of this control.
- REQUIRED by any class derived from MaskedEditMixin.
- """
-#### dbg("MaskedTextCtrl::_SetSelection(%(sel_start)d, %(sel_to)d)" % locals())
- return self.SetSelection( sel_start, sel_to )
-
- def SetSelection(self, sel_start, sel_to):
- """
- This is just for debugging...
- """
-## dbg("MaskedTextCtrl::SetSelection(%(sel_start)d, %(sel_to)d)" % locals())
- wx.TextCtrl.SetSelection(self, sel_start, sel_to)
-
-
- def _GetInsertionPoint(self):
- return self.GetInsertionPoint()
-
- def _SetInsertionPoint(self, pos):
-#### dbg("MaskedTextCtrl::_SetInsertionPoint(%(pos)d)" % locals())
- self.SetInsertionPoint(pos)
-
- def SetInsertionPoint(self, pos):
- """
- This is just for debugging...
- """
-## dbg("MaskedTextCtrl::SetInsertionPoint(%(pos)d)" % locals())
- wx.TextCtrl.SetInsertionPoint(self, pos)
-
-
- def _GetValue(self):
- """
- Allow mixin to get the raw value of the control with this function.
- REQUIRED by any class derived from MaskedEditMixin.
- """
- return self.GetValue()
-
- def _SetValue(self, value):
- """
- Allow mixin to set the raw value of the control with this function.
- REQUIRED by any class derived from MaskedEditMixin.
- """
-## dbg('MaskedTextCtrl::_SetValue("%(value)s")' % locals(), indent=1)
- # Record current selection and insertion point, for undo
- self._prevSelection = self._GetSelection()
- self._prevInsertionPoint = self._GetInsertionPoint()
- wx.TextCtrl.SetValue(self, value)
-## dbg(indent=0)
-
- def SetValue(self, value):
- """
- This function redefines the externally accessible .SetValue to be
- a smart "paste" of the text in question, so as not to corrupt the
- masked control. NOTE: this must be done in the class derived
- from the base wx control.
- """
-## dbg('MaskedTextCtrl::SetValue = "%s"' % value, indent=1)
-
- if not self._mask:
- wx.TextCtrl.SetValue(self, value) # revert to base control behavior
- return
-
- # empty previous contents, replacing entire value:
- self._SetInsertionPoint(0)
- self._SetSelection(0, self._masklength)
- if self._signOk and self._useParens:
- signpos = value.find('-')
- if signpos != -1:
- value = value[:signpos] + '(' + value[signpos+1:].strip() + ')'
- elif value.find(')') == -1 and len(value) < self._masklength:
- value += ' ' # add place holder for reserved space for right paren
-
- if( len(value) < self._masklength # value shorter than control
- and (self._isFloat or self._isInt) # and it's a numeric control
- and self._ctrl_constraints._alignRight ): # and it's a right-aligned control
-
-## dbg('len(value)', len(value), ' < self._masklength', self._masklength)
- # try to intelligently "pad out" the value to the right size:
- value = self._template[0:self._masklength - len(value)] + value
- if self._isFloat and value.find('.') == -1:
- value = value[1:]
-## dbg('padded value = "%s"' % value)
-
- # make SetValue behave the same as if you had typed the value in:
- try:
- value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
- if self._isFloat:
- self._isNeg = False # (clear current assumptions)
- value = self._adjustFloat(value)
- elif self._isInt:
- self._isNeg = False # (clear current assumptions)
- value = self._adjustInt(value)
- elif self._isDate and not self.IsValid(value) and self._4digityear:
- value = self._adjustDate(value, fixcentury=True)
- except ValueError:
- # If date, year might be 2 digits vs. 4; try adjusting it:
- if self._isDate and self._4digityear:
- dateparts = value.split(' ')
- dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True)
- value = string.join(dateparts, ' ')
-## dbg('adjusted value: "%s"' % value)
- value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
- else:
-## dbg('exception thrown', indent=0)
- raise
-
- self._SetValue(value) # note: to preserve similar capability, .SetValue()
- # does not change IsModified()
-#### dbg('queuing insertion after .SetValue', self._masklength)
- wx.CallAfter(self._SetInsertionPoint, self._masklength)
- wx.CallAfter(self._SetSelection, self._masklength, self._masklength)
-## dbg(indent=0)
-
-
- def Clear(self):
- """ Blanks the current control value by replacing it with the default value."""
-## dbg("MaskedTextCtrl::Clear - value reset to default value (template)")
- if self._mask:
- self.ClearValue()
- else:
- wx.TextCtrl.Clear(self) # else revert to base control behavior
-
-
- def _Refresh(self):
- """
- Allow mixin to refresh the base control with this function.
- REQUIRED by any class derived from MaskedEditMixin.
- """
-## dbg('MaskedTextCtrl::_Refresh', indent=1)
- wx.TextCtrl.Refresh(self)
-## dbg(indent=0)
-
-
- def Refresh(self):
- """
- This function redefines the externally accessible .Refresh() to
- validate the contents of the masked control as it refreshes.
- NOTE: this must be done in the class derived from the base wx control.
- """
-## dbg('MaskedTextCtrl::Refresh', indent=1)
- self._CheckValid()
- self._Refresh()
-## dbg(indent=0)
-
-
- def _IsEditable(self):
- """
- Allow mixin to determine if the base control is editable with this function.
- REQUIRED by any class derived from MaskedEditMixin.
- """
- return wx.TextCtrl.IsEditable(self)
-
-
- def Cut(self):
- """
- This function redefines the externally accessible .Cut to be
- a smart "erase" of the text in question, so as not to corrupt the
- masked control. NOTE: this must be done in the class derived
- from the base wx control.
- """
- if self._mask:
- self._Cut() # call the mixin's Cut method
- else:
- wx.TextCtrl.Cut(self) # else revert to base control behavior
-
-
- def Paste(self):
- """
- This function redefines the externally accessible .Paste to be
- a smart "paste" of the text in question, so as not to corrupt the
- masked control. NOTE: this must be done in the class derived
- from the base wx control.
- """
- if self._mask:
- self._Paste() # call the mixin's Paste method
- else:
- wx.TextCtrl.Paste(self, value) # else revert to base control behavior
-
-
- def Undo(self):
- """
- This function defines the undo operation for the control. (The default
- undo is 1-deep.)
- """
- if self._mask:
- self._Undo()
- else:
- wx.TextCtrl.Undo(self) # else revert to base control behavior
-
-
- def IsModified(self):
- """
- This function overrides the raw wxTextCtrl method, because the
- masked edit mixin uses SetValue to change the value, which doesn't
- modify the state of this attribute. So, we keep track on each
- keystroke to see if the value changes, and if so, it's been
- modified.
- """
- return wx.TextCtrl.IsModified(self) or self.modified
-
-
- def _CalcSize(self, size=None):
- """
- Calculate automatic size if allowed; use base mixin function.
- """
- return self._calcSize(size)
-
-
-class MaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ):
- """
- This extra level of inheritance allows us to add the generic set of
- masked edit parameters only to this class while allowing other
- classes to derive from the "base" masked text control, and provide
- a smaller set of valid accessor functions.
- """
- pass
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-## Because calling SetSelection programmatically does not fire EVT_COMBOBOX
-## events, we have to do it ourselves when we auto-complete.
-class MaskedComboBoxSelectEvent(wx.PyCommandEvent):
- def __init__(self, id, selection = 0, object=None):
- wx.PyCommandEvent.__init__(self, wx.wxEVT_COMMAND_COMBOBOX_SELECTED, id)
-
- self.__selection = selection
- self.SetEventObject(object)
-
- def GetSelection(self):
- """Retrieve the value of the control at the time
- this event was generated."""
- return self.__selection
-
-
-class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
- """
- This masked edit control adds the ability to use a masked input
- on a combobox, and do auto-complete of such values.
- """
- def __init__( self, parent, id=-1, value = '',
- pos = wx.DefaultPosition,
- size = wx.DefaultSize,
- choices = [],
- style = wx.CB_DROPDOWN,
- validator = wx.DefaultValidator,
- name = "maskedComboBox",
- setupEventHandling = True, ## setup event handling by default):
- **kwargs):
-
-
- # This is necessary, because wxComboBox currently provides no
- # method for determining later if this was specified in the
- # constructor for the control...
- self.__readonly = style & wx.CB_READONLY == wx.CB_READONLY
-
- kwargs['choices'] = choices ## set up maskededit to work with choice list too
-
- ## Since combobox completion is case-insensitive, always validate same way
- if not kwargs.has_key('compareNoCase'):
- kwargs['compareNoCase'] = True
-
- MaskedEditMixin.__init__( self, name, **kwargs )
-
- self._choices = self._ctrl_constraints._choices
-## dbg('self._choices:', self._choices)
-
- if self._ctrl_constraints._alignRight:
- choices = [choice.rjust(self._masklength) for choice in choices]
- else:
- choices = [choice.ljust(self._masklength) for choice in choices]
-
- wx.ComboBox.__init__(self, parent, id, value='',
- pos=pos, size = size,
- choices=choices, style=style|wx.WANTS_CHARS,
- validator=validator,
- name=name)
-
- self.controlInitialized = True
-
- # Set control font - fixed width by default
- self._setFont()
-
- if self._autofit:
- self.SetClientSize(self._CalcSize())
-
- if value:
- # ensure value is width of the mask of the control:
- if self._ctrl_constraints._alignRight:
- value = value.rjust(self._masklength)
- else:
- value = value.ljust(self._masklength)
-
- if self.__readonly:
- self.SetStringSelection(value)
- else:
- self._SetInitialValue(value)
-
-
- self._SetKeycodeHandler(wx.WXK_UP, self.OnSelectChoice)
- self._SetKeycodeHandler(wx.WXK_DOWN, self.OnSelectChoice)
-
- if setupEventHandling:
- ## Setup event handlers
- 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_DCLICK, self._OnDoubleClick) ## select field under cursor on dclick
- self.Bind(wx.EVT_RIGHT_UP, self._OnContextMenu ) ## bring up an appropriate context menu
- self.Bind(wx.EVT_CHAR, self._OnChar ) ## handle each keypress
- self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown ) ## for special processing of up/down keys
- self.Bind(wx.EVT_KEY_DOWN, self._OnKeyDown ) ## for processing the rest of the control keys
- ## (next in evt chain)
- self.Bind(wx.EVT_TEXT, self._OnTextChange ) ## color control appropriately & keep
- ## track of previous value for undo
-
-
-
- def __repr__(self):
- return "<MaskedComboBox: %s>" % self.GetValue()
-
-
- def _CalcSize(self, size=None):
- """
- Calculate automatic size if allowed; augment base mixin function
- to account for the selector button.
- """
- size = self._calcSize(size)
- return (size[0]+20, size[1])
-
-
- def _GetSelection(self):
- """
- Allow mixin to get the text selection of this control.
- REQUIRED by any class derived from MaskedEditMixin.
- """
- return self.GetMark()
-
- def _SetSelection(self, sel_start, sel_to):
- """
- Allow mixin to set the text selection of this control.
- REQUIRED by any class derived from MaskedEditMixin.
- """
- return self.SetMark( sel_start, sel_to )
-
-
- def _GetInsertionPoint(self):
- return self.GetInsertionPoint()
-
- def _SetInsertionPoint(self, pos):
- self.SetInsertionPoint(pos)
-
-
- def _GetValue(self):
- """
- Allow mixin to get the raw value of the control with this function.
- REQUIRED by any class derived from MaskedEditMixin.
- """
- return self.GetValue()
-
- def _SetValue(self, value):
- """
- Allow mixin to set the raw value of the control with this function.
- REQUIRED by any class derived from MaskedEditMixin.
- """
- # For wxComboBox, ensure that values are properly padded so that
- # if varying length choices are supplied, they always show up
- # in the window properly, and will be the appropriate length
- # to match the mask:
- if self._ctrl_constraints._alignRight:
- value = value.rjust(self._masklength)
- else:
- value = value.ljust(self._masklength)
-
- # Record current selection and insertion point, for undo
- self._prevSelection = self._GetSelection()
- self._prevInsertionPoint = self._GetInsertionPoint()
- wx.ComboBox.SetValue(self, value)
- # text change events don't always fire, so we check validity here
- # to make certain formatting is applied:
- self._CheckValid()
-
- def SetValue(self, value):
- """
- This function redefines the externally accessible .SetValue to be
- a smart "paste" of the text in question, so as not to corrupt the
- masked control. NOTE: this must be done in the class derived
- from the base wx control.
- """
- if not self._mask:
- wx.ComboBox.SetValue(value) # revert to base control behavior
- return
- # else...
- # empty previous contents, replacing entire value:
- self._SetInsertionPoint(0)
- self._SetSelection(0, self._masklength)
-
- if( len(value) < self._masklength # value shorter than control
- and (self._isFloat or self._isInt) # and it's a numeric control
- and self._ctrl_constraints._alignRight ): # and it's a right-aligned control
- # try to intelligently "pad out" the value to the right size:
- value = self._template[0:self._masklength - len(value)] + value
-## dbg('padded value = "%s"' % value)
-
- # For wxComboBox, ensure that values are properly padded so that
- # if varying length choices are supplied, they always show up
- # in the window properly, and will be the appropriate length
- # to match the mask:
- elif self._ctrl_constraints._alignRight:
- value = value.rjust(self._masklength)
- else:
- value = value.ljust(self._masklength)
-
-
- # make SetValue behave the same as if you had typed the value in:
- try:
- value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
- if self._isFloat:
- self._isNeg = False # (clear current assumptions)
- value = self._adjustFloat(value)
- elif self._isInt:
- self._isNeg = False # (clear current assumptions)
- value = self._adjustInt(value)
- elif self._isDate and not self.IsValid(value) and self._4digityear:
- value = self._adjustDate(value, fixcentury=True)
- except ValueError:
- # If date, year might be 2 digits vs. 4; try adjusting it:
- if self._isDate and self._4digityear:
- dateparts = value.split(' ')
- dateparts[0] = self._adjustDate(dateparts[0], fixcentury=True)
- value = string.join(dateparts, ' ')
-## dbg('adjusted value: "%s"' % value)
- value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
- else:
- raise
-
- self._SetValue(value)
-#### dbg('queuing insertion after .SetValue', self._masklength)
- wx.CallAfter(self._SetInsertionPoint, self._masklength)
- wx.CallAfter(self._SetSelection, self._masklength, self._masklength)
-
-
- def _Refresh(self):
- """
- Allow mixin to refresh the base control with this function.
- REQUIRED by any class derived from MaskedEditMixin.
- """
- wx.ComboBox.Refresh(self)
-
- def Refresh(self):
- """
- This function redefines the externally accessible .Refresh() to
- validate the contents of the masked control as it refreshes.
- NOTE: this must be done in the class derived from the base wx control.
- """
- self._CheckValid()
- self._Refresh()
-
-
- def _IsEditable(self):
- """
- Allow mixin to determine if the base control is editable with this function.
- REQUIRED by any class derived from MaskedEditMixin.
- """
- return not self.__readonly
-
-
- def Cut(self):
- """
- This function redefines the externally accessible .Cut to be
- a smart "erase" of the text in question, so as not to corrupt the
- masked control. NOTE: this must be done in the class derived
- from the base wx control.
- """
- if self._mask:
- self._Cut() # call the mixin's Cut method
- else:
- wx.ComboBox.Cut(self) # else revert to base control behavior
-
-
- def Paste(self):
- """
- This function redefines the externally accessible .Paste to be
- a smart "paste" of the text in question, so as not to corrupt the
- masked control. NOTE: this must be done in the class derived
- from the base wx control.
- """
- if self._mask:
- self._Paste() # call the mixin's Paste method
- else:
- wx.ComboBox.Paste(self) # else revert to base control behavior
-
-
- def Undo(self):
- """
- This function defines the undo operation for the control. (The default
- undo is 1-deep.)
- """
- if self._mask:
- self._Undo()
- else:
- wx.ComboBox.Undo() # else revert to base control behavior
-
-
- def Append( self, choice, clientData=None ):
- """
- This function override is necessary so we can keep track of any additions to the list
- of choices, because wxComboBox doesn't have an accessor for the choice list.
- The code here is the same as in the SetParameters() mixin function, but is
- done for the individual value as appended, so the list can be built incrementally
- without speed penalty.
- """
- if self._mask:
- if type(choice) not in (types.StringType, types.UnicodeType):
- raise TypeError('%s: choices must be a sequence of strings' % str(self._index))
- elif not self.IsValid(choice):
- raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self._index), choice))
-
- if not self._ctrl_constraints._choices:
- self._ctrl_constraints._compareChoices = []
- self._ctrl_constraints._choices = []
- self._hasList = True
-
- compareChoice = choice.strip()
-
- if self._ctrl_constraints._compareNoCase:
- compareChoice = compareChoice.lower()
-
- if self._ctrl_constraints._alignRight:
- choice = choice.rjust(self._masklength)
- else:
- choice = choice.ljust(self._masklength)
- if self._ctrl_constraints._fillChar != ' ':
- choice = choice.replace(' ', self._fillChar)
-## dbg('updated choice:', choice)
-
-
- self._ctrl_constraints._compareChoices.append(compareChoice)
- self._ctrl_constraints._choices.append(choice)
- self._choices = self._ctrl_constraints._choices # (for shorthand)
-
- if( not self.IsValid(choice) and
- (not self._ctrl_constraints.IsEmpty(choice) or
- (self._ctrl_constraints.IsEmpty(choice) and self._ctrl_constraints._validRequired) ) ):
- raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice, self.name))
-
- wx.ComboBox.Append(self, choice, clientData)
-
-
-
- def Clear( self ):
- """
- This function override is necessary so we can keep track of any additions to the list
- of choices, because wxComboBox doesn't have an accessor for the choice list.
- """
- if self._mask:
- self._choices = []
- self._ctrl_constraints._autoCompleteIndex = -1
- if self._ctrl_constraints._choices:
- self.SetCtrlParameters(choices=[])
- wx.ComboBox.Clear(self)
-
-
- def _OnCtrlParametersChanged(self):
- """
- Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so
- we can update the base control:
- """
- if self.controlInitialized and self._choices != self._ctrl_constraints._choices:
- wx.ComboBox.Clear(self)
- self._choices = self._ctrl_constraints._choices
- for choice in self._choices:
- wx.ComboBox.Append( self, choice )
-
-
- def GetMark(self):
- """
- This function is a hack to make up for the fact that wxComboBox has no
- method for returning the selected portion of its edit control. It
- works, but has the nasty side effect of generating lots of intermediate
- events.
- """
-## dbg(suspend=1) # turn off debugging around this function
-## dbg('MaskedComboBox::GetMark', indent=1)
- if self.__readonly:
-## dbg(indent=0)
- return 0, 0 # no selection possible for editing
-## sel_start, sel_to = wxComboBox.GetMark(self) # what I'd *like* to have!
- sel_start = sel_to = self.GetInsertionPoint()
-## dbg("current sel_start:", sel_start)
- value = self.GetValue()
-## dbg('value: "%s"' % value)
-
- self._ignoreChange = True # tell _OnTextChange() to ignore next event (if any)
-
- wx.ComboBox.Cut(self)
- newvalue = self.GetValue()
-## dbg("value after Cut operation:", newvalue)
-
- if newvalue != value: # something was selected; calculate extent
-## dbg("something selected")
- sel_to = sel_start + len(value) - len(newvalue)
- wx.ComboBox.SetValue(self, value) # restore original value and selection (still ignoring change)
- wx.ComboBox.SetInsertionPoint(self, sel_start)
- wx.ComboBox.SetMark(self, sel_start, sel_to)
-
- self._ignoreChange = False # tell _OnTextChange() to pay attn again
-
-## dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0)
- return sel_start, sel_to
-
-
- def SetSelection(self, index):
- """
- Necessary for bookkeeping on choice selection, to keep current value
- current.
- """
-## dbg('MaskedComboBox::SetSelection(%d)' % index)
- if self._mask:
- self._prevValue = self._curValue
- self._curValue = self._choices[index]
- self._ctrl_constraints._autoCompleteIndex = index
- wx.ComboBox.SetSelection(self, index)
-
-
- def OnKeyDown(self, event):
- """
- This function is necessary because navigation and control key
- events do not seem to normally be seen by the wxComboBox's
- EVT_CHAR routine. (Tabs don't seem to be visible no matter
- what... {:-( )
- """
- if event.GetKeyCode() in self._nav + self._control:
- self._OnChar(event)
- return
- else:
- event.Skip() # let mixin default KeyDown behavior occur
-
-
- def OnSelectChoice(self, event):
- """
- This function appears to be necessary, because the processing done
- on the text of the control somehow interferes with the combobox's
- selection mechanism for the arrow keys.
- """
-## dbg('MaskedComboBox::OnSelectChoice', indent=1)
-
- if not self._mask:
- event.Skip()
- return
-
- value = self.GetValue().strip()
-
- if self._ctrl_constraints._compareNoCase:
- value = value.lower()
-
- if event.GetKeyCode() == wx.WXK_UP:
- direction = -1
- else:
- direction = 1
- match_index, partial_match = self._autoComplete(
- direction,
- self._ctrl_constraints._compareChoices,
- value,
- self._ctrl_constraints._compareNoCase,
- current_index = self._ctrl_constraints._autoCompleteIndex)
- if match_index is not None:
-## dbg('setting selection to', match_index)
- # issue appropriate event to outside:
- self._OnAutoSelect(self._ctrl_constraints, match_index=match_index)
- self._CheckValid()
- keep_processing = False
- else:
- pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
- field = self._FindField(pos)
- if self.IsEmpty() or not field._hasList:
-## dbg('selecting 1st value in list')
- self._OnAutoSelect(self._ctrl_constraints, match_index=0)
- self._CheckValid()
- keep_processing = False
- else:
- # attempt field-level auto-complete
-## dbg(indent=0)
- keep_processing = self._OnAutoCompleteField(event)
-## dbg('keep processing?', keep_processing, indent=0)
- return keep_processing
-
-
- def _OnAutoSelect(self, field, match_index):
- """
- Override mixin (empty) autocomplete handler, so that autocompletion causes
- combobox to update appropriately.
- """
-## dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1)
-## field._autoCompleteIndex = match_index
- if field == self._ctrl_constraints:
- self.SetSelection(match_index)
-## dbg('issuing combo selection event')
- self.GetEventHandler().ProcessEvent(
- MaskedComboBoxSelectEvent( self.GetId(), match_index, self ) )
- self._CheckValid()
-## dbg('field._autoCompleteIndex:', match_index)
-## dbg('self.GetSelection():', self.GetSelection())
-## dbg(indent=0)
-
-
- def _OnReturn(self, event):
- """
- For wxComboBox, it seems that if you hit return when the dropdown is
- dropped, the event that dismisses the dropdown will also blank the
- control, because of the implementation of wxComboBox. So here,
- we look and if the selection is -1, and the value according to
- (the base control!) is a value in the list, then we schedule a
- programmatic wxComboBox.SetSelection() call to pick the appropriate
- item in the list. (and then do the usual OnReturn bit.)
- """
-## dbg('MaskedComboBox::OnReturn', indent=1)
-## dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection())
- if self.GetSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
- wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
-
- event.m_keyCode = wx.WXK_TAB
- event.Skip()
-## dbg(indent=0)
-
-
-class MaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
- """
- This extra level of inheritance allows us to add the generic set of
- masked edit parameters only to this class while allowing other
- classes to derive from the "base" masked combobox control, and provide
- a smaller set of valid accessor functions.
- """
- pass
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-class IpAddrCtrlAccessorsMixin:
- # Define IpAddrCtrl's list of attributes having their own
- # Get/Set functions, exposing only those that make sense for
- # an IP address control.
-
- exposed_basectrl_params = (
- 'fields',
- 'retainFieldValidation',
- 'formatcodes',
- 'fillChar',
- 'defaultValue',
- 'description',
-
- 'useFixedWidthFont',
- 'signedForegroundColour',
- 'emptyBackgroundColour',
- 'validBackgroundColour',
- 'invalidBackgroundColour',
-
- 'emptyInvalid',
- '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 IpAddrCtrl( BaseMaskedTextCtrl, IpAddrCtrlAccessorsMixin ):
- """
- This class is a particular type of MaskedTextCtrl that accepts
- and understands the semantics of IP addresses, reformats input
- as you move from field to field, and accepts '.' as a navigation
- character, so that typing an IP address can be done naturally.
- """
-
-
-
- def __init__( self, parent, id=-1, value = '',
- pos = wx.DefaultPosition,
- size = wx.DefaultSize,
- style = wx.TE_PROCESS_TAB,
- validator = wx.DefaultValidator,
- name = 'IpAddrCtrl',
- setupEventHandling = True, ## setup event handling by default
- **kwargs):
-
- if not kwargs.has_key('mask'):
- kwargs['mask'] = mask = "###.###.###.###"
- if not kwargs.has_key('formatcodes'):
- kwargs['formatcodes'] = 'F_Sr<'
- if not kwargs.has_key('validRegex'):
- kwargs['validRegex'] = "( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))(\.( \d| \d\d|(1\d\d|2[0-4]\d|25[0-5]))){3}"
-
-
- BaseMaskedTextCtrl.__init__(
- self, parent, id=id, value = value,
- pos=pos, size=size,
- style = style,
- validator = validator,
- name = name,
- setupEventHandling = setupEventHandling,
- **kwargs)
-
-
- # set up individual field parameters as well:
- field_params = {}
- field_params['validRegex'] = "( | \d| \d |\d | \d\d|\d\d |\d \d|(1\d\d|2[0-4]\d|25[0-5]))"
-
- # require "valid" string; this prevents entry of any value > 255, but allows
- # intermediate constructions; overall control validation requires well-formatted value.
- field_params['formatcodes'] = 'V'
-
- if field_params:
- for i in self._field_indices:
- self.SetFieldParameters(i, **field_params)
-
- # This makes '.' act like tab:
- self._AddNavKey('.', handler=self.OnDot)
- self._AddNavKey('>', handler=self.OnDot) # for "shift-."
-
-
- def OnDot(self, event):
-## dbg('IpAddrCtrl::OnDot', indent=1)
- pos = self._adjustPos(self._GetInsertionPoint(), event.GetKeyCode())
- oldvalue = self.GetValue()
- edit_start, edit_end, slice = self._FindFieldExtent(pos, getslice=True)
- if not event.ShiftDown():
- if pos > edit_start and pos < edit_end:
- # clip data in field to the right of pos, if adjusting fields
- # when not at delimeter; (assumption == they hit '.')
- newvalue = oldvalue[:pos] + ' ' * (edit_end - pos) + oldvalue[edit_end:]
- self._SetValue(newvalue)
- self._SetInsertionPoint(pos)
-## dbg(indent=0)
- return self._OnChangeField(event)
-
-
-
- def GetAddress(self):
- value = BaseMaskedTextCtrl.GetValue(self)
- return value.replace(' ','') # remove spaces from the value
-
-
- def _OnCtrl_S(self, event):
-## dbg("IpAddrCtrl::_OnCtrl_S")
- if self._demo:
- print "value:", self.GetAddress()
- return False
-
- def SetValue(self, value):
-## dbg('IpAddrCtrl::SetValue(%s)' % str(value), indent=1)
- if type(value) not in (types.StringType, types.UnicodeType):
-## dbg(indent=0)
- raise ValueError('%s must be a string', str(value))
-
- bValid = True # assume True
- parts = value.split('.')
- if len(parts) != 4:
- bValid = False
- else:
- for i in range(4):
- part = parts[i]
- if not 0 <= len(part) <= 3:
- bValid = False
- break
- elif part.strip(): # non-empty part
- try:
- j = string.atoi(part)
- if not 0 <= j <= 255:
- bValid = False
- break
- else:
- parts[i] = '%3d' % j
- except:
- bValid = False
- break
- else:
- # allow empty sections for SetValue (will result in "invalid" value,
- # but this may be useful for initializing the control:
- parts[i] = ' ' # convert empty field to 3-char length
-
- if not bValid:
-## dbg(indent=0)
- raise ValueError('value (%s) must be a string of form n.n.n.n where n is empty or in range 0-255' % str(value))
- else:
-## dbg('parts:', parts)
- value = string.join(parts, '.')
- BaseMaskedTextCtrl.SetValue(self, value)
-## dbg(indent=0)
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-## these are helper subroutines:
-
-def movetofloat( origvalue, fmtstring, neg, addseparators=False, sepchar = ',',fillchar=' '):
- """ addseparators = add separator character every three numerals if True
- """
- fmt0 = fmtstring.split('.')
- fmt1 = fmt0[0]
- fmt2 = fmt0[1]
- val = origvalue.split('.')[0].strip()
- ret = fillchar * (len(fmt1)-len(val)) + val + "." + "0" * len(fmt2)
- if neg:
- ret = '-' + ret[1:]
- return (ret,len(fmt1))
-
-
-def isDateType( fmtstring ):
- """ Checks the mask and returns True if it fits an allowed
- date or datetime format.
- """
- dateMasks = ("^##/##/####",
- "^##-##-####",
- "^##.##.####",
- "^####/##/##",
- "^####-##-##",
- "^####.##.##",
- "^##/CCC/####",
- "^##.CCC.####",
- "^##/##/##$",
- "^##/##/## ",
- "^##/CCC/##$",
- "^##.CCC.## ",)
- reString = "|".join(dateMasks)
- filter = re.compile( reString)
- if re.match(filter,fmtstring): return True
- return False
-
-def isTimeType( fmtstring ):
- """ Checks the mask and returns True if it fits an allowed
- time format.
- """
- reTimeMask = "^##:##(:##)?( (AM|PM))?"
- filter = re.compile( reTimeMask )
- if re.match(filter,fmtstring): return True
- return False
-
-
-def isFloatingPoint( fmtstring):
- filter = re.compile("[ ]?[#]+\.[#]+\n")
- if re.match(filter,fmtstring+"\n"): return True
- return False
-
-
-def isInteger( fmtstring ):
- filter = re.compile("[#]+\n")
- if re.match(filter,fmtstring+"\n"): return True
- return False
-
-
-def getDateParts( dateStr, dateFmt ):
- if len(dateStr) > 11: clip = dateStr[0:11]
- else: clip = dateStr
- if clip[-2] not in string.digits:
- clip = clip[:-1] # (got part of time; drop it)
-
- dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.')
- slices = clip.split(dateSep)
- if dateFmt == "MDY":
- y,m,d = (slices[2],slices[0],slices[1]) ## year, month, date parts
- elif dateFmt == "DMY":
- y,m,d = (slices[2],slices[1],slices[0]) ## year, month, date parts
- elif dateFmt == "YMD":
- y,m,d = (slices[0],slices[1],slices[2]) ## year, month, date parts
- else:
- y,m,d = None, None, None
- if not y:
- return None
- else:
- return y,m,d
-
-
-def getDateSepChar(dateStr):
- clip = dateStr[0:10]
- dateSep = (('/' in clip) * '/') + (('-' in clip) * '-') + (('.' in clip) * '.')
- return dateSep
-
-
-def makeDate( year, month, day, dateFmt, dateStr):
- sep = getDateSepChar( dateStr)
- if dateFmt == "MDY":
- return "%s%s%s%s%s" % (month,sep,day,sep,year) ## year, month, date parts
- elif dateFmt == "DMY":
- return "%s%s%s%s%s" % (day,sep,month,sep,year) ## year, month, date parts
- elif dateFmt == "YMD":
- return "%s%s%s%s%s" % (year,sep,month,sep,day) ## year, month, date parts
- else:
- return none
-
-
-def getYear(dateStr,dateFmt):
- parts = getDateParts( dateStr, dateFmt)
- return parts[0]
-
-def getMonth(dateStr,dateFmt):
- parts = getDateParts( dateStr, dateFmt)
- return parts[1]
-
-def getDay(dateStr,dateFmt):
- parts = getDateParts( dateStr, dateFmt)
- return parts[2]
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-class test(wx.PySimpleApp):
- def OnInit(self):
- from wx.lib.rcsizer import RowColSizer
- self.frame = wx.Frame( None, -1, "MaskedEditMixin 0.0.7 Demo Page #1", size = (700,600))
- self.panel = wx.Panel( self.frame, -1)
- self.sizer = RowColSizer()
- self.labels = []
- self.editList = []
- rowcount = 4
-
- id, id1 = wx.NewId(), wx.NewId()
- self.command1 = wx.Button( self.panel, id, "&Close" )
- self.command2 = wx.Button( self.panel, id1, "&AutoFormats" )
- self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5)
- self.sizer.Add(self.command2, row=0, col=1, colspan=2, flag=wx.ALL, border = 5)
- self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1 )
-## self.panel.SetDefaultItem(self.command1 )
- self.panel.Bind(wx.EVT_BUTTON, self.onClickPage, self.command2)
-
- self.check1 = wx.CheckBox( self.panel, -1, "Disallow Empty" )
- self.check2 = wx.CheckBox( self.panel, -1, "Highlight Empty" )
- self.sizer.Add( self.check1, row=0,col=3, flag=wx.ALL,border=5 )
- self.sizer.Add( self.check2, row=0,col=4, flag=wx.ALL,border=5 )
- self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck1, self.check1 )
- self.panel.Bind(wx.EVT_CHECKBOX, self._onCheck2, self.check2 )
-
-
- label = """Press ctrl-s in any field to output the value and plain value. Press ctrl-x to clear and re-set any field.
-Note that all controls have been auto-sized by including F in the format code.
-Try entering nonsensical or partial values in validated fields to see what happens (use ctrl-s to test the valid status)."""
- label2 = "\nNote that the State and Last Name fields are list-limited (Name:Smith,Jones,Williams)."
-
- self.label1 = wx.StaticText( self.panel, -1, label)
- self.label2 = wx.StaticText( self.panel, -1, "Description")
- self.label3 = wx.StaticText( self.panel, -1, "Mask Value")
- self.label4 = wx.StaticText( self.panel, -1, "Format")
- self.label5 = wx.StaticText( self.panel, -1, "Reg Expr Val. (opt)")
- self.label6 = wx.StaticText( self.panel, -1, "wxMaskedEdit Ctrl")
- self.label7 = wx.StaticText( self.panel, -1, label2)
- self.label7.SetForegroundColour("Blue")
- self.label1.SetForegroundColour("Blue")
- self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
- self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
- self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
- self.label5.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
- self.label6.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
-
- self.sizer.Add( self.label1, row=1,col=0,colspan=7, flag=wx.ALL,border=5)
- self.sizer.Add( self.label7, row=2,col=0,colspan=7, flag=wx.ALL,border=5)
- self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5)
- self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5)
- self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5)
- self.sizer.Add( self.label5, row=3,col=3, flag=wx.ALL,border=5)
- self.sizer.Add( self.label6, row=3,col=4, flag=wx.ALL,border=5)
-
- # The following list is of the controls for the demo. Feel free to play around with
- # the options!
- controls = [
- #description mask excl format regexp range,list,initial
- ("Phone No", "(###) ###-#### x:###", "", 'F!^-R', "^\(\d\d\d\) \d\d\d-\d\d\d\d", (),[],''),
- ("Last Name Only", "C{14}", "", 'F {list}', '^[A-Z][a-zA-Z]+', (),('Smith','Jones','Williams'),''),
- ("Full Name", "C{14}", "", 'F_', '^[A-Z][a-zA-Z]+ [A-Z][a-zA-Z]+', (),[],''),
- ("Social Sec#", "###-##-####", "", 'F', "\d{3}-\d{2}-\d{4}", (),[],''),
- ("U.S. Zip+4", "#{5}-#{4}", "", 'F', "\d{5}-(\s{4}|\d{4})",(),[],''),
- ("U.S. State (2 char)\n(with default)","AA", "", 'F!', "[A-Z]{2}", (),states, 'AZ'),
- ("Customer No", "\CAA-###", "", 'F!', "C[A-Z]{2}-\d{3}", (),[],''),
- ("Date (MDY) + Time\n(with default)", "##/##/#### ##:## AM", 'BCDEFGHIJKLMNOQRSTUVWXYZ','DFR!',"", (),[], r'03/05/2003 12:00 AM'),
- ("Invoice Total", "#{9}.##", "", 'F-R,', "", (),[], ''),
- ("Integer (signed)\n(with default)", "#{6}", "", 'F-R', "", (),[], '0 '),
- ("Integer (unsigned)\n(with default), 1-399", "######", "", 'F', "", (1,399),[], '1 '),
- ("Month selector", "XXX", "", 'F', "", (),
- ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],""),
- ("fraction selector","#/##", "", 'F', "^\d\/\d\d?", (),
- ['2/3', '3/4', '1/2', '1/4', '1/8', '1/16', '1/32', '1/64'], "")
- ]
-
- for control in controls:
- self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL)
- self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL)
- self.sizer.Add( wx.StaticText( self.panel, -1, control[3]),row=rowcount, col=2,border=5, flag=wx.ALL)
- self.sizer.Add( wx.StaticText( self.panel, -1, control[4][:20]),row=rowcount, col=3,border=5, flag=wx.ALL)
-
- if control in controls[:]:#-2]:
- newControl = MaskedTextCtrl( self.panel, -1, "",
- mask = control[1],
- excludeChars = control[2],
- formatcodes = control[3],
- includeChars = "",
- validRegex = control[4],
- validRange = control[5],
- choices = control[6],
- defaultValue = control[7],
- demo = True)
- if control[6]: newControl.SetCtrlParameters(choiceRequired = True)
- else:
- newControl = MaskedComboBox( self.panel, -1, "",
- choices = control[7],
- choiceRequired = True,
- mask = control[1],
- formatcodes = control[3],
- excludeChars = control[2],
- includeChars = "",
- validRegex = control[4],
- validRange = control[5],
- demo = True)
- self.editList.append( newControl )
-
- self.sizer.Add( newControl, row=rowcount,col=4,flag=wx.ALL,border=5)
- rowcount += 1
-
- self.sizer.AddGrowableCol(4)
-
- self.panel.SetSizer(self.sizer)
- self.panel.SetAutoLayout(1)
-
- self.frame.Show(1)
- self.MainLoop()
-
- return True
-
- def onClick(self, event):
- self.frame.Close()
-
- def onClickPage(self, event):
- self.page2 = test2(self.frame,-1,"")
- self.page2.Show(True)
-
- def _onCheck1(self,event):
- """ Set required value on/off """
- value = event.IsChecked()
- if value:
- for control in self.editList:
- control.SetCtrlParameters(emptyInvalid=True)
- control.Refresh()
- else:
- for control in self.editList:
- control.SetCtrlParameters(emptyInvalid=False)
- control.Refresh()
- self.panel.Refresh()
-
- def _onCheck2(self,event):
- """ Highlight empty values"""
- value = event.IsChecked()
- if value:
- for control in self.editList:
- control.SetCtrlParameters( emptyBackgroundColour = 'Aquamarine')
- control.Refresh()
- else:
- for control in self.editList:
- control.SetCtrlParameters( emptyBackgroundColour = 'White')
- control.Refresh()
- self.panel.Refresh()
-
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-class test2(wx.Frame):
- def __init__(self, parent, id, caption):
- wx.Frame.__init__( self, parent, id, "wxMaskedEdit control 0.0.7 Demo Page #2 -- AutoFormats", size = (550,600))
- from wx.lib.rcsizer import RowColSizer
- self.panel = wx.Panel( self, -1)
- self.sizer = RowColSizer()
- self.labels = []
- self.texts = []
- rowcount = 4
-
- label = """\
-All these controls have been created by passing a single parameter, the AutoFormat code.
-The class contains an internal dictionary of types and formats (autoformats).
-To see a great example of validations in action, try entering a bad email address, then tab out."""
-
- self.label1 = wx.StaticText( self.panel, -1, label)
- self.label2 = wx.StaticText( self.panel, -1, "Description")
- self.label3 = wx.StaticText( self.panel, -1, "AutoFormat Code")
- self.label4 = wx.StaticText( self.panel, -1, "wxMaskedEdit Control")
- self.label1.SetForegroundColour("Blue")
- self.label2.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
- self.label3.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
- self.label4.SetFont(wx.Font(9,wx.SWISS,wx.NORMAL,wx.BOLD))
-
- self.sizer.Add( self.label1, row=1,col=0,colspan=3, flag=wx.ALL,border=5)
- self.sizer.Add( self.label2, row=3,col=0, flag=wx.ALL,border=5)
- self.sizer.Add( self.label3, row=3,col=1, flag=wx.ALL,border=5)
- self.sizer.Add( self.label4, row=3,col=2, flag=wx.ALL,border=5)
-
- id, id1 = wx.NewId(), wx.NewId()
- self.command1 = wx.Button( self.panel, id, "&Close")
- self.command2 = wx.Button( self.panel, id1, "&Print Formats")
- self.panel.Bind(wx.EVT_BUTTON, self.onClick, self.command1)
- self.panel.SetDefaultItem(self.command1)
- self.panel.Bind(wx.EVT_BUTTON, self.onClickPrint, self.command2)
-
- # The following list is of the controls for the demo. Feel free to play around with
- # the options!
- controls = [
- ("Phone No","USPHONEFULLEXT"),
- ("US Date + Time","USDATETIMEMMDDYYYY/HHMM"),
- ("US Date MMDDYYYY","USDATEMMDDYYYY/"),
- ("Time (with seconds)","TIMEHHMMSS"),
- ("Military Time\n(without seconds)","24HRTIMEHHMM"),
- ("Social Sec#","USSOCIALSEC"),
- ("Credit Card","CREDITCARD"),
- ("Expiration MM/YY","EXPDATEMMYY"),
- ("Percentage","PERCENT"),
- ("Person's Age","AGE"),
- ("US Zip Code","USZIP"),
- ("US Zip+4","USZIPPLUS4"),
- ("Email Address","EMAIL"),
- ("IP Address", "(derived control IpAddrCtrl)")
- ]
-
- for control in controls:
- self.sizer.Add( wx.StaticText( self.panel, -1, control[0]),row=rowcount, col=0,border=5,flag=wx.ALL)
- self.sizer.Add( wx.StaticText( self.panel, -1, control[1]),row=rowcount, col=1,border=5, flag=wx.ALL)
- if control in controls[:-1]:
- self.sizer.Add( MaskedTextCtrl( self.panel, -1, "",
- autoformat = control[1],
- demo = True),
- row=rowcount,col=2,flag=wx.ALL,border=5)
- else:
- self.sizer.Add( IpAddrCtrl( self.panel, -1, "", demo=True ),
- row=rowcount,col=2,flag=wx.ALL,border=5)
- rowcount += 1
-
- self.sizer.Add(self.command1, row=0, col=0, flag=wx.ALL, border = 5)
- self.sizer.Add(self.command2, row=0, col=1, flag=wx.ALL, border = 5)
- self.sizer.AddGrowableCol(3)
-
- self.panel.SetSizer(self.sizer)
- self.panel.SetAutoLayout(1)
-
- def onClick(self, event):
- self.Close()
-
- def onClickPrint(self, event):
- for format in masktags.keys():
- sep = "+------------------------+"
- print "%s\n%s \n Mask: %s \n RE Validation string: %s\n" % (sep,format, masktags[format]['mask'], masktags[format]['validRegex'])
-
-## ---------- ---------- ---------- ---------- ---------- ---------- ----------
-
-if __name__ == "__main__":
- app = test(False)
-
-i=1
-##
-## Current Issues:
-## ===================================
-##
-## 1. WS: For some reason I don't understand, the control is generating two (2)
-## EVT_TEXT events for every one (1) .SetValue() of the underlying control.
-## I've been unsuccessful in determining why or in my efforts to make just one
-## occur. So, I've added a hack to save the last seen value from the
-## control in the EVT_TEXT handler, and if *different*, call event.Skip()
-## to propagate it down the event chain, and let the application see it.
-##
-## 2. WS: MaskedComboBox is deficient in several areas, all having to do with the
-## behavior of the underlying control that I can't fix. The problems are:
-## a) The background coloring doesn't work in the text field of the control;
-## instead, there's a only border around it that assumes the correct color.
-## b) The control will not pass WXK_TAB to the event handler, no matter what
-## I do, and there's no style wxCB_PROCESS_TAB like wxTE_PROCESS_TAB to
-## indicate that we want these events. As a result, MaskedComboBox
-## doesn't do the nice field-tabbing that MaskedTextCtrl does.
-## c) Auto-complete had to be reimplemented for the control because programmatic
-## setting of the value of the text field does not set up the auto complete
-## the way that the control processing keystrokes does. (But I think I've
-## implemented a fairly decent approximation.) Because of this the control
-## also won't auto-complete on dropdown, and there's no event I can catch
-## to work around this problem.
-## d) There is no method provided for getting the selection; the hack I've
-## implemented has its flaws, not the least of which is that due to the
-## strategy that I'm using, the paste buffer is always replaced by the
-## contents of the control's selection when in focus, on each keystroke;
-## this makes it impossible to paste anything into a MaskedComboBox
-## at the moment... :-(
-## e) The other deficient behavior, likely induced by the workaround for (d),
-## is that you can can't shift-left to select more than one character
-## at a time.
-##
-##
-## 3. WS: Controls on wxPanels don't seem to pass Shift-WXK_TAB to their
-## EVT_KEY_DOWN or EVT_CHAR event handlers. Until this is fixed in
-## wxWidgets, shift-tab won't take you backwards through the fields of
-## a MaskedTextCtrl like it should. Until then Shifted arrow keys will
-## work like shift-tab and tab ought to.
-##
-
-## To-Do's:
-## =============================##
-## 1. Add Popup list for auto-completable fields that simulates combobox on individual
-## fields. Example: City validates against list of cities, or zip vs zip code list.
-## 2. Allow optional monetary symbols (eg. $, pounds, etc.) at front of a "decimal"
-## control.
-## 3. Fix shift-left selection for MaskedComboBox.
-## 5. Transform notion of "decimal control" to be less "entire control"-centric,
-## so that monetary symbols can be included and still have the appropriate
-## semantics. (Big job, as currently written, but would make control even
-## more useful for business applications.)
-
-
-## CHANGELOG:
-## ====================
-## Version 1.5
-## (Reported) bugs fixed:
-## 1. Crash ensues if you attempt to change the mask of a read-only
-## MaskedComboBox after initial construction.
-## 2. Changed strategy of defining Get/Set property functions so that
-## these are now generated dynamically at runtime, rather than as
-## part of the class definition. (This makes it possible to have
-## more general base classes that have many more options for configuration
-## without requiring that derivations support the same options.)
-## 3. Fixed IsModified for _Paste() and _OnErase().
-##
-## Enhancements:
-## 1. Fixed "attribute function inheritance," since base control is more
-## generic than subsequent derivations, not all property functions of a
-## generic control should be exposed in those derivations. New strategy
-## uses base control classes (eg. BaseMaskedTextCtrl) that should be
-## used to derive new class types, and mixed with their own mixins to
-## only expose those attributes from the generic masked controls that
-## make sense for the derivation. (This makes Boa happier.)
-## 2. Renamed (with b-c) MILTIME autoformats to 24HRTIME, so as to be less
-## "parochial."
-##
-## Version 1.4
-## (Reported) bugs fixed:
-## 1. Right-click menu allowed "cut" operation that destroyed mask
-## (was implemented by base control)
-## 2. MaskedComboBox didn't allow .Append() of mixed-case values; all
-## got converted to lower case.
-## 3. MaskedComboBox selection didn't deal with spaces in values
-## properly when autocompleting, and didn't have a concept of "next"
-## match for handling choice list duplicates.
-## 4. Size of MaskedComboBox was always default.
-## 5. Email address regexp allowed some "non-standard" things, and wasn't
-## general enough.
-## 6. Couldn't easily reset MaskedComboBox contents programmatically.
-## 7. Couldn't set emptyInvalid during construction.
-## 8. Under some versions of wxPython, readonly comboboxes can apparently
-## return a GetInsertionPoint() result (655535), causing masked control
-## to fail.
-## 9. Specifying an empty mask caused the controls to traceback.
-## 10. Can't specify float ranges for validRange.
-## 11. '.' from within a the static portion of a restricted IP address
-## destroyed the mask from that point rightward; tab when cursor is
-## before 1st field takes cursor past that field.
-##
-## Enhancements:
-## 12. Added Ctrl-Z/Undo handling, (and implemented context-menu properly.)
-## 13. Added auto-select option on char input for masked controls with
-## choice lists.
-## 14. Added '>' formatcode, allowing insert within a given or each field
-## as appropriate, rather than requiring "overwrite". This makes single
-## field controls that just have validation rules (eg. EMAIL) much more
-## friendly. The same flag controls left shift when deleting vs just
-## blanking the value, and for right-insert fields, allows right-insert
-## at any non-blank (non-sign) position in the field.
-## 15. Added option to use to indicate negative values for numeric controls.
-## 16. Improved OnFocus handling of numeric controls.
-## 17. Enhanced Home/End processing to allow operation on a field level,
-## using ctrl key.
-## 18. Added individual Get/Set functions for control parameters, for
-## simplified integration with Boa Constructor.
-## 19. Standardized "Colour" parameter names to match wxPython, with
-## non-british spellings still supported for backward-compatibility.
-## 20. Added '&' mask specification character for punctuation only (no letters
-## or digits).
-## 21. Added (in a separate file) wxMaskedCtrl() factory function to provide
-## unified interface to the masked edit subclasses.
-##
-##
-## Version 1.3
-## 1. Made it possible to configure grouping, decimal and shift-decimal characters,
-## to make controls more usable internationally.
-## 2. Added code to smart "adjust" value strings presented to .SetValue()
-## for right-aligned numeric format controls if they are shorter than
-## than the control width, prepending the missing portion, prepending control
-## template left substring for the missing characters, so that setting
-## numeric values is easier.
-## 3. Renamed SetMaskParameters SetCtrlParameters() (with old name preserved
-## for b-c), as this makes more sense.
-##
-## Version 1.2
-## 1. Fixed .SetValue() to replace the current value, rather than the current
-## selection. Also changed it to generate ValueError if presented with
-## either a value which doesn't follow the format or won't fit. Also made
-## set value adjust numeric and date controls as if user entered the value.
-## Expanded doc explaining how SetValue() works.
-## 2. Fixed EUDATE* autoformats, fixed IsDateType mask list, and added ability to
-## use 3-char months for dates, and EUDATETIME, and EUDATEMILTIME autoformats.
-## 3. Made all date autoformats automatically pick implied "datestyle".
-## 4. Added IsModified override, since base wxTextCtrl never reports modified if
-## .SetValue used to change the value, which is what the masked edit controls
-## use internally.
-## 5. Fixed bug in date position adjustment on 2 to 4 digit date conversion when
-## using tab to "leave field" and auto-adjust.
-## 6. Fixed bug in _isCharAllowed() for negative number insertion on pastes,
-## and bug in ._Paste() that didn't account for signs in signed masks either.
-## 7. Fixed issues with _adjustPos for right-insert fields causing improper
-## selection/replacement of values
-## 8. Fixed _OnHome handler to properly handle extending current selection to
-## beginning of control.
-## 9. Exposed all (valid) autoformats to demo, binding descriptions to
-## autoformats.
-## 10. Fixed a couple of bugs in email regexp.
-## 11. Made maskchardict an instance var, to make mask chars to be more
-## amenable to international use.
-## 12. Clarified meaning of '-' formatcode in doc.
-## 13. Fixed a couple of coding bugs being flagged by Python2.1.
-## 14. Fixed several issues with sign positioning, erasure and validity
-## checking for "numeric" masked controls.
-## 15. Added validation to IpAddrCtrl.SetValue().
-##
-## Version 1.1
-## 1. Changed calling interface to use boolean "useFixedWidthFont" (True by default)
-## vs. literal font facename, and use wxTELETYPE as the font family
-## if so specified.
-## 2. Switched to use of dbg module vs. locally defined version.
-## 3. Revamped entire control structure to use Field classes to hold constraint
-## and formatting data, to make code more hierarchical, allow for more
-## sophisticated masked edit construction.
-## 4. Better strategy for managing options, and better validation on keywords.
-## 5. Added 'V' format code, which requires that in order for a character
-## to be accepted, it must result in a string that passes the validRegex.
-## 6. Added 'S' format code which means "select entire field when navigating
-## to new field."
-## 7. Added 'r' format code to allow "right-insert" fields. (implies 'R'--right-alignment)
-## 8. Added '<' format code to allow fields to require explicit cursor movement
-## to leave field.
-## 9. Added validFunc option to other validation mechanisms, that allows derived
-## classes to add dynamic validation constraints to the control.
-## 10. Fixed bug in validatePaste code causing possible IndexErrors, and also
-## fixed failure to obey case conversion codes when pasting.
-## 11. Implemented '0' (zero-pad) formatting code, as it wasn't being done anywhere...
-## 12. Removed condition from OnDecimalPoint, so that it always truncates right on '.'
-## 13. Enhanced IpAddrCtrl to use right-insert fields, selection on field traversal,
-## individual field validation to prevent field values > 255, and require explicit
-## tab/. to change fields.
-## 14. Added handler for left double-click to select field under cursor.
-## 15. Fixed handling for "Read-only" styles.
-## 16. Separated signedForegroundColor from 'R' style, and added foregroundColor
-## attribute, for more consistent and controllable coloring.
-## 17. Added retainFieldValidation parameter, allowing top-level constraints
-## such as "validRequired" to be set independently of field-level equivalent.
-## (needed in TimeCtrl for bounds constraints.)
-## 18. Refactored code a bit, cleaned up and commented code more heavily, fixed
-## some of the logic for setting/resetting parameters, eg. fillChar, defaultValue,
-## etc.
-## 19. Fixed maskchar setting for upper/lowercase, to work in all locales.
-##
-##
-## Version 1.0
-## 1. Decimal point behavior restored for decimal and integer type controls:
-## decimal point now trucates the portion > 0.
-## 2. Return key now works like the tab character and moves to the next field,
-## provided no default button is set for the form panel on which the control
-## resides.
-## 3. Support added in _FindField() for subclasses controls (like timecontrol)
-## to determine where the current insertion point is within the mask (i.e.
-## which sub-'field'). See method documentation for more info and examples.
-## 4. Added Field class and support for all constraints to be field-specific
-## in addition to being globally settable for the control.
-## Choices for each field are validated for length and pastability into
-## the field in question, raising ValueError if not appropriate for the control.
-## Also added selective additional validation based on individual field constraints.
-## By default, SHIFT-WXK_DOWN, SHIFT-WXK_UP, WXK_PRIOR and WXK_NEXT all
-## auto-complete fields with choice lists, supplying the 1st entry in
-## the choice list if the field is empty, and cycling through the list in
-## the appropriate direction if already a match. WXK_DOWN will also auto-
-## complete if the field is partially completed and a match can be made.
-## SHIFT-WXK_UP/DOWN will also take you to the next field after any
-## auto-completion performed.
-## 5. Added autoCompleteKeycodes=[] parameters for allowing further
-## customization of the control. Any keycode supplied as a member
-## of the _autoCompleteKeycodes list will be treated like WXK_NEXT. If
-## requireFieldChoice is set, then a valid value from each non-empty
-## choice list will be required for the value of the control to validate.
-## 6. Fixed "auto-sizing" to be relative to the font actually used, rather
-## than making assumptions about character width.
-## 7. Fixed GetMaskParameter(), which was non-functional in previous version.
-## 8. Fixed exceptions raised to provide info on which control had the error.
-## 9. Fixed bug in choice management of MaskedComboBox.
-## 10. Fixed bug in IpAddrCtrl causing traceback if field value was of
-## the form '# #'. Modified control code for IpAddrCtrl so that '.'
-## in the middle of a field clips the rest of that field, similar to
-## decimal and integer controls.
-##
-##
-## Version 0.0.7
-## 1. "-" is a toggle for sign; "+" now changes - signed numerics to positive.
-## 2. ',' in formatcodes now causes numeric values to be comma-delimited (e.g.333,333).
-## 3. New support for selecting text within the control.(thanks Will Sadkin!)
-## Shift-End and Shift-Home now select text as you would expect
-## Control-Shift-End selects to the end of the mask string, even if value not entered.
-## Control-A selects all *entered* text, Shift-Control-A selects everything in the control.
-## 4. event.Skip() added to onKillFocus to correct remnants when running in Linux (contributed-
-## for some reason I couldn't find the original email but thanks!!!)
-## 5. All major key-handling code moved to their own methods for easier subclassing: OnHome,
-## OnErase, OnEnd, OnCtrl_X, OnCtrl_A, etc.
-## 6. Email and autoformat validations corrected using regex provided by Will Sadkin (thanks!).
-## (The rest of the changes in this version were done by Will Sadkin with permission from Jeff...)
-## 7. New mechanism for replacing default behavior for any given key, using
-## ._SetKeycodeHandler(keycode, func) and ._SetKeyHandler(char, func) now available
-## for easier subclassing of the control.
-## 8. Reworked the delete logic, cut, paste and select/replace logic, as well as some bugs
-## with insertion point/selection modification. Changed Ctrl-X to use standard "cut"
-## semantics, erasing the selection, rather than erasing the entire control.
-## 9. Added option for an "default value" (ie. the template) for use when a single fillChar
-## is not desired in every position. Added IsDefault() function to mean "does the value
-## equal the template?" and modified .IsEmpty() to mean "do all of the editable
-## positions in the template == the fillChar?"
-## 10. Extracted mask logic into mixin, so we can have both MaskedTextCtrl and MaskedComboBox,
-## now included.
-## 11. MaskedComboBox now adds the capability to validate from list of valid values.
-## Example: City validates against list of cities, or zip vs zip code list.
-## 12. Fixed oversight in EVT_TEXT handler that prevented the events from being
-## passed to the next handler in the event chain, causing updates to the
-## control to be invisible to the parent code.
-## 13. Added IPADDR autoformat code, and subclass IpAddrCtrl for controlling tabbing within
-## the control, that auto-reformats as you move between cells.
-## 14. Mask characters [A,a,X,#] can now appear in the format string as literals, by using '\'.
-## 15. It is now possible to specify repeating masks, e.g. #{3}-#{3}-#{14}
-## 16. Fixed major bugs in date validation, due to the fact that
-## wxDateTime.ParseDate is too liberal, and will accept any form that
-## makes any kind of sense, regardless of the datestyle you specified
-## for the control. Unfortunately, the strategy used to fix it only
-## works for versions of wxPython post 2.3.3.1, as a C++ assert box
-## seems to show up on an invalid date otherwise, instead of a catchable
-## exception.
-## 17. Enhanced date adjustment to automatically adjust heuristic based on
-## current year, making last century/this century determination on
-## 2-digit year based on distance between today's year and value;
-## if > 50 year separation, assume last century (and don't assume last
-## century is 20th.)
-## 18. Added autoformats and support for including HHMMSS as well as HHMM for
-## date times, and added similar time, and militaray time autoformats.
-## 19. Enhanced tabbing logic so that tab takes you to the next field if the
-## control is a multi-field control.
-## 20. Added stub method called whenever the control "changes fields", that
-## can be overridden by subclasses (eg. IpAddrCtrl.)
-## 21. Changed a lot of code to be more functionally-oriented so side-effects
-## aren't as problematic when maintaining code and/or adding features.
-## Eg: IsValid() now does not have side-effects; it merely reflects the
-## validity of the value of the control; to determine validity AND recolor
-## the control, _CheckValid() should be used with a value argument of None.
-## Similarly, made most reformatting function take an optional candidate value
-## rather than just using the current value of the control, and only
-## have them change the value of the control if a candidate is not specified.
-## In this way, you can do validation *before* changing the control.
-## 22. Changed validRequired to mean "disallow chars that result in invalid
-## value." (Old meaning now represented by emptyInvalid.) (This was
-## possible once I'd made the changes in (19) above.)
-## 23. Added .SetMaskParameters and .GetMaskParameter methods, so they
-## can be set/modified/retrieved after construction. Removed individual
-## parameter setting functions, in favor of this mechanism, so that
-## all adjustment of the control based on changing parameter values can
-## be handled in one place with unified mechanism.
-## 24. Did a *lot* of testing and fixing re: numeric values. Added ability
-## to type "grouping char" (ie. ',') and validate as appropriate.
-## 25. Fixed ZIPPLUS4 to allow either 5 or 4, but if > 5 must be 9.
-## 26. Fixed assumption about "decimal or integer" masks so that they're only
-## made iff there's no validRegex associated with the field. (This
-## is so things like zipcodes which look like integers can have more
-## restrictive validation (ie. must be 5 digits.)
-## 27. Added a ton more doc strings to explain use and derivation requirements
-## and did regularization of the naming conventions.
-## 28. Fixed a range bug in _adjustKey preventing z from being handled properly.
-## 29. Changed behavior of '.' (and shift-.) in numeric controls to move to
-## reformat the value and move the next field as appropriate. (shift-'.',
-## ie. '>' moves to the previous field.
-
-## Version 0.0.6
-## 1. Fixed regex bug that caused autoformat AGE to invalidate any age ending
-## in '0'.
-## 2. New format character 'D' to trigger date type. If the user enters 2 digits in the
-## year position, the control will expand the value to four digits, using numerals below
-## 50 as 21st century (20+nn) and less than 50 as 20th century (19+nn).
-## Also, new optional parameter datestyle = set to one of {MDY|DMY|YDM}
-## 3. revalid parameter renamed validRegex to conform to standard for all validation
-## parameters (see 2 new ones below).
-## 4. New optional init parameter = validRange. Used only for int/dec (numeric) types.
-## Allows the developer to specify a valid low/high range of values.
-## 5. New optional init parameter = validList. Used for character types. Allows developer
-## to send a list of values to the control to be used for specific validation.
-## See the Last Name Only example - it is list restricted to Smith/Jones/Williams.
-## 6. Date type fields now use wxDateTime's parser to validate the date and time.
-## This works MUCH better than my kludgy regex!! Thanks to Robin Dunn for pointing
-## me toward this solution!
-## 7. Date fields now automatically expand 2-digit years when it can. For example,
-## if the user types "03/10/67", then "67" will auto-expand to "1967". If a two-year
-## date is entered it will be expanded in any case when the user tabs out of the
-## field.
-## 8. New class functions: SetValidBackgroundColor, SetInvalidBackgroundColor, SetEmptyBackgroundColor,
-## SetSignedForeColor allow accessto override default class coloring behavior.
-## 9. Documentation updated and improved.
-## 10. Demo - page 2 is now a wxFrame class instead of a wxPyApp class. Works better.
-## Two new options (checkboxes) - test highlight empty and disallow empty.
-## 11. Home and End now work more intuitively, moving to the first and last user-entry
-## value, respectively.
-## 12. New class function: SetRequired(bool). Sets the control's entry required flag
-## (i.e. disallow empty values if True).
-##
-## Version 0.0.5
-## 1. get_plainValue method renamed to GetPlainValue following the wxWidgets
-## StudlyCaps(tm) standard (thanks Paul Moore). ;)
-## 2. New format code 'F' causes the control to auto-fit (auto-size) itself
-## based on the length of the mask template.
-## 3. Class now supports "autoformat" codes. These can be passed to the class
-## on instantiation using the parameter autoformat="code". If the code is in
-## the dictionary, it will self set the mask, formatting, and validation string.
-## I have included a number of samples, but I am hoping that someone out there
-## can help me to define a whole bunch more.
-## 4. I have added a second page to the demo (as well as a second demo class, test2)
-## to showcase how autoformats work. The way they self-format and self-size is,
-## I must say, pretty cool.
-## 5. Comments added and some internal cosmetic revisions re: matching the code
-## standards for class submission.
-## 6. Regex validation is now done in real time - field turns yellow immediately
-## and stays yellow until the entered value is valid
-## 7. Cursor now skips over template characters in a more intuitive way (before the
-## next keypress).
-## 8. Change, Keypress and LostFocus methods added for convenience of subclasses.
-## Developer may use these methods which will be called after EVT_TEXT, EVT_CHAR,
-## and EVT_KILL_FOCUS, respectively.
-## 9. Decimal and numeric handlers have been rewritten and now work more intuitively.
-##
-## Version 0.0.4
-## 1. New .IsEmpty() method returns True if the control's value is equal to the
-## blank template string
-## 2. Control now supports a new init parameter: revalid. Pass a regular expression
-## that the value will have to match when the control loses focus. If invalid,
-## the control's BackgroundColor will turn yellow, and an internal flag is set (see next).
-## 3. Demo now shows revalid functionality. Try entering a partial value, such as a
-## partial social security number.
-## 4. New .IsValid() value returns True if the control is empty, or if the value matches
-## the revalid expression. If not, .IsValid() returns False.
-## 5. Decimal values now collapse to decimal with '.00' on losefocus if the user never
-## presses the decimal point.
-## 6. Cursor now goes to the beginning of the field if the user clicks in an
-## "empty" field intead of leaving the insertion point in the middle of the
-## field.
-## 7. New "N" mask type includes upper and lower chars plus digits. a-zA-Z0-9.
-## 8. New formatcodes init parameter replaces other init params and adds functions.
-## String passed to control on init controls:
-## _ Allow spaces
-## ! Force upper
-## ^ Force lower
-## R Show negative #s in red
-## , Group digits
-## - Signed numerals
-## 0 Numeric fields get leading zeros
-## 9. Ctrl-X in any field clears the current value.
-## 10. Code refactored and made more modular (esp in OnChar method). Should be more
-## easy to read and understand.
-## 11. Demo enhanced.
-## 12. Now has _doc_.
-##
-## Version 0.0.3
-## 1. GetPlainValue() now returns the value without the template characters;
-## so, for example, a social security number (123-33-1212) would return as
-## 123331212; also removes white spaces from numeric/decimal values, so
-## "- 955.32" is returned "-955.32". Press ctrl-S to see the plain value.
-## 2. Press '.' in an integer style masked control and truncate any trailing digits.
-## 3. Code moderately refactored. Internal names improved for clarity. Additional
-## internal documentation.
-## 4. Home and End keys now supported to move cursor to beginning or end of field.
-## 5. Un-signed integers and decimals now supported.
-## 6. Cosmetic improvements to the demo.
-## 7. Class renamed to MaskedTextCtrl.
-## 8. Can now specify include characters that will override the basic
-## controls: for example, includeChars = "@." for email addresses
-## 9. Added mask character 'C' -> allow any upper or lowercase character
-## 10. .SetSignColor(str:color) sets the foreground color for negative values
-## in signed controls (defaults to red)
-## 11. Overview documentation written.
-##
-## Version 0.0.2
-## 1. Tab now works properly when pressed in last position
-## 2. Decimal types now work (e.g. #####.##)
-## 3. Signed decimal or numeric values supported (i.e. negative numbers)
-## 4. Negative decimal or numeric values now can show in red.
-## 5. Can now specify an "exclude list" with the excludeChars parameter.
-## See date/time formatted example - you can only enter A or P in the
-## character mask space (i.e. AM/PM).
-## 6. Backspace now works properly, including clearing data from a selected
-## region but leaving template characters intact. Also delete key.
-## 7. Left/right arrows now work properly.
-## 8. Removed EventManager call from test so demo should work with wxPython 2.3.3
-##