combobox.py 33.3 KB
1 2 3 4 5 6 7 8 9 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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794
#----------------------------------------------------------------------------
# Name:         masked.combobox.py
# Authors:      Will Sadkin
# Email:        wsadkin@nameconnector.com
# Created:      02/11/2003
# Copyright:    (c) 2003 by Will Sadkin, 2003
# RCS-ID:       $Id$
# License:      wxWidgets license
#----------------------------------------------------------------------------
#
# This masked edit class allows for the semantics of masked controls
# to be applied to combo boxes.
#
#----------------------------------------------------------------------------

"""
Provides masked edit capabilities within a ComboBox format, as well as
a base class from which you can derive masked comboboxes tailored to a specific
function.  See maskededit module overview for how to configure the control.
"""

import  wx, types, string
from wx.lib.masked import *

# 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=1)

## ---------- ---------- ---------- ---------- ---------- ---------- ----------
## Because calling SetSelection programmatically does not fire EVT_COMBOBOX
## events, we have to do it ourselves when we auto-complete.
class MaskedComboBoxSelectEvent(wx.PyCommandEvent):
    """
    Because calling SetSelection programmatically does not fire EVT_COMBOBOX
    events, the derived control has to do it itself when it auto-completes.
    """
    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 MaskedComboBoxEventHandler(wx.EvtHandler):
    """
    This handler ensures that the derived control can react to events
    from the base control before any external handlers run, to ensure 
    proper behavior.
    """
    def __init__(self, combobox):
        wx.EvtHandler.__init__(self)
        self.combobox = combobox
        combobox.PushEventHandler(self)
        self.Bind(wx.EVT_SET_FOCUS, self.combobox._OnFocus )            ## defeat automatic full selection
        self.Bind(wx.EVT_KILL_FOCUS, self.combobox._OnKillFocus )       ## run internal validator
        self.Bind(wx.EVT_LEFT_DCLICK, self.combobox._OnDoubleClick)     ## select field under cursor on dclick
        self.Bind(wx.EVT_RIGHT_UP, self.combobox._OnContextMenu )       ## bring up an appropriate context menu
        self.Bind(wx.EVT_CHAR, self.combobox._OnChar )                  ## handle each keypress
        self.Bind(wx.EVT_KEY_DOWN, self.combobox._OnKeyDownInComboBox ) ## for special processing of up/down keys
        self.Bind(wx.EVT_KEY_DOWN, self.combobox._OnKeyDown )           ## for processing the rest of the control keys
                                                                        ## (next in evt chain)
        self.Bind(wx.EVT_COMBOBOX, self.combobox._OnDropdownSelect )    ## to bring otherwise completely independent base
                                                                        ## ctrl selection into maskededit framework
        self.Bind(wx.EVT_TEXT, self.combobox._OnTextChange )            ## color control appropriately & keep
                                                                        ## track of previous value for undo



class BaseMaskedComboBox( wx.ComboBox, MaskedEditMixin ):
    """
    Base class for generic masked edit comboboxes; allows auto-complete of values.
    It is not meant to be instantiated directly, but rather serves as a base class
    for any subsequent refinements.
    """
    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):


        kwargs['choices'] = choices                 ## set up maskededit to work with choice list too

        self._prevSelection = (-1, -1)

        ## 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

        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())
            width = self.GetSize().width
            height = self.GetBestSize().height
            self.SetInitialSize((width, height))


        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)

        self.replace_next_combobox_event = False
        self.correct_selection = -1

        if setupEventHandling:
            ## Setup event handling functions through event handler object,
            ## to guarantee processing prior to giving event callbacks from
            ## outside the class:
            self.evt_handler = MaskedComboBoxEventHandler(self)
            self.Bind(wx.EVT_WINDOW_DESTROY, self.OnWindowDestroy )



    def __repr__(self):
        return "<MaskedComboBox: %s>" % self.GetValue()


    def OnWindowDestroy(self, event):
        # clean up associated event handler object:
        if self.RemoveEventHandler(self.evt_handler):
            wx.CallAfter(self.evt_handler.Destroy)
        event.Skip()        


    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 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.SetInitialSize((width, height))


    def _GetSelection(self):
        """
        Allow mixin to get the text selection of this control.
        REQUIRED by any class derived from MaskedEditMixin.
        """
