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 Provides masked edit capabilities within a ComboBox format, as well as 
  18 a base class from which you can derive masked comboboxes tailored to a specific 
  19 function.  See maskededit module overview for how to configure the control. 
  22 import  wx
, types
, string
 
  23 from wx
.lib
.masked 
import * 
  25 # jmg 12/9/03 - when we cut ties with Py 2.2 and earlier, this would 
  26 # be a good place to implement the 2.3 logger class 
  27 from wx
.tools
.dbg 
import Logger
 
  31 ## ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
  32 ## Because calling SetSelection programmatically does not fire EVT_COMBOBOX 
  33 ## events, we have to do it ourselves when we auto-complete. 
  34 class MaskedComboBoxSelectEvent(wx
.PyCommandEvent
): 
  36     Because calling SetSelection programmatically does not fire EVT_COMBOBOX 
  37     events, the derived control has to do it itself when it auto-completes. 
  39     def __init__(self
, id, selection 
= 0, object=None): 
  40         wx
.PyCommandEvent
.__init
__(self
, wx
.wxEVT_COMMAND_COMBOBOX_SELECTED
, id) 
  42         self
.__selection 
= selection
 
  43         self
.SetEventObject(object) 
  45     def GetSelection(self
): 
  46         """Retrieve the value of the control at the time 
  47         this event was generated.""" 
  48         return self
