1 #---------------------------------------------------------------------------- 
   2 # Name:         masked.combobox.py 
   4 # Email:        wsadkin@nameconnector.com 
   6 # Copyright:    (c) 2003 by Will Sadkin, 2003 
   8 # License:      wxWidgets license 
   9 #---------------------------------------------------------------------------- 
  11 # This masked edit class allows for the semantics of masked controls 
  12 # to be applied to combo boxes. 
  14 #---------------------------------------------------------------------------- 
  17 from wx
.lib
.masked 
import * 
  19 # jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would 
  20 # be a good place to implement the 2.3 logger class 
  21 from wx
.tools
.dbg 
import Logger
 
  25 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
  26 ## Because calling SetSelection programmatically does not fire EVT_COMBOBOX 
  27 ## events, we have to do it ourselves when we auto-complete. 
  28 class MaskedComboBoxSelectEvent(wx
.PyCommandEvent
): 
  29     def __init__(self
, id, selection 
= 0, object=None): 
  30         wx
.PyCommandEvent
.__init
__(self
, wx
.wxEVT_COMMAND_COMBOBOX_SELECTED
, id) 
  32         self
.__selection 
= selection
 
  33         self
.SetEventObject(object) 
  35     def GetSelection(self
): 
  36         """Retrieve the value of the control at the time 
  37         this event was generated.""" 
  38         return self
