I've made a Tkinter widget that is essentially a Combobox
whose list 1) is visible during typing, 2) shortens during typing and 3) allows for arrow key/enter functionality. I used these two posts (first & second) to figure out some of the functionality. See below for class code and MRE.
My question: My Search
widget works for Windows but not Mac OS, and I believe it is due to the fact that the .wm_overrideredirect()
method behaves differently in Mac OS (at least for Big Sur) vs Windows. My intended functionality is to have a TopLevel
widget only contain & show the Text
widget and not receive focus. The code below achieves this in Windows but not in Mac OS. What is an alternative for Mac OS? In other words, are there any alternatives for allowing widgets to extend outside of root
?
Clicking on entry widget when running on Windows 10 does the following:
Clicking on entry widget when running on Mac OS Big Sur does the following:
Thanks!
Search Widget
from tkinter import Entry, Text, StringVar, Toplevel, WORD, END
class Search:
'''A widget with features of Entry and Combobox that allows for a dropdown list
that 1) dynamically reduces listed options to those that match user input and
2) allows for arrow key/enter and mouseover/click functionality.'''
def __init__(self, root, lst, width=15, allow_unlisted=True):
self.root = root
self.lst = lst
self.current_lst = lst
self.width = width
self.allow_unlisted = allow_unlisted
self.indices = [0.0] * 2
self.stringvar = StringVar()
self.stringvar.trace_add("write", self.updateList)
self.entry = Entry(root, width=self.width, font=("Arial", 9), textvariable=self.stringvar)
self.entry.bind("<FocusIn>", self.showList)
self.entry.bind("<FocusOut>", self.hideList)
self.entry.bind("<Up>", self.keyList)
self.entry.bind("<Down>", self.keyList)
self.entry.bind("<Return>", self.hideList)
self.toplevel = None
self.text = None
self.updateList()
def showList(self, *args):
self.toplevel = Toplevel(self.root)
self.toplevel.wm_overrideredirect(True)
x = self.entry.winfo_rootx()
y = self.entry.winfo_rooty() + self.entry.winfo_height()
self.toplevel.wm_geometry(f"+{x}+{y}")
self.text = Text(self.toplevel, wrap=WORD, foreground="#333333")
self.text.config(font=("Arial", 9), width=self.width)
self.text.tag_configure("highlight", background="dodger blue", foreground="white")
self.text.bind("<Motion>", self.motionList)
self.text.bind("<Up>", self.keyList)
self.text.bind("<Down>", self.keyList)
self.text.bind("<Button-1>", self.hideList)
self.text.bind("<Return>", self.hideList)
self.text.pack()
self.updateList()
def hideList(self, *args):
if self.text and self.text.winfo_ismapped():
self.select()
if self.toplevel:
self.toplevel.destroy()
self.toplevel = self.text = None
self.root.focus_set()
def updateList(self, *args):
var = self.stringvar.get()
self.indices = [0.0] * 2
self.current_lst = [item for item in self.lst if var.lower() in item.lower()]
if self.text:
self.text.tag_remove("highlight", 1.0, END)
self.text.config(height=min(10, len(self.current_lst)))
self.text.delete(1.0, END)
self.text.insert(END, "\n".join(self.current_lst))
def motionList(self, event):
index1 = float(self.text.index(f"@{event.x},{event.y} linestart"))
self.highlight(index1)
def keyList(self, event):
if event.keysym == "Up" and self.indices[0] > 0:
self.indices[0] -= 1
elif event.keysym == "Down" and self.indices[0] < len(self.current_lst):
self.indices[0] += 1
if self.indices[0]:
self.highlight(self.indices[0])
else:
self.indices = [0.0] * 2
self.text.tag_remove("highlight", 1.0, END)
def highlight(self, index1):
self.indices = [index1, float(self.text.index(f"{index1} lineend"))]
self.text.tag_remove("highlight", 1.0, END)
self.text.tag_add("highlight", *self.indices)
def select(self, *args):
if self.indices[0]:
self.stringvar.set(self.text.get(*self.indices))
def get(self):
user_input = self.stringvar.get()
if user_input in self.lst or self.allow_unlisted:
return user_input
else: return ""
def set(self, user_input):
self.stringvar.set(user_input)
def __getattr__(self, name):
return lambda *args, **kwargs: getattr(Entry, name)(self.entry, *args, **kwargs)
MRE
from tkinter import Tk, Label
from Search import Search # or however you want to import this
# example options for widget
options = ["Option 1", "Option 2", "Option 3", "Rabala", "Ploplepla",
"This is a very long option compared to others",
"OPTION 1", "option 2", "option 3", "option1", "option2",
"option3", "oPTION1"]
root = Tk()
l0 = Label(root, text="Widget:")
r = Search(root, lst=options)
l0.grid(row=0, column=0, padx=10, pady=50)
r.grid(row=0, column=1, padx=10, pady=50)
root.mainloop()