.__selection
 
  51 class BaseMaskedComboBox( wx
.ComboBox
, MaskedEditMixin 
): 
  53     Base class for generic masked edit comboboxes; allows auto-complete of values. 
  54     It is not meant to be instantiated directly, but rather serves as a base class 
  55     for any subsequent refinements. 
  57     def __init__( self
, parent
, id=-1, value 
= '', 
  58                   pos 
= wx
.DefaultPosition
, 
  59                   size 
= wx
.DefaultSize
, 
  61                   style 
= wx
.CB_DROPDOWN
, 
  62                   validator 
= wx
.DefaultValidator
, 
  63                   name 
= "maskedComboBox", 
  64                   setupEventHandling 
= True,        ## setup event handling by default): 
  68         kwargs
['choices'] = choices                 
## set up maskededit to work with choice list too 
  70         ## Since combobox completion is case-insensitive, always validate same way 
  71         if not kwargs
.has_key('compareNoCase'): 
  72             kwargs
['compareNoCase'] = True 
  74         MaskedEditMixin
.__init
__( self
, name
, **kwargs 
) 
  76         self
._choices 
= self
._ctrl
_constraints
._choices
 
  77 ##        dbg('self._choices:', self._choices) 
  79         if self
._ctrl
_constraints
._alignRight
: 
  80             choices 
= [choice
.rjust(self
._masklength
) for choice 
in choices
] 
  82             choices 
= [choice
.ljust(self
._masklength
) for choice 
in choices
] 
  84         wx
.ComboBox
.__init
__(self
, parent
, id, value
='', 
  86                             choices
=choices
, style
=style|wx
.WANTS_CHARS
, 
  89         self
.controlInitialized 
= True 
  91         self
._PostInit
(style
=style
, setupEventHandling
=setupEventHandling
, 
  92                        name
=name
, value
=value
, **kwargs
) 
  95     def _PostInit(self
, style
=wx
.CB_DROPDOWN
, 
  96                   setupEventHandling 
= True,        ## setup event handling by default): 
  97                   name 
= "maskedComboBox", value
='', **kwargs
): 
  99         # This is necessary, because wxComboBox currently provides no 
 100         # method for determining later if this was specified in the 
 101         # constructor for the control... 
 102         self
.__readonly 
= style 
& wx
.CB_READONLY 
== wx
.CB_READONLY
 
 104         if not hasattr(self
, 'controlInitialized'): 
 106             self
.controlInitialized 
= True          ## must have been called via XRC, therefore base class is constructed 
 107             if not kwargs
.has_key('choices'): 
 109                 kwargs
['choices'] = choices         
## set up maskededit to work with choice list too 
 112             ## Since combobox completion is case-insensitive, always validate same way 
 113             if not kwargs
.has_key('compareNoCase'): 
 114                 kwargs
['compareNoCase'] = True 
 116             MaskedEditMixin
.__init
__( self
, name
, **kwargs 
) 
 118             self
._choices 
= self
._ctrl
_constraints
._choices
 
 119 ##        dbg('self._choices:', self._choices) 
 121             if self
._ctrl
_constraints
._alignRight
: 
 122                 choices 
= [choice
.rjust(self
._masklength
) for choice 
in choices
] 
 124                 choices 
= [choice
.ljust(self
._masklength
) for choice 
in choices
] 
 125             wx
.ComboBox
.Clear(self
) 
 126             wx
.ComboBox
.AppendItems(self
, choices
) 
 129         # Set control font - fixed width by default 
 133             self
.SetClientSize(self
._CalcSize
()) 
 134             width 
= self
.GetSize().width
 
 135             height 
= self
.GetBestSize().height
 
 136             self
.SetBestFittingSize((width
, height
)) 
 140             # ensure value is width of the mask of the control: 
 141             if self
._ctrl
_constraints
._alignRight
: 
 142                 value 
= value
.rjust(self
._masklength
) 
 144                 value 
= value
.ljust(self
._masklength
) 
 147             self
.SetStringSelection(value
) 
 149             self
._SetInitialValue
(value
) 
 152         self
._SetKeycodeHandler
(wx
.WXK_UP
, self
._OnSelectChoice
) 
 153         self
._SetKeycodeHandler
(wx
.WXK_DOWN
, self
._OnSelectChoice
) 
 155         if setupEventHandling
: 
 156             ## Setup event handlers 
 157             self
.Bind(wx
.EVT_SET_FOCUS
, self
._OnFocus 
)         ## defeat automatic full selection 
 158             self
.Bind(wx
.EVT_KILL_FOCUS
, self
._OnKillFocus 
)    ## run internal validator 
 159             self
.Bind(wx
.EVT_LEFT_DCLICK
, self
._OnDoubleClick
)  ## select field under cursor on dclick 
 160             self
.Bind(wx
.EVT_RIGHT_UP
, self
._OnContextMenu 
)    ## bring up an appropriate context menu 
 161             self
.Bind(wx
.EVT_CHAR
, self
._OnChar 
)               ## handle each keypress 
 162             self
.Bind(wx
.EVT_KEY_DOWN
, self
._OnKeyDownInComboBox 
) ## for special processing of up/down keys 
 163             self
.Bind(wx
.EVT_KEY_DOWN
, self
._OnKeyDown 
)        ## for processing the rest of the control keys 
 164                                                                 ## (next in evt chain) 
 165             self
.Bind(wx
.EVT_TEXT
, self
._OnTextChange 
)         ## color control appropriately & keep 
 166                                                                 ## track of previous value for undo 
 171         return "<MaskedComboBox: %s>" % self
.GetValue() 
 174     def _CalcSize(self
, size
=None): 
 176         Calculate automatic size if allowed; augment base mixin function 
 177         to account for the selector button. 
 179         size 
= self
._calcSize
(size
) 
 180         return (size
[0]+20, size
[1]) 
 183     def SetFont(self
, *args
, **kwargs
): 
 184         """ Set the font, then recalculate control size, if appropriate. """ 
 185         wx
.ComboBox
.SetFont(self
, *args
, **kwargs
) 
 187 ##            dbg('calculated size:', self._CalcSize())             
 188             self
.SetClientSize(self
._CalcSize
()) 
 189             width 
= self
.GetSize().width
 
 190             height 
= self
.GetBestSize().height
 
 191 ##            dbg('setting client size to:', (width, height)) 
 192             self
.SetBestFittingSize((width
, height
)) 
 195     def _GetSelection(self
): 
 197         Allow mixin to get the text selection of this control. 
 198         REQUIRED by any class derived from MaskedEditMixin. 
 200         return self
.GetMark() 
 202     def _SetSelection(self
, sel_start
, sel_to
): 
 204         Allow mixin to set the text selection of this control. 
 205         REQUIRED by any class derived from MaskedEditMixin. 
 207         return self
.SetMark( sel_start
, sel_to 
) 
 210     def _GetInsertionPoint(self
): 
 211         return self
.GetInsertionPoint() 
 213     def _SetInsertionPoint(self
, pos
): 
 214         self
.SetInsertionPoint(pos
) 
 219         Allow mixin to get the raw value of the control with this function. 
 220         REQUIRED by any class derived from MaskedEditMixin. 
 222         return self
.GetValue() 
 224     def _SetValue(self
, value
): 
 226         Allow mixin to set the raw value of the control with this function. 
 227         REQUIRED by any class derived from MaskedEditMixin. 
 229         # For wxComboBox, ensure that values are properly padded so that 
 230         # if varying length choices are supplied, they always show up 
 231         # in the window properly, and will be the appropriate length 
 233         if self
._ctrl
_constraints
._alignRight
: 
 234             value 
= value
.rjust(self
._masklength
) 
 236             value 
= value
.ljust(self
._masklength
) 
 238         # Record current selection and insertion point, for undo 
 239         self
._prevSelection 
= self
._GetSelection
() 
 240         self
._prevInsertionPoint 
= self
._GetInsertionPoint
() 
 241         wx
.ComboBox
.SetValue(self
, value
) 
 242         # text change events don't always fire, so we check validity here 
 243         # to make certain formatting is applied: 
 246     def SetValue(self
, value
): 
 248         This function redefines the externally accessible .SetValue to be 
 249         a smart "paste" of the text in question, so as not to corrupt the 
 250         masked control.  NOTE: this must be done in the class derived 
 251         from the base wx control. 
 254             wx
.ComboBox
.SetValue(value
)   # revert to base control behavior 
 257         # empty previous contents, replacing entire value: 
 258         self
._SetInsertionPoint
(0) 
 259         self
._SetSelection
(0, self
._masklength
) 
 261         if( len(value
) < self
._masklength                
# value shorter than control 
 262             and (self
._isFloat 
or self
._isInt
)            # and it's a numeric control 
 263             and self
._ctrl
_constraints
._alignRight 
):   # and it's a right-aligned control 
 264             # try to intelligently "pad out" the value to the right size: 
 265             value 
= self
._template
[0:self
._masklength 
- len(value
)] + value
 
 266 ##            dbg('padded value = "%s"' % value) 
 268         # For wxComboBox, ensure that values are properly padded so that 
 269         # if varying length choices are supplied, they always show up 
 270         # in the window properly, and will be the appropriate length 
 272         elif self
._ctrl
_constraints
._alignRight
: 
 273             value 
= value
.rjust(self
._masklength
) 
 275             value 
= value
.ljust(self
._masklength
) 
 278         # make SetValue behave the same as if you had typed the value in: 
 280             value
, replace_to 
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True) 
 282                 self
._isNeg 
= False     # (clear current assumptions) 
 283                 value 
= self
._adjustFloat
(value
) 
 285                 self
._isNeg 
= False     # (clear current assumptions) 
 286                 value 
= self
._adjustInt
(value
) 
 287             elif self
._isDate 
and not self
.IsValid(value
) and self
._4digityear
: 
 288                 value 
= self
._adjustDate
(value
, fixcentury
=True) 
 290             # If date, year might be 2 digits vs. 4; try adjusting it: 
 291             if self
._isDate 
and self
._4digityear
: 
 292                 dateparts 
= value
.split(' ') 
 293                 dateparts
[0] = self
._adjustDate
(dateparts
[0], fixcentury
=True) 
 294                 value 
= string
.join(dateparts
, ' ') 
 295 ##                dbg('adjusted value: "%s"' % value) 
 296                 value 
= self
._Paste
(value
, raise_on_invalid
=True, just_return_value
=True) 
 300         self
._SetValue
(value
) 
 301 ####        dbg('queuing insertion after .SetValue', replace_to) 
 302         wx
.CallAfter(self
._SetInsertionPoint
, replace_to
) 
 303         wx
.CallAfter(self
._SetSelection
, replace_to
, replace_to
) 
 308         Allow mixin to refresh the base control with this function. 
 309         REQUIRED by any class derived from MaskedEditMixin. 
 311         wx
.ComboBox
.Refresh(self
) 
 315         This function redefines the externally accessible .Refresh() to 
 316         validate the contents of the masked control as it refreshes. 
 317         NOTE: this must be done in the class derived from the base wx control. 
 323     def _IsEditable(self
): 
 325         Allow mixin to determine if the base control is editable with this function. 
 326         REQUIRED by any class derived from MaskedEditMixin. 
 328         return not self
.__readonly
 
 333         This function redefines the externally accessible .Cut to be 
 334         a smart "erase" of the text in question, so as not to corrupt the 
 335         masked control.  NOTE: this must be done in the class derived 
 336         from the base wx control. 
 339             self
._Cut
()             # call the mixin's Cut method 
 341             wx
.ComboBox
.Cut(self
)    # else revert to base control behavior 
 346         This function redefines the externally accessible .Paste to be 
 347         a smart "paste" of the text in question, so as not to corrupt the 
 348         masked control.  NOTE: this must be done in the class derived 
 349         from the base wx control. 
 352             self
._Paste
()           # call the mixin's Paste method 
 354             wx
.ComboBox
.Paste(self
)  # else revert to base control behavior 
 359         This function defines the undo operation for the control. (The default 
 365             wx
.ComboBox
.Undo()       # else revert to base control behavior 
 367     def Append( self
, choice
, clientData
=None ): 
 369         This base control function override is necessary so the control can keep track 
 370         of any additions to the list of choices, because wx.ComboBox doesn't have an 
 371         accessor for the choice list.  The code here is the same as in the 
 372         SetParameters() mixin function, but is done for the individual value 
 373         as appended, so the list can be built incrementally without speed penalty. 
 376             if type(choice
) not in (types
.StringType
, types
.UnicodeType
): 
 377                 raise TypeError('%s: choices must be a sequence of strings' % str(self
._index
)) 
 378             elif not self
.IsValid(choice
): 
 379                 raise ValueError('%s: "%s" is not a valid value for the control as specified.' % (str(self
._index
), choice
)) 
 381             if not self
._ctrl
_constraints
._choices
: 
 382                 self
._ctrl
_constraints
._compareChoices 
= [] 
 383                 self
._ctrl
_constraints
._choices 
= [] 
 386             compareChoice 
= choice
.strip() 
 388             if self
._ctrl
_constraints
._compareNoCase
: 
 389                 compareChoice 
= compareChoice
.lower() 
 391             if self
._ctrl
_constraints
._alignRight
: 
 392                 choice 
= choice
.rjust(self
._masklength
) 
 394                 choice 
= choice
.ljust(self
._masklength
) 
 395             if self
._ctrl
_constraints
._fillChar 
!= ' ': 
 396                 choice 
= choice
.replace(' ', self
._fillChar
) 
 397 ##            dbg('updated choice:', choice) 
 400             self
._ctrl
_constraints
._compareChoices
.append(compareChoice
) 
 401             self
._ctrl
_constraints
._choices
.append(choice
) 
 402             self
._choices 
= self
._ctrl
_constraints
._choices     
# (for shorthand) 
 404             if( not self
.IsValid(choice
) and 
 405                (not self
._ctrl
_constraints
.IsEmpty(choice
) or 
 406                 (self
._ctrl
_constraints
.IsEmpty(choice
) and self
._ctrl
_constraints
._validRequired
) ) ): 
 407                 raise ValueError('"%s" is not a valid value for the control "%s" as specified.' % (choice
, self
.name
)) 
 409         wx
.ComboBox
.Append(self
, choice
, clientData
) 
 412     def AppendItems( self
, choices 
): 
 414         AppendItems() is handled in terms of Append, to avoid code replication. 
 416         for choice 
in choices
: 
 422         This base control function override is necessary so the derived control can 
 423         keep track of any additions to the list of choices, because wx.ComboBox 
 424         doesn't have an accessor for the choice list. 
 428             self
._ctrl
_constraints
._autoCompleteIndex 
= -1 
 429             if self
._ctrl
_constraints
._choices
: 
 430                 self
.SetCtrlParameters(choices
=[]) 
 431         wx
.ComboBox
.Clear(self
) 
 434     def _OnCtrlParametersChanged(self
): 
 436         This overrides the mixin's default OnCtrlParametersChanged to detect 
 437         changes in choice list, so masked.Combobox can update the base control: 
 439         if self
.controlInitialized 
and self
._choices 
!= self
._ctrl
_constraints
._choices
: 
 440             wx
.ComboBox
.Clear(self
) 
 441             self
._choices 
= self
._ctrl
_constraints
._choices
 
 442             for choice 
in self
._choices
: 
 443                 wx
.ComboBox
.Append( self
, choice 
) 
 448         This function is a hack to make up for the fact that wx.ComboBox has no 
 449         method for returning the selected portion of its edit control.  It 
 450         works, but has the nasty side effect of generating lots of intermediate 
 453 ##        dbg(suspend=1)  # turn off debugging around this function 
 454 ##        dbg('MaskedComboBox::GetMark', indent=1) 
 457             return 0, 0 # no selection possible for editing 
 458 ##        sel_start, sel_to = wxComboBox.GetMark(self)        # what I'd *like* to have! 
 459         sel_start 
= sel_to 
= self
.GetInsertionPoint() 
 460 ##        dbg("current sel_start:", sel_start) 
 461         value 
= self
.GetValue() 
 462 ##        dbg('value: "%s"' % value) 
 464         self
._ignoreChange 
= True               # tell _OnTextChange() to ignore next event (if any) 
 466         wx
.ComboBox
.Cut(self
) 
 467         newvalue 
= self
.GetValue() 
 468 ##        dbg("value after Cut operation:", newvalue) 
 470         if newvalue 
!= value
:                   # something was selected; calculate extent 
 471 ##            dbg("something selected") 
 472             sel_to 
= sel_start 
+ len(value
) - len(newvalue
) 
 473             wx
.ComboBox
.SetValue(self
, value
)    # restore original value and selection (still ignoring change) 
 474             wx
.ComboBox
.SetInsertionPoint(self
, sel_start
) 
 475             wx
.ComboBox
.SetMark(self
, sel_start
, sel_to
) 
 477         self
._ignoreChange 
= False              # tell _OnTextChange() to pay attn again 
 479 ##        dbg('computed selection:', sel_start, sel_to, indent=0, suspend=0) 
 480         return sel_start
, sel_to
 
 483     def SetSelection(self
, index
): 
 485         Necessary override for bookkeeping on choice selection, to keep current value 
 488 ##        dbg('MaskedComboBox::SetSelection(%d)' % index) 
 490             self
._prevValue 
= self
._curValue
 
 491             self
._curValue 
= self
._choices
[index
] 
 492             self
._ctrl
_constraints
._autoCompleteIndex 
= index
 
 493         wx
.ComboBox
.SetSelection(self
, index
) 
 496     def _OnKeyDownInComboBox(self
, event
): 
 498         This function is necessary because navigation and control key 
 499         events do not seem to normally be seen by the wxComboBox's 
 500         EVT_CHAR routine.  (Tabs don't seem to be visible no matter 
 503         if event
.GetKeyCode() in self
._nav 
+ self
._control
: 
 507             event
.Skip()    # let mixin default KeyDown behavior occur 
 510     def _OnSelectChoice(self
, event
): 
 512         This function appears to be necessary, because the processing done 
 513         on the text of the control somehow interferes with the combobox's 
 514         selection mechanism for the arrow keys. 
 516 ##        dbg('MaskedComboBox::OnSelectChoice', indent=1) 
 522         value 
= self
.GetValue().strip() 
 524         if self
._ctrl
_constraints
._compareNoCase
: 
 525             value 
= value
.lower() 
 527         if event
.GetKeyCode() == wx
.WXK_UP
: 
 531         match_index
, partial_match 
= self
._autoComplete
( 
 533                                                 self
._ctrl
_constraints
._compareChoices
, 
 535                                                 self
._ctrl
_constraints
._compareNoCase
, 
 536                                                 current_index 
= self
._ctrl
_constraints
._autoCompleteIndex
) 
 537         if match_index 
is not None: 
 538 ##            dbg('setting selection to', match_index) 
 539             # issue appropriate event to outside: 
 540             self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=match_index
) 
 542             keep_processing 
= False 
 544             pos 
= self
._adjustPos
(self
._GetInsertionPoint
(), event
.GetKeyCode()) 
 545             field 
= self
._FindField
(pos
) 
 546             if self
.IsEmpty() or not field
._hasList
: 
 547 ##                dbg('selecting 1st value in list') 
 548                 self
._OnAutoSelect
(self
._ctrl
_constraints
, match_index
=0) 
 550                 keep_processing 
= False 
 552                 # attempt field-level auto-complete 
 554                 keep_processing 
= self
._OnAutoCompleteField
(event
) 
 555 ##        dbg('keep processing?', keep_processing, indent=0) 
 556         return keep_processing
 
 559     def _OnAutoSelect(self
, field
, match_index
): 
 561         Override mixin (empty) autocomplete handler, so that autocompletion causes 
 562         combobox to update appropriately. 
 564 ##        dbg('MaskedComboBox::OnAutoSelect', field._index, indent=1) 
 565 ##        field._autoCompleteIndex = match_index 
 566         if field 
== self
._ctrl
_constraints
: 
 567             self
.SetSelection(match_index
) 
 568 ##            dbg('issuing combo selection event') 
 569             self
.GetEventHandler().ProcessEvent( 
 570                 MaskedComboBoxSelectEvent( self
.GetId(), match_index
, self 
) ) 
 572 ##        dbg('field._autoCompleteIndex:', match_index) 
 573 ##        dbg('self.GetSelection():', self.GetSelection()) 
 574         end 
= self
._goEnd
(getPosOnly
=True) 
 575 ##        dbg('scheduling set of end position to:', end) 
 576         # work around bug in wx 2.5 
 577         wx
.CallAfter(self
.SetInsertionPoint
, 0) 
 578         wx
.CallAfter(self
.SetInsertionPoint
, end
) 
 582     def _OnReturn(self
, event
): 
 584         For wx.ComboBox, it seems that if you hit return when the dropdown is 
 585         dropped, the event that dismisses the dropdown will also blank the 
 586         control, because of the implementation of wxComboBox.  So this function 
 587         examines the selection and if it is -1, and the value according to 
 588         (the base control!) is a value in the list, then it schedules a 
 589         programmatic wxComboBox.SetSelection() call to pick the appropriate 
 590         item in the list. (and then does the usual OnReturn bit.) 
 592 ##        dbg('MaskedComboBox::OnReturn', indent=1) 
 593 ##        dbg('current value: "%s"' % self.GetValue(), 'current index:', self.GetSelection()) 
 594         if self
.GetSelection() == -1 and self
.GetValue().lower().strip() in self
._ctrl
_constraints
._compareChoices
: 
 595             wx
.CallAfter(self
.SetSelection
, self
._ctrl
_constraints
._autoCompleteIndex
) 
 597         event
.m_keyCode 
= wx
.WXK_TAB
 
 602 class ComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin 
): 
 604     The "user-visible" masked combobox control, this class is 
 605     identical to the BaseMaskedComboBox class it's derived from. 
 606     (This extra level of inheritance allows us to add the generic 
 607     set of masked edit parameters only to this class while allowing 
 608     other classes to derive from the "base" masked combobox control, 
 609     and provide a smaller set of valid accessor functions.) 
 610     See BaseMaskedComboBox for available methods. 
 615 class PreMaskedComboBox( BaseMaskedComboBox
, MaskedEditAccessorsMixin 
): 
 617     This class exists to support the use of XRC subclassing. 
 619     # This should really be wx.EVT_WINDOW_CREATE but it is not 
 620     # currently delivered for native controls on all platforms, so 
 621     # we'll use EVT_SIZE instead.  It should happen shortly after the 
 622     # control is created as the control is set to its "best" size. 
 623     _firstEventType 
= wx
.EVT_SIZE
 
 626         pre 
= wx
.PreComboBox() 
 628         self
.Bind(self
._firstEventType
, self
.OnCreate
) 
 631     def OnCreate(self
, evt
): 
 632         self
.Unbind(self
._firstEventType
) 
 637 ## ==================== 
 639 ##  1. Converted docstrings to reST format, added doc for ePyDoc. 
 640 ##  2. Renamed helper functions, vars etc. not intended to be visible in public 
 641 ##     interface to code. 
 644 ##  1. Added .SetFont() method that properly resizes control 
 645 ##  2. Modified control to support construction via XRC mechanism. 
 646 ##  3. Added AppendItems() to conform with latest combobox.