ComboPopUp - show listctrl but with combotext having focus

Dear Friends of wxPython,
First,
thank you for your help. Sorry, I am very frustrated trying to make a
combo box that will drop open without a mouse click on got focus.
So
have tried the wx.ComboPopup. In the Docs Demo and Frank Millman code
example the list control opens and it gets the focus. I can’t get the
focus back to the combo text box without the list control going away ,
.dismiss().

I need the user to type in chars in the textbox and have the usual
search effect of a combo box (searches multiple chars not just the
first) not a list box which finds the first instance of only 1 letter.
I can write a search function to search the list or combo or the
Choices data. I know the combobox won’t drop programmatically. Yet a
data entry form with combo boxes that won’t open with the keyboard -
unless one presses F4 on win or Space on Gtk - is irritating to the
entry person.

I want the list control to be shown - the combo textbox to have
focus so the user can type into it. Which each keypress i can run a
function to search the list of items and highlight/select it in both
controls.

Actually if the combo textbox has focus then it will do the correct searching even if the list part isn’t dropped down.
After
each keypress one can check the index of the selected item and
highlight/select it in the popped up list control. This seems to
me to be closer to the usual combobox behavior than a combopopup where
one can only scroll or search as in a list control

I tried to finds the source code of the combopopup by downloading the wxpython source code but can’ find it.
I am fairly new to wxPython and to Python itself but am diving in.
Any help or ideas will be very much appreciated. What do you do in your programs?

Markandeya wrote:

Dear Friends of wxPython,
First, thank you for your help. Sorry, I am very frustrated
trying to make a combo box that will drop open without a
mouse click on got focus.
So have tried the wx.ComboPopup. In the Docs Demo and Frank
Millman code example the list control opens and it gets the
focus. I can't get the focus back to the combo text box
without the list control going away , .dismiss().

I spent a bit of time experimenting, but I also could not achieve a scenario
where you could type into the text control while the list of choices is
visible.

Maybe you could achieve it by subclassing wx.TextCtrl, and displaying the
list of choices yourself when it gets focus. You could trap changes using
EVT_TEXT, search for the first matching string, and highlight it in the
list.

Sorry I could not be more help. Maybe Robin has some ideas.

Frank Millman

Frank Millman wrote:

Markandeya wrote:

Dear Friends of wxPython,
First, thank you for your help. Sorry, I am very frustrated trying to make a combo box that will drop open without a mouse click on got focus.
So have tried the wx.ComboPopup. In the Docs Demo and Frank Millman code example the list control opens and it gets the focus. I can't get the focus back to the combo text box without the list control going away , .dismiss().

I spent a bit of time experimenting, but I also could not achieve a scenario
where you could type into the text control while the list of choices is
visible.

Maybe you could achieve it by subclassing wx.TextCtrl, and displaying the
list of choices yourself when it gets focus. You could trap changes using
EVT_TEXT, search for the first matching string, and highlight it in the
list.

Sorry I could not be more help. Maybe Robin has some ideas.

Sorry, nothing comes to mind.

···

--
Robin Dunn
Software Craftsman
http://wxPython.org Java give you jitters? Relax with wxPython!

facing this same issue now with my implementation of a combobox that shows subset of choices as you type… works fine on windows, linux the popup shows and steals focus from the textbox and typing cannot continue. Clicking the textbox hides the popup.

Code:

import sys

import wx.lib.mixins.inspection
import wx.lib.inspection
import wx.stc as stc

from tic.start.dev_release.tree_ctrl_with_checkboxes import GitChangesDialog


def patch_inspection_tool():
    original_fmt_widget = wx.lib.inspection.InspectionInfoPanel.FmtWidget

    def patched_fmt_widget(self, obj):
        obj.GetChildrenOld = obj.GetChildren
        obj.GetChildren = lambda: list(obj.GetChildrenOld())
        return original_fmt_widget(self, obj)

    wx.lib.inspection.InspectionInfoPanel.FmtWidget = patched_fmt_widget


# Apply the patch, needed for wxpython <4.2.3 and Python >= 3.13.1
if wx.VERSION < (4, 2, 3) and sys.version_info >= (3, 13, 1):
    patch_inspection_tool()