##        dbg('MaskedComboBox::_GetSelection()')
        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.
        """
##        dbg('MaskedComboBox::_SetSelection: setting mark to (%d, %d)' % (sel_start, sel_to))
        if not self.__readonly:
            return self.SetMark( sel_start, sel_to )


    def _GetInsertionPoint(self):
##        dbg('MaskedComboBox::_GetInsertionPoint()', indent=1)
##        ret = self.GetInsertionPoint()
        # work around new bug in 2.5, in which the insertion point
        # returned is always at the right side of the selection,
        # rather than the start, as is the case with TextCtrl.
        ret = self.GetMark()[0]
##        dbg('returned', ret, indent=0)
        return ret

    def _SetInsertionPoint(self, pos):
##        dbg('MaskedComboBox::_SetInsertionPoint(%d)' % pos)
        if not self.__readonly:
            self.SetInsertionPoint(pos)


    def IsEmpty(*args, **kw):
        return MaskedEditMixin.IsEmpty(*args, **kw)


    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()
##        dbg('MaskedComboBox::_SetValue(%s), selection beforehand: %d' % (value, self.GetSelection()))
        wx.ComboBox.SetValue(self, value)
##        dbg('MaskedComboBox::_SetValue(%s), selection now: %d' % (value, self.GetSelection()))
        # 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.
        """
##        dbg('MaskedComboBox::SetValue(%s)' % value, indent=1)
        if not self._mask:
            wx.ComboBox.SetValue(value)   # revert to base control behavior
##            dbg('no mask; deferring to base class', indent=0)
            return
        # else...
        # empty previous contents, replacing entire value:
##        dbg('MaskedComboBox::SetValue: selection beforehand: %d' % (self.GetSelection()))
        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, replace_to = 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, ' ')
                value = self._Paste(value, raise_on_invalid=True, just_return_value=True)
            else:
                raise
##        dbg('adjusted value: "%s"' % value)

        # Attempt to compensate for fact that calling .SetInsertionPoint() makes the
        # selection index -1, even if the resulting set value is in the list.  
        # So, if we are setting a value that's in the list, use index selection instead.
        if value in self._choices:
            index = self._choices.index(value)
            self._prevValue = self._curValue
            self._curValue = self._choices[index]
            self._ctrl_constraints._autoCompleteIndex = index
            self.SetSelection(index)
        else:
            self._SetValue(value)
####            dbg('queuing insertion after .SetValue', replace_to)
            wx.CallAfter(self._SetInsertionPoint, replace_to)
            wx.CallAfter(self._SetSelection, replace_to, replace_to)
##        dbg(indent=0)


    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 not self.__readonly:
            if self._mask:
                self._Undo()
            else:
                wx.ComboBox.Undo(self)       # else revert to base control behavior

    def Append( self, choice, clientData=None ):
        """
        This base control function override is necessary so the control can keep track
        of any additions to the list of choices, because wx.ComboBox 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 AppendItems( self, choices ):
        """
        AppendItems() is handled in terms of Append, to avoid code replication.
        """
        for choice in choices:
            self.Append(choice)


    def Clear( self ):
        """
        This base control function override is necessary so the derived control can
        keep track of any additions to the list of choices, because wx.ComboBox
        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):
        """
        This overrides the mixin's default OnCtrlParametersChanged to detect
        changes in choice list, so masked.Combobox 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 )


    # Not all wx platform implementations have .GetMark, so we make the following test,
    # and fall back to our old hack if they don't...
    #
    if not hasattr(wx.ComboBox, 'GetMark'):
        def GetMark(self):
            """
            This function is a hack to make up for the fact that wx.ComboBox 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
    else:
        def GetMark(self):
##            dbg('MaskedComboBox::GetMark()', indent = 1)
            ret = wx.ComboBox.GetMark(self)
##            dbg('returned', ret, indent=0)
            return ret


    def SetSelection(self, index):
        """
        Necessary override for bookkeeping on choice selection, to keep current value
        current.
        """
##        dbg('MaskedComboBox::SetSelection(%d)' % index, indent=1)
        if self._mask:
            self._prevValue = self._curValue
            self._ctrl_constraints._autoCompleteIndex = index
            if index != -1:
                self._curValue = self._choices[index]
            else:
                self._curValue = None
        wx.ComboBox.SetSelection(self, index)
