Skip to content

Commit

Permalink
FEATURE: Parameter name search and autocompletion
Browse files Browse the repository at this point in the history
# Conflicts:
#	MethodicConfigurator/frontend_tkinter_parameter_editor_table.py
  • Loading branch information
amilcarlucas committed Aug 28, 2024
1 parent b76fc6c commit 2605eb9
Show file tree
Hide file tree
Showing 2 changed files with 309 additions and 1 deletion.
268 changes: 268 additions & 0 deletions MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
# https://code.activestate.com/recipes/580770-combobox-autocomplete/

import re

from tkinter import StringVar, Entry, Frame, Listbox, Scrollbar
from tkinter.constants import *

Check failure on line 6 in MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py

View workflow job for this annotation

GitHub Actions / build (3.12)

Ruff (F403)

MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py:6:1: F403 `from tkinter.constants import *` used; unable to detect undefined names


def autoscroll(sbar, first, last):
"""Hide and show scrollbar as needed."""
first, last = float(first), float(last)
if first <= 0 and last >= 1:
sbar.grid_remove()
else:
sbar.grid()
sbar.set(first, last)


class Combobox_Autocomplete(Entry, object):
def __init__(self, master, list_of_items=None, autocomplete_function=None, listbox_width=None, listbox_height=7, ignorecase_match=False, startswith_match=True, vscrollbar=True, hscrollbar=True, **kwargs):
if hasattr(self, "autocomplete_function"):
if autocomplete_function is not None:
raise ValueError("Combobox_Autocomplete subclass has 'autocomplete_function' implemented")
else:
if autocomplete_function is not None:
self.autocomplete_function = autocomplete_function
else:
if list_of_items is None:
raise ValueError("If not given complete function, list_of_items can't be 'None'")

if ignorecase_match:
if startswith_match:
def matches_function(entry_data, item):
return item.startswith(entry_data)
else:
def matches_function(entry_data, item):
return item in entry_data

self.autocomplete_function = lambda entry_data: [item for item in self.list_of_items if matches_function(entry_data, item)]
else:
if startswith_match:
def matches_function(escaped_entry_data, item):
if re.match(escaped_entry_data, item, re.IGNORECASE):
return True
else:
return False
else:
def matches_function(escaped_entry_data, item):
if re.search(escaped_entry_data, item, re.IGNORECASE):
return True
else:
return False

def autocomplete_function(entry_data):
escaped_entry_data = re.escape(entry_data)
return [item for item in self.list_of_items if matches_function(escaped_entry_data, item)]

self.autocomplete_function = autocomplete_function

self._listbox_height = int(listbox_height)
self._listbox_width = listbox_width

self.list_of_items = list_of_items

self._use_vscrollbar = vscrollbar
self._use_hscrollbar = hscrollbar

kwargs.setdefault("background", "white")

if "textvariable" in kwargs:
self._entry_var = kwargs["textvariable"]
else:
self._entry_var = kwargs["textvariable"] = StringVar()

Entry.__init__(self, master, **kwargs)

self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)

self._listbox = None

self.bind("<Tab>", self._on_tab)
self.bind("<Up>", self._previous)
self.bind("<Down>", self._next)
self.bind('<Control-n>', self._next)
self.bind('<Control-p>', self._previous)

self.bind("<Return>", self._update_entry_from_listbox)
self.bind("<Escape>", lambda event: self.unpost_listbox())

def _on_tab(self, event):
self.post_listbox()
return "break"

def _on_change_entry_var(self, name, index, mode):

entry_data = self._entry_var.get()

if entry_data == '':
self.unpost_listbox()
self.focus()
else:
values = self.autocomplete_function(entry_data)
if values:
if self._listbox is None:
self._build_listbox(values)
else:
self._listbox.delete(0, END)

Check failure on line 107 in MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py

View workflow job for this annotation

GitHub Actions / build (3.12)

Ruff (F405)

MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py:107:45: F405 `END` may be undefined, or defined from star imports

height = min(self._listbox_height, len(values))
self._listbox.configure(height=height)

for item in values:
self._listbox.insert(END, item)

Check failure on line 113 in MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py

View workflow job for this annotation

GitHub Actions / build (3.12)

Ruff (F405)

MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py:113:46: F405 `END` may be undefined, or defined from star imports

else:
self.unpost_listbox()
self.focus()

def _build_listbox(self, values):
listbox_frame = Frame()

self._listbox = Listbox(listbox_frame, background="white", selectmode=SINGLE, activestyle="none", exportselection=False)

Check failure on line 122 in MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py

View workflow job for this annotation

GitHub Actions / build (3.12)

Ruff (F405)

MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py:122:79: F405 `SINGLE` may be undefined, or defined from star imports
self._listbox.grid(row=0, column=0,sticky = N+E+W+S)

Check failure on line 123 in MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py

View workflow job for this annotation

GitHub Actions / build (3.12)

Ruff (F405)

MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py:123:53: F405 `N` may be undefined, or defined from star imports

Check failure on line 123 in MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py

