wxComboCtrl: Left and Right arrow keys do not trigger OnComboKeyEvent()

I have been experimenting with custom ComboCtrl and ComboPopup classes based on the example in the wxPython demo. I have noticed that when I press the left or right arrow keys the OnComboKeyEvent() method is not triggered. However, it is triggered for all other keys including the left and right arrow keys on the numeric keypad.

Below is a stand-alone version of the example from wxPython demo. It has a print call added in OnComboKeyEvent() to output the key codes. When I run it, I select an item from the drop-down list and then press the left and right arrow keys. I don’t get any output for these keys, but I do for any other keys.

Is this intentional or a bug?

I am running on Python 3.8.10 + wxPython 4.1.1 gtk3 (phoenix) wxWidgets 3.1.5 + Linux Mint 20.3. Does the same thing happen on other platforms?

import wx

#----------------------------------------------------------------------
# This class is used to provide an interface between a ComboCtrl and the
# ListCtrl that is used as the popup for the combo widget.

class ListCtrlComboPopup(wx.ComboPopup):

    def __init__(self):
        wx.ComboPopup.__init__(self)
        self.lc = None

    def AddItem(self, txt):
        self.lc.InsertItem(self.lc.GetItemCount(), txt)

    def OnMotion(self, evt):
        item, flags = self.lc.HitTest(evt.GetPosition())
        if item >= 0:
            self.lc.Select(item)
            self.curitem = item

    def OnLeftDown(self, evt):
        self.value = self.curitem
        self.Dismiss()


    # The following methods are those that are overridable from the
    # ComboPopup base class.  Most of them are not required, but all
    # are shown here for demonstration purposes.

    # This is called immediately after construction finishes.  You can
    # use self.GetCombo if needed to get to the ComboCtrl instance.
    def Init(self):
        self.value = -1
        self.curitem = -1

    # Create the popup child control.  Return true for success.
    def Create(self, parent):
        self.lc = wx.ListCtrl(parent, style=wx.LC_LIST | wx.LC_SINGLE_SEL | wx.SIMPLE_BORDER)
        self.lc.Bind(wx.EVT_MOTION, self.OnMotion)
        self.lc.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        return True

    # Return the widget that is to be used for the popup
    def GetControl(self):
        return self.lc

    # Called just prior to displaying the popup, you can use it to
    # 'select' the current item.
    def SetStringValue(self, val):
        idx = self.lc.FindItem(-1, val)
        if idx != wx.NOT_FOUND:
            self.lc.Select(idx)

    # Return a string representation of the current item.
    def GetStringValue(self):
        if self.value >= 0:
            return self.lc.GetItemText(self.value)
        return ""

    # Called immediately after the popup is shown
    def OnPopup(self):
        wx.ComboPopup.OnPopup(self)

    # Called when popup is dismissed
    def OnDismiss(self):
        wx.ComboPopup.OnDismiss(self)

    # This is called to custom paint in the combo control itself
    # (ie. not the popup).  Default implementation draws value as
    # string.
    def PaintComboControl(self, dc, rect):
        wx.ComboPopup.PaintComboControl(self, dc, rect)

    # Receives key events from the parent ComboCtrl.  Events not
    # handled should be skipped, as usual.
    def OnComboKeyEvent(self, event):
        print("OnComboKeyEvent", event.GetKeyCode())
        wx.ComboPopup.OnComboKeyEvent(self, event)

    # Implement if you need to support special action when user
    # double-clicks on the parent wxComboCtrl.
    def OnComboDoubleClick(self):
        wx.ComboPopup.OnComboDoubleClick(self)

    # Return final size of popup. Called on every popup, just prior to OnPopup.
    # minWidth = preferred minimum width for window
    # prefHeight = preferred height. Only applies if > 0,
    # maxHeight = max height for window, as limited by screen size
    #   and should only be rounded down, if necessary.
    def GetAdjustedSize(self, minWidth, prefHeight, maxHeight):
        return wx.ComboPopup.GetAdjustedSize(self, minWidth, prefHeight, maxHeight)

    # Return true if you want delay the call to Create until the popup
    # is shown for the first time. It is more efficient, but note that
    # it is often more convenient to have the control created
    # immediately.
    # Default returns false.
    def LazyCreate(self):
        return wx.ComboPopup.LazyCreate(self)


#----------------------------------------------------------------------

class MyFrame(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY):
        wx.Frame.__init__(self, parent, id)

        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        comboCtrl = wx.ComboCtrl(self, wx.ID_ANY, "", size=(200, -1))
        main_sizer.Add(comboCtrl, 0, 0, 0)

        popupCtrl = ListCtrlComboPopup()

        # It is important to call SetPopupControl() as soon as possible
        comboCtrl.SetPopupControl(popupCtrl)

        self.SetSizer(main_sizer)
        main_sizer.Fit(self)
        self.Layout()

        # Populate using wx.ListView methods
        popupCtrl.AddItem("First Item")
        popupCtrl.AddItem("Second Item")
        popupCtrl.AddItem("Third Item")


class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, wx.ID_ANY)
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

# end of class MyApp

if __name__ == "__main__":
    app = MyApp(0)
    app.MainLoop()

Same thing with :

  • Python 3.10.1
  • wxPython 4.1.2a1.dev5449+3ac1e526 (phoenix)
  • wxWidgets 3.1.6
  • Windows 11.

ditto (except the snapshot was built with “wxWidgets 3.2.0 RC1” :slight_smile:).
My guess is that the left and right keys are used to move the caret, so it is designed behavior.

Thanks to both of you for confirming it isn’t just GTK behaviour.

The actual control I am developing automatically selects the first item in the list that matches what the user has typed so far. I find this really useful in data entry panels. I had previously sub-classed wx.ComboBox to do the same, but one of the things I really hate about GTK3 is their version of the ComboBox - when there are a lot of items it looks awful, takes up too much space and is clunky to use. Thus I have been trying to combine the auto-selection technique with the ComboCtrl and ListCtrl pop-up.

In the new control I need to be able to do some processing if the user moves the caret. As a work-around I have added another event handler for EVT_CHAR in the text control, which just handles the left and right arrow keys and then calls event.Skip() so the original handler continues as before. It does work, but it would be nicer if all the logic for key events was in one place.

I’m not quite sure what you are after, but I’ve made a context sensitive TextCtrl which could easily be extended with flap trigger (or help button): but things like that are from yesterday I think… :grin:
ctx_textctrl_notb.py (7.6 KB) test_frame_ctx_tc_notb.py (849 Bytes)

Thanks for your reply, Georg. I have not had the chance to look at your code in detail yet as I am in the middle of trying to upgrade one of my PCs to Linux Mint 21 which is proving to be a lot more complex than usual (including building wxPython from source, due to it moving to Python 3.10).

I did make some progress with my own ComboCtrl+ListCtrl popup and it appears to be working as I need it to, although it still needs to tidied up and tested more. Perhaps I will be able to improve it with some ideas from your example!

Cheers,
Richard

well, he said we ought to have fun & I couldn’t resist: the combo context aware TextCtrl :stuck_out_tongue_winking_eye:
ctx_textctrl_notb_hlp.py (8.4 KB) test_frame_ctx_tc_notb_hlp.py (859 Bytes)