class AutocompleteCombobox(ttk.Combobox): """ A Combobox widget with autocomplete functionality. Features: - Real-time filtering as user types - Dropdown shows matching items only - Supports case-insensitive matching - Maintains original data list - Customizable match function - Handles arrow keys for navigation - Supports both string and display-friendly values """ def __init__( self, master=None, completevalues: List[Any] = None, case_sensitive: bool = False, match_function: Optional[Callable] = None, **kwargs ): """ Initialize the autocomplete combobox. Args: master: Parent widget completevalues: List of all possible values case_sensitive: Whether matching should be case-sensitive match_function: Custom function to determine matches (takes value and search text) **kwargs: Additional arguments for ttk.Combobox """ # Store the complete list of all possible values self._completevalues = completevalues or [] self._case_sensitive = case_sensitive self._match_function = match_function or self._default_match_function # Initialize the combobox super().__init__(master, **kwargs) # Track the current value to avoid recursion self._current_value = "" # Bind events self.bind('<KeyRelease>', self._on_keyrelease) self.bind('<FocusOut>', self._on_focusout) self.bind('<Return>', self._on_return) self.bind('<Tab>', self._on_tab) # Configure the dropdown listbox self._configure_listbox() # Set initial values self._update_autocomplete() def _configure_listbox(self): """Configure the dropdown listbox for better interaction.""" # This method is called after the widget is created to access the listbox # We need to wait until the dropdown is created def configure_listbox_callback(): try: # Get the listbox from the combobox's internal widget listbox = self.tk.call('ttk::combobox::PopdownWindow', self, 'listbox') if listbox: # Bind events to the listbox self._listbox = listbox self.tk.call('bind', listbox, '<ButtonRelease-1>', lambda e: self._on_listbox_select()) except tk.TclError: # Listbox not yet created, try again later self.after(100, configure_listbox_callback) self.after(100, configure_listbox_callback) def _default_match_function(self, item: Any, search_text: str) -> bool: """ Default matching function - checks if search text is contained in the item. Args: item: The item to check search_text: The text to search for Returns: True if the item matches the search text """ if not search_text: return True item_str = str(item) if self._case_sensitive: return search_text in item_str else: return search_text.lower() in item_str.lower() def _update_autocomplete(self): """Update the dropdown values based on current input.""" current_text = self.get() # Don't update if nothing changed if current_text == self._current_value: return self._current_value = current_text # Filter the values filtered_values = [ item for item in self._completevalues if self._match_function(item, current_text) ] # Update the dropdown list if filtered_values: self['values'] = filtered_values # Show dropdown if there are matches and we have input if current_text: self.event_generate('<Down>') self.event_generate('<Down>') # Sometimes needed to properly show dropdown else: self['values'] = [] # Hide dropdown if no matches try: self.tk.call('ttk::combobox::PopdownWindow', self, 'hide') except tk.TclError: pass def _on_keyrelease(self, event): """Handle key release events to update autocomplete.""" # Ignore navigation keys if event.keysym in ('Up', 'Down', 'Left', 'Right', 'Home', 'End', 'Page_Up', 'Page_Down', 'Return', 'Tab'): return # Update the autocomplete list self._update_autocomplete() def _on_focusout(self, event): """Handle focus out event.""" # Validate current value when focus is lost current_text = self.get() if current_text and current_text not in self['values']: # Optionally clear invalid input or keep as is # self.set('') # Uncomment to clear invalid input pass def _on_return(self, event): """Handle Return key press.""" current_text = self.get() if current_text and current_text not in self['values'] and self['values']: # If current text is not in values but we have matches, select the first match self.set(self['values'][0]) self.event_generate('<Return>') def _on_tab(self, event): """Handle Tab key press.""" current_text = self.get() if current_text and current_text not in self['values'] and self['values']: # Auto-complete with the first match when tab is pressed self.set(self['values'][0]) self.icursor(tk.END) return 'break' # Prevent default tab behavior return None def _on_listbox_select(self): """Handle selection from dropdown listbox.""" # This is called when user clicks on an item in the dropdown # The value is automatically set by the combobox self.after(10, self._validate_and_update) def _validate_and_update(self): """Validate the selected value and update autocomplete.""" current_text = self.get() if current_text and current_text in self['values']: # Valid selection, update autocomplete for next time self._update_autocomplete() def set_completevalues(self, values: List[Any]): """ Update the complete list of possible values. Args: values: New list of all possible values """ self._completevalues = values self._update_autocomplete() def get_completevalues(self) -> List[Any]: """ Get the complete list of possible values. Returns: List of all possible values """ return self._completevalues.copy() def set_match_function(self, match_function: Callable): """ Set a custom matching function. Args: match_function: Function that takes (item, search_text) and returns bool """ self._match_function = match_function self._update_autocomplete()
import tkinter as tk from tkinter import ttk import re from typing import List, Callable, Optional, Any autocomplete combobox tkinter
class AdvancedAutocompleteCombobox(AutocompleteCombobox): """ Enhanced autocomplete combobox with additional features: - Partial word matching - Fuzzy matching (Levenshtein distance) - Highlight matching text in dropdown - Recent items support - Maximum items limit """ def __init__( self, master=None, completevalues: List[Any] = None, max_items: int = 20, enable_recent: bool = True, max_recent: int = 5, **kwargs ): """ Initialize the advanced autocomplete combobox. Args: master: Parent widget completevalues: List of all possible values max_items: Maximum number of items to show in dropdown enable_recent: Whether to track recent selections max_recent: Maximum number of recent items to track **kwargs: Additional arguments for parent class """ self._max_items = max_items self._enable_recent = enable_recent self._max_recent = max_recent self._recent_items = [] super().__init__(master, completevalues, **kwargs) if self._enable_recent: self._load_recent_items() def _default_match_function(self, item: Any, search_text: str) -> bool: """ Enhanced matching function with partial word matching. Args: item: The item to check search_text: The text to search for Returns: True if the item matches the search text """ if not search_text: return True item_str = str(item) search_terms = search_text.lower().split() if self._case_sensitive: item_str_lower = item_str search_terms_original = search_text.split() else: item_str_lower = item_str.lower() search_terms_original = search_terms # Check if all search terms appear in the item return all(term in item_str_lower for term in search_terms_original) def _update_autocomplete(self): """Override to add max items limit and recent items integration.""" current_text = self.get() if current_text == self._current_value: return self._current_value = current_text # Filter values based on current text matching_values = [ item for item in self._completevalues if self._match_function(item, current_text) ] # Add recent items that match but aren't already in the list if self._enable_recent and self._recent_items: recent_matches = [ item for item in self._recent_items if item not in matching_values and self._match_function(item, current_text) ] # Add recent matches at the beginning matching_values = recent_matches + matching_values # Limit number of items if len(matching_values) > self._max_items: matching_values = matching_values[:self._max_items] # Update dropdown if matching_values: self['values'] = matching_values if current_text: self.event_generate('<Down>') self.event_generate('<Down>') else: self['values'] = [] try: self.tk.call('ttk::combobox::PopdownWindow', self, 'hide') except tk.TclError: pass def _add_to_recent(self, value: Any): """ Add a value to recent items list. Args: value: Value to add to recent items """ if not self._enable_recent or not value: return # Remove if already exists if value in self._recent_items: self._recent_items.remove(value) # Add to front self._recent_items.insert(0, value) # Limit size if len(self._recent_items) > self._max_recent: self._recent_items = self._recent_items[:self._max_recent] self._save_recent_items() def _load_recent_items(self): """Load recent items from persistent storage.""" # In a real implementation, this could load from a file or database # For now, just initialize empty list pass def _save_recent_items(self): """Save recent items to persistent storage.""" # In a real implementation, this could save to a file or database pass def _on_listbox_select(self): """Handle selection and track recent items.""" super()._on_listbox_select() current_text = self.get() if current_text: self._add_to_recent(current_text) def _on_return(self, event): """Handle Return key and track recent items.""" super()._on_return(event) current_text = self.get() if current_text: self._add_to_recent(current_text) class AutocompleteCombobox(ttk