.__selection
 
  41 class BaseMaskedComboBox( wx
.ComboBox
, MaskedEditMixin 
): 
  43     This masked edit control adds the ability to use a masked input 
  44     on a combobox, and do auto-complete of such values. 
  46     def __init__( self
, parent
, id=-1, value 
= '', 
  47                   pos 
= wx
.DefaultPosition
, 
  48                   size 
= wx
.DefaultSize
, 
  50                   style 
= wx
.CB_DROPDOWN
, 
  51                   validator 
= wx
.DefaultValidator
, 
  52                   name 
= "maskedComboBox", 
  53                   setupEventHandling 
= True,        ## setup event handling by default): 
  57         # This is necessary, because wxComboBox currently provides no 
  58         # method for determining later if this was specified in the 
  59         # constructor for the control... 
  60         self
.__readonly 
= style 
& wx
.CB_READONLY 
== wx
.CB_READONLY
 
  62         kwargs
['choices'] = choices                 
## set up maskededit to work with choice list too 
  64         ## Since combobox completion is case-insensitive, always validate same way 
  65         if not kwargs
.has_key('compareNoCase'): 
  66             kwargs
['compareNoCase'] = True 
  68         MaskedEditMixin
.__init
__( self
, name
, **kwargs 
) 
  70         self
._choices 
= self
._ctrl
_constraints
._choices
 
  71 ##        dbg('self._choices:', self._choices) 
  73         if self
._ctrl
_constraints
._alignRight
: 
  74             choices 
= [choice
.rjust(self
._masklength
) for choice 
in choices
] 
  76             choices 
= [choice
.ljust(self
._masklength
) for choice 
in choices
] 
  78         wx
.ComboBox
.__init
__(self
, parent
, id, value
='', 
  80                             choices
=choices
, style
=style|wx
.WANTS_CHARS
, 
  84         self
.controlInitialized 
= True 
  86         # Set control font - fixed width by default 
  90             self
.SetClientSize(self
._CalcSize
()) 
  93             # ensure value is width of the mask of the control: 
  94             if self
._ctrl
_constraints
._alignRight
: 
  95                 value 
= value
.rjust(self
._masklength
) 
  97                 value 
= value
.ljust(self
._masklength
) 
 100             self
.SetStringSelection(value
) 
 102             self
._SetInitialValue
(value
) 
 105         self
._SetKeycodeHandler
(wx
.WXK_UP
, self
.OnSelectChoice
) 
 106         self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
.OnSelectChoice
) 
 108         if setupEventHandling
: 
 109             ## Setup event handlers 
 110             self
.Bind(wx
.EVT_SET_FOCUS
, self
._OnFocus 
)         ## defeat automatic full selection 
 111             self
.Bind(wx
.EVT_KILL_FOCUS
, self
._OnKillFocus 
)    ## run internal validator 
 112             self
.Bind(wx
.EVT_LEFT_DCLICK
, self
._OnDoubleClick
)  ## select field under cursor on dclick 
 113             self
.Bind(wx
.EVT_RIGHT_UP
, self
._OnContextMenu 
)    ## bring up an appropriate context menu 
 114             self
.Bind(wx
.EVT_CHAR
, self
._OnChar 
)               ## handle each keypress 
 115             self
.Bind(wx
.EVT_KEY_DOWN
, self
.OnKeyDown 
)         ## for special processing of up/down keys 
 116             self
.Bind(wx
.EVT_KEY_DOWN
, self
._OnKeyDown 
)        ## for processing the rest of the control keys 
 117                                                                 ## (next in evt chain) 
 118             self
.Bind(wx
.EVT_TEXT
, self
._OnTextChange 
)         ## color control appropriately & keep 
 119                                                                 ## track of previous value for undo 
 124         return "<MaskedComboBox: %s>" % self
.GetValue() 
 127     def _CalcSize(self
, size
=None): 
 129         Calculate automatic size if allowed; augment base mixin function 
 130         to account for the selector button. 
 132         size 
= self
._calcSize
(size
) 
 133         return (size
[0]+20, size
[1]) 
 136     def _GetSelection(self
): 
 138         Allow mixin to get the text selection of this control. 
 139         REQUIRED by any class derived from MaskedEditMixin. 
 141         return self
.GetMark() 
 143     def _SetSelection(self
, sel_start
, sel_to
): 
 145         Allow mixin to set the text selection of this control. 
 146         REQUIRED by any class derived from MaskedEditMixin. 
 148         return self
.SetMark( sel_start
, sel_to 
) 
 151     def _GetInsertionPoint(self
): 
 152         return self
.GetInsertionPoint() 
 154     def _SetInsertionPoint(self
, pos
): 
 155         self
.SetInsertionPoint(pos
) 
 160         Allow mixin to get the raw value of the control with this function. 
 161         REQUIRED by any class derived from MaskedEditMixin. 
 163         return self
.GetValue() 
 165     def _SetValue(self
, value
): 
 167         Allow mixin to set the raw value of the control with this function. 
 168         REQUIRED by any class derived from MaskedEditMixin. 
 170         # For wxComboBox, ensure that values are properly padded so that 
 171         # if varying length choices are supplied, they always show up 
 172         # in the window properly, and will be the appropriate length 
 174         if self
._ctrl
_constraints
._alignRight
: 
 175             value 
= value
.rjust(self
._masklength
) 
 177             value 
= value
.ljust(self
._masklength
) 
 179         # Record current selection and insertion point, for undo 
 180         self
._prevSelection 
= self
._GetSelection
() 
 181         self
._prevInsertionPoint 
= self
._GetInsertionPoint
() 
 182         wx
.ComboBox
.SetValue(self
, value
) 
 183         # text change events don't always fire, so we check validity here 
 184         # to make certain formatting is applied: 
 187     def SetValue(self
, value
): 
 189         This function redefines the externally accessible .SetValue to be 
 190         a smart "paste" of the text in question, so as not to corrupt the 
 191         masked control.  NOTE: this must be done in the class derived 
 192         from the base wx control. 
 195             wx
.ComboBox
.SetValue(value
)   # revert to base control behavior 
 198         # empty previous contents, replacing entire value: 
 199         self
._SetInsertionPoint
(0) 
 200         self
._SetSelection
(0, self
._masklength
) 
 202         if( len(value
) < self
._masklength                
# value shorter than control 
 203             and (self
._isFloat 
or self
._isInt
)            # and it's a numeric control 
 204             and self
._ctrl
_constraints
._alignRight 
):   # and it's a right-aligned control 
 205             # try to intelligently "pad out" the value to the right size: 
 206             value 
= self
._template
[0:self
._masklength 
- len(value
)] + value
 
 207 ##            dbg('padded value = "%s"' % value) 
 209         # For wxComboBox, ensure that values are properly padded so that 
 210         # if varying length choices are supplied, they always show up 
 211         # in the window properly, and will be the appropriate length 
 213         elif self
._ctrl
_constraints
._alignRight
: 
 214             value 
= value
.rjust(self
._masklength
) 
 216             value 
= value
.ljust(self
._masklength
) 
 219         # make SetValue behave the same as if you had typed the value in: 
 221             value 
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True) 
 223                 self
