# import relevant external symbols into package namespace:
from maskededit import *
-from textctrl import BaseMaskedTextCtrl, TextCtrl
-from combobox import BaseMaskedComboBox, ComboBox, MaskedComboBoxSelectEvent
+from textctrl import BaseMaskedTextCtrl, PreMaskedTextCtrl, TextCtrl
+from combobox import BaseMaskedComboBox, PreMaskedComboBox, ComboBox, MaskedComboBoxSelectEvent
from numctrl import NumCtrl, wxEVT_COMMAND_MASKED_NUMBER_UPDATED, EVT_NUM, NumberUpdatedEvent
from timectrl import TimeCtrl, wxEVT_TIMEVAL_UPDATED, EVT_TIMEUPDATE, TimeUpdatedEvent
from ipaddrctrl import IpAddrCtrl
**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
choices=choices, style=style|wx.WANTS_CHARS,
validator=validator,
name=name)
-
self.controlInitialized = True
+ self._PostInit(style=style, setupEventHandling=setupEventHandling,
+ name=name, value=value, **kwargs)
+
+
+ def _PostInit(self, style=wx.CB_DROPDOWN,
+ setupEventHandling = True, ## setup event handling by default):
+ name = "maskedComboBox", value='', **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
+
+ if not hasattr(self, 'controlInitialized'):
+
+ self.controlInitialized = True ## must have been called via XRC, therefore base class is constructed
+ if not kwargs.has_key('choices'):
+ choices=[]
+ kwargs['choices'] = choices ## set up maskededit to work with choice list too
+ self._choices = []
+
+ ## 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.Clear(self)
+ wx.ComboBox.AppendItems(self, choices)
+
+
# Set control font - fixed width by default
self._setFont()
if self._autofit:
self.SetClientSize(self._CalcSize())
- self.SetSizeHints(self.GetSize())
+ width = self.GetSize().width
+ height = self.GetBestSize().height
+ self.SetSize((width, height))
+ self.SetSizeHints((width, height))
+
if value:
# ensure value is width of the mask of the control:
return (size[0]+20, size[1])
+ def SetFont(self, *args, **kwargs):
+ """ Set the font, then recalculate control size, if appropriate. """
+ wx.ComboBox.SetFont(self, *args, **kwargs)
+ if self._autofit:
+ dbg('calculated size:', self._CalcSize())
+ self.SetClientSize(self._CalcSize())
+ width = self.GetSize().width
+ height = self.GetBestSize().height
+ dbg('setting client size to:', (width, height))
+ self.SetSize((width, height))
+ self.SetSizeHints((width, height))
+
+
def _GetSelection(self):
"""
Allow mixin to get the text selection of this control.
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
wx.ComboBox.Append(self, choice, clientData)
+ def AppendItems( self, choices ):
+ """
+ AppendItems() is handled in terms of Append, to avoid code replication.
+ """
+ for choice in choices:
+ self.Append(choice)
+
def Clear( self ):
"""
pass
+class PreMaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
+ """
+ This allows us to use XRC subclassing.
+ """
+ # This should really be wx.EVT_WINDOW_CREATE but it is not
+ # currently delivered for native controls on all platforms, so
+ # we'll use EVT_SIZE instead. It should happen shortly after the
+ # control is created as the control is set to its "best" size.
+ _firstEventType = wx.EVT_SIZE
+
+ def __init__(self):
+ pre = wx.PreComboBox()
+ self.PostCreate(pre)
+ self.Bind(self._firstEventType, self.OnCreate)
+
+
+ def OnCreate(self, evt):
+ self.Unbind(self._firstEventType)
+ self._PostInit()
+
+i=0
+## CHANGELOG:
+## ====================
+## Version 1.1
+## 1. Added .SetFont() method that properly resizes control
+## 2. Modified control to support construction via XRC mechanism.
+## 3. Added AppendItems() to conform with latest combobox.
from wx.tools.dbg import Logger
dbg = Logger()
-##dbg(enable=0)
+##dbg(enable=1)
## ---------- ---------- ---------- ---------- ---------- ---------- ----------
self._prevValue = newvalue # disallow undo of sign type
if self._autofit:
-## dbg('setting client size to:', self._CalcSize())
+## dbg('calculated size:', self._CalcSize())
self.SetClientSize(self._CalcSize())
- self.SetSizeHints(self.GetSize())
+ width = self.GetSize().width
+ height = self.GetBestSize().height
+## dbg('setting client size to:', (width, height))
+ self.SetSize((width, height))
+ self.SetSizeHints((width, height))
# Set value/type-specific formatting
self._applyFormatting()
self._SetInitialValue()
if self._autofit:
+ # this is tricky, because, as Robin explains:
+ # "Basically there are two sizes to deal with, that are potentially
+ # different. The client size is the inside size and may, depending
+ # on platform, exclude the borders and such. The normal size is
+ # the outside size that does include the borders. What you are
+ # calculating (in _CalcSize) is the client size, but the sizers
+ # deal with the full size and so that is the minimum size that
+ # we need to set with SetSizeHints. The root of the problem is
+ # that in _calcSize the current client size height is returned,
+ # instead of a height based on the current font. So I suggest using
+ # _calcSize to just get the width, and then use GetBestSize to
+ # get the height."
self.SetClientSize(self._CalcSize())
- self.SetSizeHints(self.GetSize())
+ width = self.GetSize().width
+ height = self.GetBestSize().height
+ self.SetSize((width, height))
+ self.SetSizeHints((width, height))
+
# Set value/type-specific formatting
self._applyFormatting()
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)
+ size = (w+4, self.GetSize().height)
#### dbg('size:', size, indent=0)
return size
## dbg('ignoring bogus text change event', indent=0)
pass
else:
-## dbg('curvalue: "%s", newvalue: "%s"' % (self._curValue, newvalue))
+## dbg('curvalue: "%s", newvalue: "%s", len(newvalue): %d' % (self._curValue, newvalue, len(newvalue)))
if self._Change():
if self._signOk and self._isNeg and newvalue.find('-') == -1 and newvalue.find('(') == -1:
## dbg('clearing self._isNeg')
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])
+ else:
+ wx.CallAfter(self._SetSelection, newpos, new_select_to)
keep_processing = False
elif keep_processing:
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."
+ Swallows the return, issues a Navigate event instead, since
+ masked controls are "single line" by defn.
"""
## dbg('MaskedEditMixin::OnReturn')
- event.m_keyCode = wx.WXK_TAB
- event.Skip()
+ self.Navigate(True)
+ return False
def _OnHome(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:
self._AdjustField(pos)
if event.GetKeyCode() == wx.WXK_TAB:
## dbg('tab to next ctrl')
- event.Skip()
+ # As of 2.5.2, you don't call event.Skip() to do
+ # this, but instead force explicit navigation, if
+ # wx.TE_PROCESS_TAB is used (like in the masked edits)
+ self.Navigate(True)
#else: do nothing
## dbg(indent=0)
return False
self._AdjustField(pos)
if event.GetKeyCode() == wx.WXK_TAB:
## dbg('tab to previous ctrl')
- event.Skip()
+ # As of 2.5.2, you don't call event.Skip() to do
+ # this, but instead force explicit navigation, if
+ # wx.TE_PROCESS_TAB is used (like in the masked edits)
+ self.Navigate(False)
else:
## dbg('position at beginning')
wx.CallAfter(self._SetInsertionPoint, field_start)
self._AdjustField(pos)
if event.GetKeyCode() == wx.WXK_TAB:
## dbg('tab to next ctrl')
- event.Skip()
+ # As of 2.5.2, you don't call event.Skip() to do
+ # this, but instead force explicit navigation, if
+ # wx.TE_PROCESS_TAB is used (like in the masked edits)
+ self.Navigate(True)
else:
## dbg('position at end')
wx.CallAfter(self._SetInsertionPoint, field_end)
self._AdjustField(pos)
if event.GetKeyCode() == wx.WXK_TAB:
## dbg('tab to next ctrl')
- event.Skip()
+ # As of 2.5.2, you don't call event.Skip() to do
+ # this, but instead force explicit navigation, if
+ # wx.TE_PROCESS_TAB is used (like in the masked edits)
+ self.Navigate(True)
#else: do nothing
## dbg(indent=0)
return False
if fraction._selectOnFieldEntry:
## dbg('queuing selection after decimal point to:', (start, end))
wx.CallAfter(self._SetSelection, start, end)
+ else:
+ wx.CallAfter(self._SetSelection, start, start)
keep_processing = False
if self._isInt: ## handle integer value, truncate from current position
if newstr.find(')') != -1:
newpos -= 1 # (don't move past right paren)
wx.CallAfter(self._SetInsertionPoint, newpos)
+ wx.CallAfter(self._SetSelection, newpos, newpos)
keep_processing = False
## dbg(indent=0)
pos = pos+2
if newvalue != value:
+## dbg('old value: "%s"\nnew value: "%s"' % (value, newvalue))
self._SetValue(newvalue)
self._SetInsertionPoint(pos)
self._SetInsertionPoint(pos)
if pos < sel_to: # restore selection
self._SetSelection(pos, sel_to)
+ else:
+ self._SetSelection(pos, pos)
## dbg('adjusted pos:', pos, indent=0)
return pos
the control, and deselect.
"""
## dbg('MaskedEditMixin::_fixSelection', indent=1)
- if not self._mask or not self._IsEditable():
+ # can get here if called with wx.CallAfter after underlying
+ # control has been destroyed on close, but after focus
+ # events
+ if not self or not self._mask or not self._IsEditable():
## dbg(indent=0)
return
## chars properly.)
## 4. Fixed autoselect behavior to work similarly to (2) above, so that combobox
## selection will only select the non-empty text, as per request.
+## 5. Fixed tabbing to work with 2.5.2 semantics.
+## 6. Fixed size calculation to handle changing fonts
##
## Version 1.6
## 1. Reorganized masked controls into separate package, renamed things accordingly
from wx.tools.dbg import Logger
from wx.lib.masked import MaskedEditMixin, Field, BaseMaskedTextCtrl
dbg = Logger()
-##dbg(enable=0)
+##dbg(enable=1)
#----------------------------------------------------------------------------
maskededit_kwargs['mask'] = intmask+fracmask
- if kwargs.has_key('groupChar'):
+ if kwargs.has_key('groupChar') or kwargs.has_key('decimalChar'):
old_groupchar = self._groupChar # save so we can reformat properly
-## dbg("old_groupchar: '%s'" % old_groupchar)
- maskededit_kwargs['groupChar'] = kwargs['groupChar']
- if kwargs.has_key('decimalChar'):
old_decimalchar = self._decimalChar
+## dbg("old_groupchar: '%s'" % old_groupchar)
## dbg("old_decimalchar: '%s'" % old_decimalchar)
- maskededit_kwargs['decimalChar'] = kwargs['decimalChar']
+ groupchar = old_groupchar
+ decimalchar = old_decimalchar
+
+ if kwargs.has_key('groupChar'):
+ maskededit_kwargs['groupChar'] = kwargs['groupChar']
+ groupchar = kwargs['groupChar']
+ if kwargs.has_key('decimalChar'):
+ maskededit_kwargs['decimalChar'] = kwargs['decimalChar']
+ decimalchar = kwargs['decimalChar']
+
+ # Add sanity check to make sure these are distinct, and if not,
+ # raise attribute error
+ if groupchar == decimalchar:
+ raise AttributeError('groupChar and decimalChar must be distinct')
+
# for all other parameters, assign keyword args as appropriate:
for key, param_value in kwargs.items():
and value[sel_start:sel_to] == self._groupChar ):
self.SetInsertionPoint(sel_start)
self.SetSelection(sel_start, sel_to+1)
-
- return BaseMaskedTextCtrl._OnErase(self, event, just_return_value)
## dbg(indent=0)
+ return BaseMaskedTextCtrl._OnErase(self, event, just_return_value)
def OnTextChange( self, event ):
A ValueError exception will be raised if an invalid value
is specified.
"""
+## dbg('NumCtrl::SetValue(%s)' % value, indent=1)
BaseMaskedTextCtrl.SetValue( self, self._toGUI(value) )
+## dbg(indent=0)
def SetIntegerWidth(self, value):
Preprocessor for base control paste; if value needs to be right-justified
to fit in control, do so prior to paste:
"""
-## dbg('NumCtrl::_Paste (value = "%s")' % value)
+## dbg('NumCtrl::_Paste (value = "%s")' % value, indent=1)
if value is None:
paste_text = self._getClipboardContents()
else:
#
field = self._FindField(sel_start)
edit_start, edit_end = field._extent
- paste_text = paste_text.replace(self._groupChar, '').replace(self._decimalChar, '.').replace('(', '-').replace(')','')
+ paste_text = paste_text.replace(self._groupChar, '').replace('(', '-').replace(')','')
if field._insertRight and self._groupDigits:
# want to paste to the left; see if it will fit:
left_text = old_value[edit_start:sel_start].lstrip()
sel_start += sel_to - orig_sel_start # decrease by amount selected
else:
## dbg("won't fit left;", 'paste text remains: "%s"' % paste_text)
-## dbg('adjusted start before accounting for grouping:', sel_start)
-## dbg('adjusted paste_text before accounting for grouping: "%s"' % paste_text)
+## dbg('adjusted start before accounting for grouping:', sel_start)
+## dbg('adjusted paste_text before accounting for grouping: "%s"' % paste_text)
pass
if self._groupDigits and sel_start != orig_sel_start:
left_len = len(old_value[:sel_to].lstrip())
self.SetInsertionPoint(sel_to)
self.SetSelection(sel_start, sel_to)
-## # treat paste as "replace number", if appropriate:
-## sel_start, sel_to = self._GetSelection()
-## if sel_start == sel_to or self._selectOnEntry and (sel_start, sel_to) == self._fields[0]._extent:
-## paste_text = self._toGUI(paste_text)
-## self._SetSelection(0, len(self._mask))
-
new_text, replace_to = MaskedEditMixin._Paste(self,
paste_text,
raise_on_invalid=raise_on_invalid,
# be a good place to implement the 2.3 logger class
from wx.tools.dbg import Logger
dbg = Logger()
-##dbg(enable=0)
+##dbg(enable=1)
# ## TRICKY BIT: to avoid a ton of boiler-plate, and to
# ## automate the getter/setter generation for each valid
style=style, validator=validator,
name=name)
+ self._PostInit(setupEventHandling = setupEventHandling,
+ name=name, value=value,**kwargs )
+
+
+ def _PostInit(self,setupEventHandling=True,
+ name='maskedTextCtrl' , value='', **kwargs):
+
self.controlInitialized = True
MaskedEditMixin.__init__( self, name, **kwargs )
wx.CallAfter(self._SetSelection, replace_to, replace_to)
## dbg(indent=0)
+ def SetFont(self, *args, **kwargs):
+ """ Set the font, then recalculate control size, if appropriate. """
+ wx.TextCtrl.SetFont(self, *args, **kwargs)
+ if self._autofit:
+## dbg('calculated size:', self._CalcSize())
+ self.SetClientSize(self._CalcSize())
+ width = self.GetSize().width
+ height = self.GetBestSize().height
+## dbg('setting client size to:', (width, height))
+ self.SetSize((width, height))
+ self.SetSizeHints((width, height))
+
def Clear(self):
""" Blanks the current control value by replacing it with the default value."""
pass
+class PreMaskedTextCtrl( BaseMaskedTextCtrl, MaskedEditAccessorsMixin ):
+ """
+ This allows us to use XRC subclassing.
+ """
+ # This should really be wx.EVT_WINDOW_CREATE but it is not
+ # currently delivered for native controls on all platforms, so
+ # we'll use EVT_SIZE instead. It should happen shortly after the
+ # control is created as the control is set to its "best" size.
+ _firstEventType = wx.EVT_SIZE
+
+ def __init__(self):
+ pre = wx.PreTextCtrl()
+ self.PostCreate(pre)
+ self.Bind(self._firstEventType, self.OnCreate)
+
+
+ def OnCreate(self, evt):
+ self.Unbind(self._firstEventType)
+ self._PostInit()
+
+i=0
+## CHANGELOG:
+## ====================
+## Version 1.1
+## 1. Added .SetFont() method that properly resizes control
+## 2. Modified control to support construction via XRC mechanism.
<DL><PRE>
<B>TimeCtrl</B>(
parent, id = -1,
- <B>value</B> = '12:00:00 AM',
+ <B>value</B> = '00:00:00',
pos = wx.DefaultPosition,
size = wx.DefaultSize,
<B>style</B> = wxTE_PROCESS_TAB,
with SetValue() after instantiation of the control.)
<DL><B>size</B>
<DD>The size of the control will be automatically adjusted for 12/24 hour format
- if wx.DefaultSize is specified.
+ if wx.DefaultSize is specified. NOTE: due to a problem with wx.DateTime, if the
+ locale does not use 'AM/PM' for its values, the default format will automatically
+ change to 24 hour format, and an AttributeError will be thrown if a non-24 format
+ is specified.
<DT><B>style</B>
<DD>By default, TimeCtrl will process TAB events, by allowing tab to the
different cells within the control.
<DD>This parameter can be used instead of the fmt24hr and displaySeconds
parameters, respectively; it provides a shorthand way to specify the time
format you want. Accepted values are 'HHMMSS', 'HHMM', '24HHMMSS', and
- '24HHMM'. If the format is specified, the other two arguments will be ignored.
+ '24HHMM'. If the format is specified, the other two arguments will be ignored.
<BR>
<DT><B>fmt24hr</B>
<DD>If True, control will display time in 24 hour time format; if False, it will
}
def __init__ (
- self, parent, id=-1, value = '12:00:00 AM',
+ self, parent, id=-1, value = '00:00:00',
pos = wx.DefaultPosition, size = wx.DefaultSize,
fmt24hr=False,
spinButton = None,
# set defaults for control:
## dbg('setting defaults:')
+
+ self.__fmt24hr = False
+ wxdt = wx.DateTimeFromDMY(1, 0, 1970)
+ if wxdt.Format('%p') != 'AM':
+ TimeCtrl.valid_ctrl_params['format'] = '24HHMMSS'
+ self.__fmt24hr = True
+ fmt24hr = True # force/change default positional argument
+ # (will countermand explicit set to False too.)
+
for key, param_value in TimeCtrl.valid_ctrl_params.items():
# This is done this way to make setattr behave consistently with
# "private attribute" name mangling
kwargs['displaySeconds'] = True
# (handle positional arg (from original release) differently from rest of kwargs:)
- self.__fmt24hr = False
if not kwargs.has_key('format'):
if fmt24hr:
if kwargs.has_key('displaySeconds') and kwargs['displaySeconds']:
self.SetLimited(limited)
self.SetValue(value)
except:
- self.SetValue('12:00:00 AM')
+ self.SetValue('00:00:00')
if spinButton:
self.BindSpinButton(spinButton) # bind spin button up/down events to this control
raise AttributeError('invalid keyword argument "%s"' % key)
if key == 'format':
+ wxdt = wx.DateTimeFromDMY(1, 0, 1970)
+ if wxdt.Format('%p') != 'AM':
+ require24hr = True
+ else:
+ require24hr = False
+
# handle both local or generic 'maskededit' autoformat codes:
if param_value == 'HHMMSS' or param_value == 'TIMEHHMMSS':
self.__displaySeconds = True
self.__fmt24hr = True
else:
raise AttributeError('"%s" is not a valid format' % param_value)
+
+ if require24hr and not self.__fmt24hr:
+ raise AttributeError('"%s" is an unsupported time format for the current locale' % param_value)
+
reset_format = True
elif key in ("displaySeconds", "display_seconds") and not kwargs.has_key('format'):
self.SetLimited(limited)
self.SetValue(value)
except:
- self.SetValue('12:00:00 AM')
+ self.SetValue('00:00:00')
## dbg(indent=0)
return {} # no arguments to return
else:
## dbg('checkTime == len(value)?', valid)
if not valid:
+ # deal with bug/deficiency in wx.DateTime:
+ if wxdt.Format('%p') not in ('AM', 'PM') and checkTime in (5,8):
+ # couldn't parse the AM/PM field
+ raise ValueError('cannot convert string "%s" to valid time for the current locale; please use 24hr time instead' % value)
+ else:
## dbg(indent=0, suspend=0)
- raise ValueError('cannot convert string "%s" to valid time' % value)
+ raise ValueError('cannot convert string "%s" to valid time' % value)
else:
if isinstance(value, wx.DateTime):