WX_WIT = wx.lib.mixins.inspection.InspectionMixin


class PromptingComboBox(wx.ComboBox):
    def __init__(self, parent, value, choices=[], style=0, **par):
        wx.ComboBox.__init__(self, parent, wx.ID_ANY, value, style=style | wx.CB_DROPDOWN|wx.TE_PROCESS_ENTER, choices=choices, **par)
        self.choices = choices
        self.verbose = False
        self.debug_print(f'PromptingComboBox::init - current selection: {self.GetValue()}, value: {value}')
        self.previous_selection = value  # Store the initial value as the previous selection
        self.Bind(wx.EVT_TEXT, self.EvtText)
        # self.Bind(wx.EVT_CHAR, self.EvtChar)
        self.Bind(wx.EVT_COMBOBOX, self.EvtCombobox)
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.Bind(wx.EVT_TEXT_ENTER, self.EvtTextEnter)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnPopupClick)  # Bind to left-click event
        self.ignoreEvtText = False
        self.popped_up = False

    def OnPopupClick(self, event):
        self.debug_print("PromptingComboBox::OnPopupClick - Popup clicked to open")
        current_value = self.GetValue()  # Cache current text
        self.clear_popup()
        self.AppendItems(self.choices)  # Re-populate the choices
        self.ChangeValue(current_value)    # Restore text after repopulating
        event.Skip()  # Ensure the default behavior (popup opening) still occurs


    def EvtCombobox(self, event):
        self.debug_print(f'PromptingComboBox::EvtCombobox - current selection: {self.GetValue()}')
        self.previous_selection = self.GetValue()  # Update the previous selection
        self.popped_up = False
        self.ignoreEvtText = True  # Ignore the EVT_TEXT event triggered by the selection
        event.Skip()

    # def EvtChar(self, event):
    #     self.debug_print('EvtChar')
    #     if event.GetKeyCode() == 8: # detect backspace
    #         self.debug_print(f'PromptingComboBox::EvtChar - backspace - current selection: {self.GetValue()}')
    #         pass
    #     event.Skip()

    def EvtText(self, event):
        currentText = event.GetString()
        self.debug_print(f'PromptingComboBox::EvtText - current selection: {self.GetValue()} - current text: {currentText} popped {self.popped_up}')
        if self.ignoreEvtText:
            self.debug_print(f'text ignored: {currentText}')
            self.ignoreEvtText = False
            return
        if currentText:
            filtered_choices = [choice for choice in self.choices if currentText in choice]
            # self.ignoreEvtText = True
            self.caret_pos = self.GetInsertionPoint()  # Save the current caret position
            self.clear_popup()

            # self.ignoreEvtText = True
            self.AppendItems(filtered_choices)
            # set focus to the frame

            wx.CallAfter(self.SetText, currentText)
            if filtered_choices:
                self.debug_print(f'PromptingComboBox::EvtText - filtered choices: {filtered_choices} popping up')
                self.Popup()
                self.SetFocus()
                self.popped_up = True
            else:
                self.Dismiss()
                self.popped_up = False
        else:
            self.debug_print(f'PromptingComboBox::EvtText - empty string - current selection: {self.GetValue()}')
            self.clear_popup()
            self.AppendItems(self.choices)
        self.fix_cursor()
        print('EvtText skipping event to allow further processing')
        event.Skip()

    def clear_popup(self):
        """ use instead of self.CLear() to avoid another trigger of EVT_TEXT """
        # clear list backwards to avoid index errors
        [self.Delete(i) for i in range(self.GetCount() - 1, -1, -1)]

    def fix_cursor(self):
        window = self
        while window:
            window.SetCursor(wx.Cursor(wx.CURSOR_ARROW))
            window = window.GetParent()

    def OnKeyDown(self, event):
        # print('PromptingComboBox::OnKeyDown - current key:', event.GetKeyCode())
        if event.GetKeyCode() == wx.WXK_RETURN:  # Detect Enter key, wx.EVT_TEXT_ENTER doesn't work when popup is shown
            self.debug_print(f'PromptingComboBox::OnKeyDown - WXK_RETURN - GetValue: {self.GetValue()}')
            if self.popped_up:  # Check if the popup list is shown,
                # get the list of items in the popup list
                items_strs = [self.GetString(i) for i in range(self.GetCount())]
                self.debug_print(f'PromptingComboBox::OnKeyDown - WXK_RETURN - items_strs: {items_strs} GetSelection {self.GetSelection()}')
                existing_selection = 0 # select the first item in the list by default
                if self.GetValue() in items_strs:
                    existing_selection = items_strs.index(self.GetValue())
                self.SetSelection(existing_selection)

            self.previous_selection = self.GetValue()  # Update the previous selection
        elif event.GetKeyCode() == wx.WXK_ESCAPE:
            self.debug_print(f'OnKeyDown - escape - current selection: ({self.GetValue()}) previous_selection ({self.previous_selection})' )
            self.SetValue(self.previous_selection)  # Revert to the previous selection
        elif event.GetKeyCode() in [wx.WXK_DOWN, wx.WXK_UP]:
            # self.debug_print(f'OnKeyDown - up/down - current selection: ({self.GetValue()}) previous_selection ({self.previous_selection}) {self.popped_up}' )
            # if the popup is not shown, show it
            if not self.popped_up:
                self.Popup()
                self.popped_up = True
        event.Skip()

    def SetText(self, text):
        """set the combobox text from the main loop thread"""
        self.debug_print(f'PromptingComboBox::SetText - current caret position: ({self.caret_pos}), text: ({text}) popped {self.popped_up}')
        self.ChangeValue(text)
        # self.SetFocus()
        self.SetInsertionPoint(self.caret_pos)  # Restore the caret position

    # intercept SetStringSelection to set the previous selection, then call the base class
    def SetStringSelection(self, value):
        self.debug_print('PromptingComboBox::SetStringSelection - current selection:', self.GetValue(), 'value:', value)
        self.previous_selection = value
        wx.ComboBox.SetStringSelection(self, value)

    def EvtTextEnter(self, event):
        self.debug_print('PromptingComboBox::EvtTextEnter - current selection:', self.GetValue())
        event.Skip()

    def debug_print(self, *args):
        if self.verbose:
            print(*args)
    
    def AddItems(self, items):
        """Append items to the combobox and update the choices list."""
        self.choices.extend(items)
        self.AppendItems(items)
        self.Layout()