._isNeg 
= False     # (clear current assumptions) 
 224                 value 
= self
._adjustFloat
(value
) 
 226                 self
._isNeg 
= False     # (clear current assumptions) 
 227                 value 
= self
._adjustInt
(value
) 
 228             elif self
._isDate 
and not self
.IsValid(value
) and self
._4digityear
: 
 229                 value 
= self
._adjustDate
(value
, fixcentury
=True) 
 231             # If date, year might be 2 digits vs. 4; try adjusting it: 
 232             if self
._isDate 
and self
._4digityear
: 
 233                 dateparts 
= value
.split(' ') 
 234                 dateparts
[0] = self
._adjustDate
(dateparts
[0], fixcentury
=True) 
 235                 value 
= string
.join(dateparts
, ' ') 
 236 ##                dbg('adjusted value: "%s"' % value) 
 237                 value 
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True) 
 241         self
._SetValue
(value
) 
 242 ####        dbg('queuing insertion after .SetValue', self._masklength) 
 243         wx
.CallAfter(self
._SetInsertionPoint
, self
._masklength
) 
 244         wx
.CallAfter(self
._SetSelection
, self
._masklength
, self
._masklength
) 
 249         Allow mixin to refresh the base control with this function. 
 250         REQUIRED by any class derived from MaskedEditMixin. 
 252         wx
.ComboBox
.Refresh(self
) 
 256         This function redefines the externally accessible .Refresh() to 
 257         validate the contents of the masked control as it refreshes. 
 258         NOTE: this must be done in the class derived from the base wx control. 
 264     def _IsEditable(self
): 
 266         Allow mixin to determine if the base control is editable with this function. 
 267         REQUIRED by any class derived from MaskedEditMixin. 
 269         return not self
.__readonly
 
 274         This function redefines the externally accessible .Cut to be 
 275         a smart "erase" of the text in question, so as not to corrupt the 
 276         masked control.  NOTE: this must be done in the class derived 
 277         from the base wx control. 
 280             self
._Cut
()             # call the mixin's Cut method 
 282             wx
.ComboBox
.Cut(self
)    # else revert to base control behavior 
 287         This function redefines the externally accessible .Paste to be 
 288         a smart "paste" of the text in question, so as not to corrupt the 
 289         masked control.  NOTE: this must be done in the class derived 
 290         from the base wx control. 
 293             self
._Paste
()           # call the mixin's Paste method 
 295             wx
.ComboBox
.Paste(self
)  # else revert to base control behavior 
 300         This function defines the undo operation for the control. (The default 
 306             wx
.ComboBox
.Undo()       # else revert to base control behavior 
 309     def Append( self
, choice
, clientData
=None ): 
 311         This function override is necessary so we can keep track of any additions to the list 
 312         of choices, because wxComboBox doesn't have an accessor for the choice list. 
 313         The code here is the same as in the SetParameters() mixin function, but is 
 314         done for the individual value as appended, so the list can be built incrementally 
 315         without speed penalty. 
 318             if type(choice
) not in (types
.StringType
, types
.UnicodeType
): 
 319                 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
)) 
 320             elif not self
.IsValid(choice
): 
 321                 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
)) 
 323             if not self
._ctrl
_constraints
._choices
: 
 324                 self
._ctrl
_constraints
._compareChoices 
= [] 
 325                 self
._ctrl
_constraints
._choices 
= [] 
 328             compareChoice 
= choice
.strip() 
 330             if self
._ctrl
_constraints
._compareNoCase
: 
 331                 compareChoice 
