Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 151 additions & 31 deletions LivescriptV2.01.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<KeyRelease>", 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("<Control-a>", self._select_all_text)
self._entry.bind("<Control-A>", self._select_all_text)
self._listbox.bind("<ButtonRelease-1>", self._select_from_list)
self._listbox.bind("<Return>", self._select_from_list)
self._listbox.bind("<Escape>", lambda event: self._hide_list())
self._listbox.bind("<MouseWheel>", self._scroll_list)
self._listbox.bind("<Button-4>", self._scroll_list)
self._listbox.bind("<Button-5>", 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:
Expand Down Expand Up @@ -1107,4 +1227,4 @@ def restart_application():


if __name__ == "__main__":
main()
main()