View workflow job for this annotation

GitHub Actions / build (3.12)

Ruff (F405)

MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py:123:55: F405 `E` may be undefined, or defined from star imports

Check failure on line 123 in MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py

View workflow job for this annotation

GitHub Actions / build (3.12)

Ruff (F405)

MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py:123:57: F405 `W` may be undefined, or defined from star imports

Check failure on line 123 in MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py

View workflow job for this annotation

GitHub Actions / build (3.12)

Ruff (F405)

MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py:123:59: F405 `S` may be undefined, or defined from star imports

self._listbox.bind("<ButtonRelease-1>", self._update_entry_from_listbox)
self._listbox.bind("<Return>", self._update_entry_from_listbox)
self._listbox.bind("<Escape>", lambda event: self.unpost_listbox())

self._listbox.bind('<Control-n>', self._next)
self._listbox.bind('<Control-p>', self._previous)

if self._use_vscrollbar:
vbar = Scrollbar(listbox_frame, orient=VERTICAL, command= self._listbox.yview)

Check failure on line 133 in MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py

View workflow job for this annotation

GitHub Actions / build (3.12)

Ruff (F405)

MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py:133:52: F405 `VERTICAL` may be undefined, or defined from star imports
vbar.grid(row=0, column=1, sticky=N+S)

Check failure on line 134 in MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py

View workflow job for this annotation

GitHub Actions / build (3.12)

Ruff (F405)

MethodicConfigurator/frontend_tkinter_combobox_autocomplete.py:134:47: F405 `N` may be undefined, or defined from star imports

self._listbox.configure(yscrollcommand= lambda f, l: autoscroll(vbar, f, l))

if self._use_hscrollbar:
hbar = Scrollbar(listbox_frame, orient=HORIZONTAL, command= self._listbox.xview)
hbar.grid(row=1, column=0, sticky=E+W)

self._listbox.configure(xscrollcommand= lambda f, l: autoscroll(hbar, f, l))

listbox_frame.grid_columnconfigure(0, weight= 1)
listbox_frame.grid_rowconfigure(0, weight= 1)

x = -self.cget("borderwidth") - self.cget("highlightthickness")
y = self.winfo_height()-self.cget("borderwidth") - self.cget("highlightthickness")

if self._listbox_width:
width = self._listbox_width
else:
width=self.winfo_width()

listbox_frame.place(in_=self, x=x, y=y, width=width)

height = min(self._listbox_height, len(values))
self._listbox.configure(height=height)

for item in values:
self._listbox.insert(END, item)

def post_listbox(self):
if self._listbox is not None: return

entry_data = self._entry_var.get()
if entry_data == '': return

values = self.autocomplete_function(entry_data)
if values:
self._build_listbox(values)

def unpost_listbox(self):
if self._listbox is not None:
self._listbox.master.destroy()
self._listbox = None

def get_value(self):
return self._entry_var.get()

def set_value(self, text, close_dialog=False):
self._set_var(text)

if close_dialog:
self.unpost_listbox()

self.icursor(END)
self.xview_moveto(1.0)

def _set_var(self, text):
self._entry_var.trace_remove("w", self._trace_id)
self._entry_var.set(text)
self._trace_id = self._entry_var.trace_add('w', self._on_change_entry_var)

def _update_entry_from_listbox(self, event):
if self._listbox is not None:
current_selection = self._listbox.curselection()

if current_selection:
text = self._listbox.get(current_selection)
self._set_var(text)

self._listbox.master.destroy()
self._listbox = None

self.focus()
self.icursor(END)
self.xview_moveto(1.0)

return "break"

def _previous(self, event):
if self._listbox is not None:
current_selection = self._listbox.curselection()

if len(current_selection)==0:
self._listbox.selection_set(0)
self._listbox.activate(0)
else:
index = int(current_selection[0])
self._listbox.selection_clear(index)

if index == 0:
index = END
else:
index -= 1

self._listbox.see(index)
self._listbox.selection_set(first=index)
self._listbox.activate(index)

return "break"

def _next(self, event):
if self._listbox is not None:

current_selection = self._listbox.curselection()
if len(current_selection)==0:
self._listbox.selection_set(0)
self._listbox.activate(0)
else:
index = int(current_selection[0])
self._listbox.selection_clear(index)

if index == self._listbox.size() - 1:
index = 0
else:
index +=1

self._listbox.see(index)
self._listbox.selection_set(index)
self._listbox.activate(index)
return "break"

if __name__ == '__main__':
from tkinter import Tk