##        dbg('selection now: %d' % self.GetCurrentSelection(), indent=0)


    def _OnKeyDownInComboBox(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, except for CB_READONLY
        controls, for some bizarre reason... {:-( )
        """
        key = event.GetKeyCode()
##        dbg('MaskedComboBox::OnKeyDownInComboBox(%d)' % key)
        if event.GetKeyCode() in self._nav + self._control:
            if not self._IsEditable():
                # WANTS_CHARS with CB_READONLY apparently prevents navigation on WXK_TAB;
                # ensure we can still navigate properly, as maskededit mixin::OnChar assumes
                # that event.Skip() will just work, but it doesn't:
                if self._keyhandlers.has_key(key):
                    self._keyhandlers[key](event)
                # else pass
            else:
##                dbg('calling OnChar()')
                self._OnChar(event)
        else:
            event.Skip()    # let mixin default KeyDown behavior occur
##        dbg(indent=0)


    def _OnDropdownSelect(self, event):
        """
        This function appears to be necessary because dropdown selection seems to
        manipulate the contents of the control in an inconsistent way, properly
        changing the selection index, but *not* the value. (!)  Calling SetSelection()
        on a selection event for the same selection would seem like a nop, but it seems to
        fix the problem.
        """
##        dbg('MaskedComboBox::OnDropdownSelect(%d)' % event.GetSelection(), indent=1)
        if self.replace_next_combobox_event:
##            dbg('replacing EVT_COMBOBOX')
            self.replace_next_combobox_event = False
            self._OnAutoSelect(self._ctrl_constraints, self.correct_selection)
        else:
##            dbg('skipping EVT_COMBOBOX')
            event.Skip()
##        dbg(indent=0)


    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=None):
        """
        Override mixin (empty) autocomplete handler, so that autocompletion causes
        combobox to update appropriately.
        Additionally allow items that aren't in the drop down.
        """
##        dbg('MaskedComboBox::OnAutoSelect(%d, %s)' % (field._index, repr(match_index)), indent=1)
##        field._autoCompleteIndex = match
        if isinstance(match_index, int):
            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)
##            dbg('self.GetCurrentSelection():', self.GetCurrentSelection())
            end = self._goEnd(getPosOnly=True)
##            dbg('scheduling set of end position to:', end)
            # work around bug in wx 2.5
            wx.CallAfter(self.SetInsertionPoint, 0)
            wx.CallAfter(self.SetInsertionPoint, end)
        elif isinstance(match_index, str) or isinstance(match_index, unicode):
##            dbg('CallAfter SetValue')
            #  Preserve the textbox contents
            #  See commentary in _OnReturn docstring.
            wx.CallAfter(self.SetValue, match_index)
####            dbg('queuing insertion after .SetValue', replace_to)
##        dbg(indent=0)


    def _OnReturn(self, event):
        """
        For wx.ComboBox, 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 this function
        examines the selection and if it is -1, and the value according to
        (the base control!) is a value in the list, then it schedules a
        programmatic wxComboBox.SetSelection() call to pick the appropriate
        item in the list. (and then does the usual OnReturn bit.)
        If the value isn't a value in the list then allow the current textbox contents to stay.
        """
##        dbg('MaskedComboBox::OnReturn', indent=1)
##        dbg('current value: "%s"' % self.GetValue(), 'current selection:', self.GetCurrentSelection())
        if self.GetCurrentSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
##            dbg('attempting to correct the selection to make it %d' % self._ctrl_constraints._autoCompleteIndex)
##            wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)
            self.replace_next_combobox_event = True
            self.correct_selection = self._ctrl_constraints._autoCompleteIndex
        else:
            # Not doing this causes the item to be empty after hitting return on a non-selection while the drop
            # down is showing. Not all masked comboboxes require choices from an autocomplete list.
            self.replace_next_combobox_event = True
            self.correct_selection = self._GetValue()
        event.m_keyCode = wx.WXK_TAB
        event.Skip()
##        dbg(indent=0)


    def _LostFocus(self):
##        dbg('MaskedComboBox::LostFocus; Selection=%d, value="%s"' % (self.GetSelection(), self.GetValue()))
        if self.GetCurrentSelection() == -1 and self.GetValue().lower().strip() in self._ctrl_constraints._compareChoices:
##            dbg('attempting to correct the selection to make it %d' % self._ctrl_constraints._autoCompleteIndex)
            wx.CallAfter(self.SetSelection, self._ctrl_constraints._autoCompleteIndex)


class ComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
    """
    The "user-visible" masked combobox control, this class is
    identical to the BaseMaskedComboBox class it's derived from.
    (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.)
    See BaseMaskedComboBox for available methods.
    """
    pass


class PreMaskedComboBox( BaseMaskedComboBox, MaskedEditAccessorsMixin ):
    """
    This class exists to support the use of 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.4
##  1. Added handler for EVT_COMBOBOX to address apparently inconsistent behavior
##     of control when the dropdown control is used to do a selection.
##     NOTE: due to misbehavior of wx.ComboBox re: losing all concept of the 
##      current selection index if SetInsertionPoint() is called, which is required
##      to support masked .SetValue(), this control is flaky about retaining selection
##      information.  I can't truly fix this without major changes to the base control,
##      but I've tried to compensate as best I can.
##     TODO: investigate replacing base control with ComboCtrl instead...
##  2. Fixed navigation in readonly masked combobox, which was not working because
##      the base control doesn't do navigation if style=CB_READONLY|WANTS_CHARS.
##
##
##  Version 1.3
##  1. Made definition of "hack" GetMark conditional on base class not
##     implementing it properly, to allow for migration in wx code base
##     while taking advantage of improvements therein for some platforms.
##
##  Version 1.2
##  1. Converted docstrings to reST format, added doc for ePyDoc.
##  2. Renamed helper functions, vars etc. not intended to be visible in public
##     interface to code.
##
##  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.