class TestApp(wx.App, WX_WIT):
    def OnInit(self):
        self.Init()  # initialize the inspection tool. Open with CTRL-ALT-I
        frame = wx.Frame(None, title="Test PromptingComboBox", size=(300, 200))
        panel = wx.Panel(frame)
        sizer = wx.BoxSizer(wx.VERTICAL)

        # choices = ['apple', 'banana', 'cherry', 'date', 'fig', 'grape']
        def derivedRelatives(relative):
            return [relative, 'step' + relative, relative + '-in-law']

        choices = ['grandmother', 'grandfather', 'cousin', 'aunt', 'uncle', 'grandson', 'granddaughter']
        for relative in ['mother', 'father', 'sister', 'brother', 'daughter', 'son']:
            choices.extend(derivedRelatives(relative))
        choices = sorted(choices)
        combo = PromptingComboBox(panel, value='', choices=['choice1'], style=wx.CB_DROPDOWN)
        combo.verbose = True
        sizer.Add(combo, 0, wx.ALL | wx.EXPAND, 5)
        textbox = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_PROCESS_ENTER,
                              value='another widget to take focus from the combobox')
        sizer.Add(textbox, 1, wx.ALL | wx.EXPAND, 5)
        combo.AddItems(choices)
        combo.AddItems(['final choice'])

        panel.SetSizer(sizer)
        frame.Show()
        # frame.SetFocus()
        return True

# main for testing with fake data
if __name__ == '__main__':
    app = TestApp()
    app.MainLoop()

apparently I also asked something similar 11 years ago… with no response then either ComboPopup (VListBox) always seems to steal focus from ComboCtrl, really frustrating