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()