list_of_items = ["Cordell Cannata", "Lacey Naples", "Zachery Manigault", "Regan Brunt", "Mario Hilgefort", "Austin Phong", "Moises Saum", "Willy Neill", "Rosendo Sokoloff", "Salley Christenberry", "Toby Schneller", "Angel Buchwald", "Nestor Criger", "Arie Jozwiak", "Nita Montelongo", "Clemencia Okane", "Alison Scaggs", "Von Petrella", "Glennie Gurley", "Jamar Callender", "Titus Wenrich", "Chadwick Liedtke", "Sharlene Yochum", "Leonida Mutchler", "Duane Pickett", "Morton Brackins", "Ervin Trundy", "Antony Orwig", "Audrea Yutzy", "Michal Hepp", "Annelle Hoadley", "Hank Wyman", "Mika Fernandez", "Elisa Legendre", "Sade Nicolson", "Jessie Yi", "Forrest Mooneyhan", "Alvin Widell", "Lizette Ruppe", "Marguerita Pilarski", "Merna Argento", "Jess Daquila", "Breann Bevans", "Melvin Guidry", "Jacelyn Vanleer", "Jerome Riendeau", "Iraida Nyquist", "Micah Glantz", "Dorene Waldrip", "Fidel Garey", "Vertie Deady", "Rosalinda Odegaard", "Chong Hayner", "Candida Palazzolo", "Bennie Faison", "Nova Bunkley", "Francis Buckwalter", "Georgianne Espinal", "Karleen Dockins", "Hertha Lucus", "Ike Alberty", "Deangelo Revelle", "Juli Gallup", "Wendie Eisner", "Khalilah Travers", "Rex Outman", "Anabel King", "Lorelei Tardiff", "Pablo Berkey", "Mariel Tutino", "Leigh Marciano", "Ok Nadeau", "Zachary Antrim", "Chun Matthew", "Golden Keniston", "Anthony Johson", "Rossana Ahlstrom", "Amado Schluter", "Delila Lovelady", "Josef Belle", "Leif Negrete", "Alec Doss", "Darryl Stryker", "Michael Cagley", "Sabina Alejo", "Delana Mewborn", "Aurelio Crouch", "Ashlie Shulman", "Danielle Conlan", "Randal Donnell", "Rheba Anzalone", "Lilian Truax", "Weston Quarterman", "Britt Brunt", "Leonie Corbett", "Monika Gamet", "Ingeborg Bello", "Angelique Zhang", "Santiago Thibeau", "Eliseo Helmuth"]

root = Tk()
root.geometry("300x200")

combobox_autocomplete = Combobox_Autocomplete(root, list_of_items, highlightthickness=1, startswith_match=False)
combobox_autocomplete.pack()

combobox_autocomplete.focus()

root.mainloop()
42 changes: 41 additions & 1 deletion MethodicConfigurator/frontend_tkinter_parameter_editor_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@

from frontend_tkinter_connection_selection import PairTupleCombobox

from frontend_tkinter_combobox_autocomplete import Combobox_Autocomplete

from annotate_params import Par


Expand Down Expand Up @@ -424,7 +426,45 @@ def __on_parameter_delete(self, param_name):

def __on_parameter_add(self, fc_parameters):
# Prompt the user for a parameter name
param_name = simpledialog.askstring("New parameter name", "Enter new parameter name:")
# param_name = simpledialog.askstring("New parameter name", "Enter new parameter name:")

add_parameter_window = tk.Toplevel(self.root)
add_parameter_window.title("Add Parameter")

# Label for instruction
instruction_label = tk.Label(add_parameter_window, text="Enter new parameter name:")
instruction_label.pack(pady=5)

# ComboBox for dynamic filtering
parameter_name_combobox = Combobox_Autocomplete(add_parameter_window, self.local_filesystem.doc_dict.keys(), highlightthickness=1, startswith_match=False)
parameter_name_combobox.pack(padx=5, pady=5)
parameter_name_combobox.focus()

#parameter_name_combobox = ttk.Combobox(add_parameter_window, values=[])
#parameter_name_combobox.pack(pady=5)

# Function to update "as you type" the ComboBox options
#def update_combobox_options(*args):
# search_term = parameter_name_combobox.get().upper()
# if len(search_term) >= 3:
# matching_keys = [key for key in self.local_filesystem.doc_dict.keys() if search_term in key]
# parameter_name_combobox["values"] = matching_keys
# Automatically select the first item if there's only one match
# if len(matching_keys) == 1:
# parameter_name_combobox.set(matching_keys[0])

# Bind the Entry widget to update the ComboBox options
#parameter_name_combobox.bind("<KeyRelease>", update_combobox_options)

# Additional bindings to handle Enter press and selection
#parameter_name_combobox.bind("<Return>", lambda event: self.__confirm_parameter_addition(parameter_name_combobox.get().upper(), fc_parameters))
parameter_name_combobox.bind("<<ComboboxSelected>>", lambda event: self.__confirm_parameter_addition(parameter_name_combobox.get().upper(), fc_parameters))

# Button to confirm the addition
#confirm_button = tk.Button(add_parameter_window, text="Confirm", command=lambda fc_parameters=fc_parameters: self.__confirm_parameter_addition(parameter_name_combobox.get().upper(), fc_parameters))
#confirm_button.pack(pady=5)

def __confirm_parameter_addition(self, param_name: str, fc_parameters: dict):
if not param_name:
messagebox.showerror("Parameter name can not be empty.")
return
Expand Down

0 comments on commit 2605eb9

Please sign in to comment.