= compareChoice
.lower() 
 333             if self
._ctrl
_constraints
._alignRight
: 
 334                 choice 
= choice
.rjust(self
._masklength
) 
 336                 choice 
= choice
.ljust(self
._masklength
) 
 337             if self
._ctrl
_constraints
._fillChar 
!= ' ': 
 338                 choice 
= choice
.replace(' ', self
._fillChar
) 
 339 ##            dbg('updated choice:', choice) 
 342             self
._ctrl
_constraints
._compareChoices
.append(compareChoice
) 
 343             self
._ctrl
_constraints
._choices
.append(choice
) 
 344             self
._choices 
= self
._ctrl
_constraints
._choices     
# (for shorthand) 
 346             if( not self
.IsValid(choice
) and 
 347                (not self
._ctrl
_constraints
.IsEmpty(choice
) or 
 348                 (self
._ctrl
_constraints
.IsEmpty(choice
) and self
._ctrl
_constraints
._validRequired
) ) ): 
 349                 raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice
, self
.name
)) 
 351         wx
.ComboBox
.Append(self
, choice
, clientData
) 
 357         This function override is necessary so we can keep track of any additions to the list 
 358         of choices, because wxComboBox doesn't have an accessor for the choice list. 
 362             self
._ctrl
_constraints
._autoCompleteIndex 
= -1 
 363             if self
._ctrl
_constraints
._choices
: 
 364                 self
.SetCtrlParameters(choices
=[]) 
 365         wx
.ComboBox
.Clear(self
) 
 368     def _OnCtrlParametersChanged(self
): 
 370         Override mixin's default OnCtrlParametersChanged to detect changes in choice list, so 
 371         we can update the base control: 
 373         if self
.controlInitialized 
and self
._choices 
!= self
._ctrl
_constraints
._choices
: 
 374             wx
.ComboBox
.Clear(self
) 
 375             self
._choices 
= self
._ctrl
_constraints
._choices
 
 376             for choice 
in self
._choices
: 
 377                 wx
.ComboBox
.Append( self
, choice 
) 
 382         This function is a hack to make up for the fact that wxComboBox has no 
 383         method for returning the selected portion of its edit control.  It 
 384         works, but has the nasty side effect of generating lots of intermediate 
 387 ##        dbg(suspend=1)  # turn off debugging around this function 
 388 ##        dbg('MaskedComboBox::GetMark', indent=1) 
 391             return 0, 0 # no selection possible for editing 
 392 ##        sel_start, sel_to = wxComboBox.GetMark(self)        # what I'd *like* to have! 
 393         sel_start 
= sel_to 
= self
.GetInsertionPoint() 
 394 ##        dbg("current sel_start:", sel_start) 
 395         value 
= self
.GetValue() 
 396 ##        dbg('value: "%s"' % value) 
 398         self
._ignoreChange 
= True               # tell _OnTextChange() to ignore next event (if any) 
 400         wx
.ComboBox
.Cut(self
) 
 401         newvalue 
= self
.GetValue() 
 402 ##        dbg("value after Cut operation:", newvalue) 
 404         if newvalue 
!= value
:                   # something was selected; calculate extent 
 405 ##            dbg("something selected") 
 406             sel_to 
= sel_start 
+ len(value
) - len(newvalue
) 
 407             wx
.ComboBox
.SetValue(self
, value
)    # restore original value and selection (still ignoring change) 
 408             wx
.ComboBox
.SetInsertionPoint(self
, sel_start
) 
 409             wx
.ComboBox
.SetMark(self
, sel_start
, sel_to
) 
 411         self
._ignoreChange 
= False              # tell _OnTextChange() to pay attn again 
 413 ##        dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0) 
 414         return sel_start
, sel_to
 
 417     def SetSelection(self
, index
): 
 419         Necessary for bookkeeping on choice selection, to keep current value 
 422 ##        dbg('MaskedComboBox::SetSelection(%d)' % index) 
 424             self
._prevValue 
= self
._curValue
 
 425             self
._curValue 
= self
._choices
[index
] 
 426             self
