From bedff7580700dd082f16dd76cb4a49f5100c2519 Mon Sep 17 00:00:00 2001 From: JSGRD22 <73097618+JSGRD22@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:26:44 +0800 Subject: [PATCH] Replace searchable combobox with scrollable search list --- LivescriptV2.01.py | 182 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 151 insertions(+), 31 deletions(-) diff --git a/LivescriptV2.01.py b/LivescriptV2.01.py index 72f86e1..c403c64 100644 --- a/LivescriptV2.01.py +++ b/LivescriptV2.01.py @@ -204,50 +204,170 @@ def hidetip(self): self.tooltip_window = None -class SearchableComboBox(ctk.CTkComboBox): +class SearchableComboBox(ctk.CTkFrame): """ - A CTkComboBox that filters its values based on user input. + Search field with an inline scrollable option list. """ + MAX_VISIBLE_ROWS = 12 - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._original_values = self.cget("values")[:] + def __init__(self, master, values=None, command=None, state="normal", width=140, height=28, **kwargs): + super().__init__(master=master, fg_color="transparent", **kwargs) + self._original_values = list(values or []) + self._values = self._original_values[:] + self._command = command + self._state = state + + self.grid_columnconfigure(0, weight=1) + + self._input_frame = ctk.CTkFrame(self, fg_color="transparent") + self._input_frame.grid(row=0, column=0, sticky="ew") + self._input_frame.grid_columnconfigure(0, weight=1) + + self._entry = ctk.CTkEntry(self._input_frame, width=width, height=height) + self._entry.grid(row=0, column=0, sticky="ew") + + self._toggle_button = ctk.CTkButton(self._input_frame, text="v", width=height, height=height, + command=self._toggle_list) + self._toggle_button.grid(row=0, column=1, padx=(4, 0)) + + self._list_frame = tk.Frame(self, bd=1, relief="solid") + self._listbox = tk.Listbox(self._list_frame, activestyle="none", exportselection=False) + self._scrollbar = tk.Scrollbar(self._list_frame, orient="vertical", command=self._listbox.yview) + self._listbox.configure(yscrollcommand=self._scrollbar.set) + self._listbox.pack(side="left", expand=True, fill="both") self._entry.bind("", self._on_keyrelease) - # Use a custom command wrapper to reset values after selection - self._original_command = self.cget("command") - self.configure(command=self._on_selection) + self._entry.bind("", self._select_all_text) + self._entry.bind("", self._select_all_text) + self._listbox.bind("", self._select_from_list) + self._listbox.bind("", self._select_from_list) + self._listbox.bind("", lambda event: self._hide_list()) + self._listbox.bind("", self._scroll_list) + self._listbox.bind("", self._scroll_list) + self._listbox.bind("", self._scroll_list) + + self._apply_state() + self._refresh_values(self._values) + + def configure(self, require_redraw=False, **kwargs): + if "values" in kwargs: + self._original_values = list(kwargs.pop("values")) + self._values = self._original_values[:] + self._refresh_values(self._values) + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "state" in kwargs: + self._state = kwargs.pop("state") + self._apply_state() + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name): + if attribute_name == "values": + return self._original_values[:] + if attribute_name == "command": + return self._command + if attribute_name == "state": + return self._state + return super().cget(attribute_name) + + def get(self): + return self._entry.get() - def _on_keyrelease(self, event): - """Filters the combobox values based on the entry text.""" - # Don't filter on navigation keys - if event.keysym in ("Up", "Down", "Return", "Escape"): + def set(self, value): + self._entry.delete(0, tk.END) + self._entry.insert(0, value) + self._values = self._original_values[:] + self._refresh_values(self._values) + self._hide_list() + + def _apply_state(self): + state = "disabled" if self._state == "disabled" else "normal" + self._entry.configure(state=state) + self._toggle_button.configure(state=state) + if state == "disabled": + self._hide_list() + + def _toggle_list(self): + if self._state == "disabled": return + if self._list_frame.winfo_ismapped(): + self._hide_list() + else: + self._filter_values(show_list=True) - current_text = self.get().lower() + def _show_list(self): + if self._state == "disabled" or not self._values: + self._hide_list() + return + + visible_rows = min(len(self._values), self.MAX_VISIBLE_ROWS) + self._listbox.configure(height=visible_rows) + self._list_frame.grid(row=1, column=0, sticky="ew", pady=(4, 0)) + + def _hide_list(self): + self._list_frame.grid_remove() + + def _refresh_values(self, values): + self._listbox.delete(0, tk.END) + for value in values: + self._listbox.insert(tk.END, value) + if len(values) > self.MAX_VISIBLE_ROWS: + self._scrollbar.pack(side="right", fill="y") + else: + self._scrollbar.pack_forget() + + def _filter_values(self, show_list=True): + current_text = self.get().lower() if current_text: - filtered_values = [v for v in self._original_values if current_text in v.lower()] + self._values = [value for value in self._original_values if current_text in value.lower()] else: - filtered_values = self._original_values[:] + self._values = self._original_values[:] - # Update the values and reopen the dropdown to show the filtered list - self.configure(values=filtered_values) - if self._dropdown_menu is not None and self._dropdown_menu.winfo_exists(): - self._dropdown_menu.destroy() - self._open_dropdown_menu() + self._refresh_values(self._values) + if show_list: + self._show_list() - def _on_selection(self, value): - """Called when a value is selected. Resets the list and calls original command.""" - self.set(value) # Set the value in the entry - self.configure(values=self._original_values) # Reset the list - if self._original_command: - self._original_command(value) + def _select_all_text(self, event=None): + self._entry.select_range(0, tk.END) + self._entry.icursor(tk.END) + return "break" - def set(self, value): - """Sets the value and resets the list of choices.""" - super().set(value) - self.configure(values=self._original_values) + def _scroll_list(self, event): + if getattr(event, "num", None) == 4 or getattr(event, "delta", 0) > 0: + self._listbox.yview_scroll(-1, "units") + else: + self._listbox.yview_scroll(1, "units") + return "break" + + def _select_from_list(self, event=None): + selection = self._listbox.curselection() + if not selection: + return + + value = self._values[selection[0]] + self.set(value) + if self._command: + self._command(value) + + def _on_keyrelease(self, event): + if event.keysym == "Escape": + self._hide_list() + return + if event.keysym == "Down" and self._values: + self._show_list() + self._listbox.focus_set() + self._listbox.selection_clear(0, tk.END) + self._listbox.selection_set(0) + self._listbox.activate(0) + return + if event.keysym in ("Up", "Return"): + return + + self._filter_values(show_list=True) class SettingsManager: @@ -1107,4 +1227,4 @@ def restart_application(): if __name__ == "__main__": - main() \ No newline at end of file + main()