._ctrl
_constraints
._autoCompleteIndex 
= index
 
 427         wx
.ComboBox
.SetSelection(self
, index
) 
 430     def OnKeyDown(self
, event
): 
 432         This function is necessary because navigation and control key 
 433         events do not seem to normally be seen by the wxComboBox's 
 434         EVT_CHAR routine.  (Tabs don't seem to be visible no matter 
 437         if event
.GetKeyCode() in self
._nav 
+ self
._control
: 
 441             event
.Skip()    # let mixin default KeyDown behavior occur 
 444     def OnSelectChoice(self
, event
): 
 446         This function appears to be necessary, because the processing done 
 447         on the text of the control somehow interferes with the combobox's 
 448         selection mechanism for the arrow keys. 
 450 ##        dbg('MaskedComboBox::OnSelectChoice', indent=1) 
 456         value 
= self
.GetValue().strip() 
 458         if self
._ctrl
_constraints
._compareNoCase
: 
 459             value 
= value
.lower() 
 461         if event
.GetKeyCode() == wx
.WXK_UP
: 
 465         match_index
, partial_match 
= self
._autoComplete
( 
 467                                                 self
._ctrl
_constraints
._compareChoices
, 
 469                                                 self
._ctrl
_constraints
._compareNoCase
, 
 470                                                 current_index 
= self
._ctrl
_constraints
._autoCompleteIndex
) 
 471         if match_index 
is not None: 
 472 ##            dbg('setting selection to', match_index) 
 473             # issue appropriate event to outside: 
 474             self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=match_index
) 
 476             keep_processing 
= False 
 478             pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
 479             field 
= self
._FindField
(pos
) 
 480             if self
.IsEmpty() or not field
._hasList
: 
 481 ##                dbg('selecting 1st value in list') 
 482                 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=0) 
 484                 keep_processing 
= False 
 486                 # attempt field-level auto-complete 
 488                 keep_processing 
= self
._OnAutoCompleteField
(event
) 
 489 ##        dbg('keep processing?', keep_processing, indent=0) 
 490         return keep_processing
 
 493     def _OnAutoSelect(self
, field
, match_index
): 
 495         Override mixin (empty) autocomplete handler, so that autocompletion causes 
 496         combobox to update appropriately. 
 498 ##        dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1) 
 499 ##        field._autoCompleteIndex = match_index 
 500         if field 
== self
._ctrl
_constraints
: 
 501             self
.SetSelection(match_index
) 
 502 ##            dbg('issuing combo selection event') 
 503             self
.GetEventHandler().ProcessEvent( 
 504                 MaskedComboBoxSelectEvent( self
.GetId(), match_index
, self 
) ) 
 506 ##        dbg('field._autoCompleteIndex:', match_index) 
 507 ##        dbg('self.GetSelection():', self.GetSelection()) 
 511     def _OnReturn(self
, event
): 
 513         For wxComboBox, it seems that if you hit return when the dropdown is 
 514         dropped, the event that dismisses the dropdown will also blank the 
 515         control, because of the implementation of wxComboBox.  So here, 
 516         we look and if the selection is -1, and the value according to 
 517         (the base control!) is a value in the list, then we schedule a 
 518         programmatic wxComboBox.SetSelection() call to pick the appropriate 
 519         item in the list. (and then do the usual OnReturn bit.) 
 521 ##        dbg('MaskedComboBox::OnReturn', indent=1) 
 522 ##        dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection()) 
 523         if self
.GetSelection() == -1 and self
.GetValue().lower().strip() in self
._ctrl
_constraints
._compareChoices
: 
 524             wx
.CallAfter(self
.SetSelection
, self
._ctrl
_constraints
._autoCompleteIndex
) 
 526         event
.m_keyCode 
= wx
.WXK_TAB
 
 531 class ComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin 
): 
 533     This extra level of inheritance allows us to add the generic set of 
 534     masked edit parameters only to this class while allowing other 
 535     classes to derive from the "base" masked combobox control, and provide 
 536     a smaller set of valid accessor functions.