widgets.py
Nov 25, 2022 18:17:00 GMT -8
Post by Uncle Buddy on Nov 25, 2022 18:17:00 GMT -8
<drive>:\treebard\widgets.py Last Changed 2024-07-25
# widgets.py
import tkinter as tk
import sqlite3
from base import Query, tbard_path
from new_tree import EVENT_TYPES
from base import COLOR_STRINGS
import dev_tools as dt
from dev_tools import look, seeline
# Tkinter errors like...
# ...File "C:\Users\<user>\AppData\Local\Programs\Pyth...\Lib\tkinter\__init__.py", line 1711, in _configure
# self.tk.call(_flatten((self._w, cmd)) + self._options(cnf))
# _tkinter.TclError: unknown option "-fg"
# ...usually mean that a widget class has not been listed in one of the
# `styles` dict keys. This is an idiosynchracy of Treebard's colorizer.
NEUTRAL_COLOR = "gray"
class Scaler(tk.Label):
def __init__(self, master, formats, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
self.master = master
self.formats = formats
self.config(bd=2, relief="ridge")
self.config(text="", width=12)
self.grid(sticky="ew")
self.grid_remove()
self.bind("<Button-1>", self.fix)
self.bind("<Enter>", self.show_tip)
self.bind("<Leave>", self.unshow_tip)
def fix(self, evt):
self.master.update_idletasks()
widg = evt.widget
click_x = round(
((widg.winfo_pointerx() - widg.winfo_rootx()) / widg.winfo_reqwidth() * 5), 1)
widg["text"] = click_x
def show_tip(self, evt):
widg = evt.widget
tip = tk.Toplevel(self.master, bd=1)
tip.overrideredirect(1)
evt.widget.bind("<Leave>", lambda event, tip=tip: self.unshow_tip(event, tip))
x, y, cx, cy = widg.bbox("insert")
x = x + widg.winfo_rootx() + 32
y = y + cy + widg.winfo_rooty() + 32
tip.wm_geometry(f"+{x}+{y}")
hint = tk.Label(
tip, text="CLICK L/R", bg=self.formats["head_bg"],
fg=self.formats["fg"], justify='left', relief='solid', bd=0)
hint.pack(ipadx=6, ipady=6)
def unshow_tip(self, evt, tip):
tip.destroy()
def get(self):
return self.cget("text")
def set(self, text):
self["text"] = str(text)
class Cell(tk.Text):
def __init__(self, master, *args, **kwargs):
tk.Text.__init__(self, master, *args, **kwargs)
self["wrap"] = "word"
self["bd"] = 0
self.dlg = self.winfo_toplevel()
self.bind("<Tab>", self.focus_next_window)
self.bind("<Shift-Tab>", self.focus_prev_window)
self.bind("<FocusIn>", self.highlight)
self.bind("<FocusOut>", self.unhighlight)
self.tip = {
# for kintips
"event_id": None, "id": None, "name": None, "kin_type": None,
# for nametips
"name_type": None, "subject_id": None, "subject_name": None}
def set_height(self, max_width=32):
""" After gridding this widget, run `self.set_height()`. The answer
returned by `displaylines` is wrong first time thru mainloop so
`update_idletasks()` has to run.
Until the GUI is actually drawn on the screen, this only works
right for monospaced fonts such as Courier, Dejavu Sans Mono, etc.
The solution is to not run this method as soon as the widget is
created or as soon as the widget's text is assigned. This works
for any font only when run in `configall` after the widgets have
been drawn with the font actually being used.
"""
# Set width:
min_width = 6
text_width = len(self.get("1.0", "end-1c"))
if text_width > max_width:
self["width"] = max_width
elif text_width > 6:
self["width"] = text_width
else:
self["width"] = min_width
# Set height:
self.tag_configure('left', justify='left')
self.tag_add('left', '1.0', 'end')
self.update_idletasks()
lines = self.count('1.0', 'end', 'displaylines')
self["height"] = lines
def highlight(self, evt):
self["bg"] = self.dlg.formats["head_bg"]
def unhighlight(self, evt):
self["bg"] = self.dlg.formats["bg"]
# Make the Text widget use the TAB key for traversal like other widgets.
# `return('break')` prevents the built-in binding to TAB.
def focus_next_window(self, evt):
evt.widget.tk_focusNext().focus()
return("break")
def focus_prev_window(self, evt):
evt.widget.tk_focusPrev().focus()
return("break")
class CellHilited(Cell):
def __init__(self, master, formats, *args, **kwargs):
Cell.__init__(self, master, *args, **kwargs)
self.formats = formats
def highlight(self, evt):
self["bg"] = self.formats["head_bg"]
def unhighlight(self, evt):
self["bg"] = self.formats["bg"]
class CellAutoPlace(Cell):
def __init__(self, master, tree, values=[], *args, **kwargs):
Cell.__init__(self, master, *args, **kwargs)
self.master = master
self.tree = tree
self.values = values
self.tree.place_autofill_inputs.append(self)
self.autofilled = None
self.config(bd=0)
self.bind("<KeyPress>", self.detect_pressed)
self.bind("<KeyRelease>", self.get_typed)
def detect_pressed(self, evt):
""" Run on every key press. """
key = evt.keysym
if len(key) == 1:
self.pos = self.index('insert')
line, char = self.pos.split(".")
keep = self.get("1.0", "end")[0:int(char)]
self.delete("1.0", 'end')
self.insert("1.0", keep)
def get_typed(self, evt):
""" Run on every key release; filters out most non-alpha-numeric
keys; runs the functions not triggered by events.
"""
def do_it():
hits = self.match_string()
self.show_hits(hits, self.pos)
key = evt.keysym
# allow alphanumeric characters
if len(key) == 1:
do_it()
# allow hyphens and apostrophes
elif key in ('minus', 'quoteright'):
do_it()
# look for other chars that should be allowed in nested names
else:
pass
def match_string(self):
hits = []
use_list = self.tree.prepended_places + self.tree.place_autofill_values
for item in use_list:
if item.lower().startswith(self.get("1.0", "end-1c").lower()):
hits.append(item)
return hits
def show_hits(self, hits, pos):
if len(hits) != 0:
self.autofilled = hits[0]
self.delete("1.0", 'end')
self.insert("1.0", self.autofilled)
line, char = pos.split(".")
cursor = int(char) + 1
self.mark_set("insert", f"{line}.{str(cursor)}")
self.set_height()
class CellAutofill(Cell):
def __init__(self, master, values=None, *args, **kwargs):
Cell.__init__(self, master, *args, **kwargs)
self.master = master
self.values = values
self.bind("<KeyPress>", self.detect_pressed)
self.bind("<KeyRelease>", self.get_typed)
def detect_pressed(self, evt):
""" Runs on every key press. """
key = evt.keysym
if len(key) == 1:
self.pos = self.index('insert')
line, char = self.pos.split(".")
keep = self.get("1.0", "end")[0:int(char)]
self.delete("1.0", 'end')
self.insert("1.0", keep)
def get_typed(self, evt):
""" Run on every key release; filters out most non-alpha-numeric
keys; runs the functions not triggered by events.
"""
def do_it():
hits = self.match_string()
self.show_hits(hits, self.pos)
key = evt.keysym
# allow alphanumeric characters
if len(key) == 1:
do_it()
# allow hyphens and apostrophes
elif key in ('minus', 'quoteright'):
do_it()
# look for other chars that should be allowed in nested names
else:
pass
def match_string(self):
hits = []
got = self.get("1.0", "end").replace("\n", "")
use_list = self.values
for item in use_list:
if item.lower().startswith(got.lower()):
hits.append(item)
return hits
def show_hits(self, hits, pos):
if len(hits) != 0:
self.autofilled = hits[0]
self.delete("1.0", 'end')
self.insert("1.0", self.autofilled)
line, char = pos.split(".")
cursor = int(char) + 1
self.mark_set("insert", f"{line}.{str(cursor)}")
self.set_height()
class CellAutoEventType(CellAutofill):
autofill_values = []
def create_lists():
CellAutoEventType.autofill_values = [i[1] for i in EVENT_TYPES]
def __init__(self, master, *args, **kwargs):
CellAutofill.__init__(self, master, *args, **kwargs)
pass
class ToolTip(object):
""" Tooltips by Michael Foord
(used for ribbon menu icons and widgets dynamically gridded).
Don't use for anything that'll be destroyed by clicking because
tooltips are displayed by pointing w/ mouse and thus a tooltip
will be displaying when destroy takes place thus leaving the
tooltip on the screen since the FocusOut that is supposed to
destroy the tooltip can't take place.
"""
def __init__(self, widget, formats):
self.widget = widget
self.formats = formats
self.tipwindow = None
self.id = None
self.x = self.y = 0
def showtip(self, text):
""" Display text in tooltip window. It seems these tooltips don't
work with Treebard's Colorizer since this class is not a widget.
"""
self.text = text
if self.tipwindow or not self.text:
return
x, y, cx, cy = self.widget.bbox("insert")
x = x + self.widget.winfo_rootx() + 27
y = y + cy + self.widget.winfo_rooty() + 27
self.tipwindow = tw = tk.Toplevel(self.widget, bd=1)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
try:
# For Mac OS
tw.tk.call("::tk::unsupported::MacWindowStyle",
"style", tw._w,
"help", "noActivates")
except tk.TclError:
pass
self.label = tk.Label(
tw, None, text=self.text, justify='left', relief='solid',
bd=0, bg=NEUTRAL_COLOR, fg="black")
self.label.pack(ipadx=6)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
def create_tooltip(widget, text, formats):
""" Call w/ arguments to use M. Foord's ToolTip class. """
def enter(event):
toolTip.showtip(text)
def leave(event):
toolTip.hidetip()
toolTip = ToolTip(widget, formats)
widget.bind('<Enter>', enter, add="+")
widget.bind('<Leave>', leave, add="+")
class ButtonBigPic(tk.Button):
""" Used for the big buttons that open galleries and for the tree pics on
the main app.
"""
def __init__(self, master, *args, **kwargs):
tk.Button.__init__(self, master, *args, **kwargs)
dlg = self.winfo_toplevel()
self.config(bd=0, relief="flat", cursor='hand2')
self.bind("<FocusIn>", lambda evt: self.highlight(evt=evt, dlg=dlg))
self.bind("<FocusOut>", lambda evt: self.unhighlight(evt=evt, dlg=dlg))
def highlight(self, evt, dlg):
evt.widget.config(bg=dlg.formats["head_bg"])
def unhighlight(self, evt, dlg):
evt.widget.config(bg=dlg.formats["bg"])
class Checkbox(tk.Frame):
def __init__(self, master, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
dlg = self.winfo_toplevel()
self.bind("<FocusIn>", lambda evt: self.highlight(evt=evt, dlg=dlg))
self.bind("<FocusOut>", lambda evt: self.unhighlight(evt=evt, dlg=dlg))
self.bind("<Enter>", lambda evt: self.highlight(evt=evt, dlg=dlg))
self.bind("<Leave>", lambda evt: self.unhighlight(evt=evt, dlg=dlg))
self.bind("<space>", lambda evt: self.add_check(evt=evt, dlg=dlg))
self.config(takefocus=1)
self.label = tk.Label(self, width=1, height=1)
self.label.pack(padx=1, pady=1, ipadx=4)
self.label.bind("<Button-1>", self.add_check)
def highlight(self, evt, dlg):
self.config(bg=dlg.formats["highlight_bg"])
def unhighlight(self, evt, dlg):
self.config(bg=dlg.formats["fg"])
def add_check(self, evt):
if self.label.cget("text") == "X":
self.label.config(text="")
else:
self.label.config(text="X")
class LabelHover(tk.Label):
def __init__(self, master, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
dlg = self.winfo_toplevel()
self.bind(
'<Enter><FocusIn>', lambda evt: self.highlight(evt=evt, dlg=dlg))
self.bind(
'<Leave><FocusOut>', lambda evt: self.unhighlight(evt=evt, dlg=dlg))
def highlight(self, evt, dlg):
self.config(bg=dlg.formats["highlight_bg"])
def unhighlight(self, evt, dlg):
self.config(bg=dlg.formats["bg"])
class LabelButtonText(tk.Label):
""" A label that looks and works like a button. """
def __init__(self, master, width=8, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
dlg = self.winfo_toplevel()
self.bind('<FocusIn>', lambda evt: self.show_focus(evt=evt, dlg=dlg))
self.bind('<FocusOut>', lambda evt: self.unshow_focus(evt=evt, dlg=dlg))
self.bind('<Button-1>', lambda evt: self.on_press(evt=evt, dlg=dlg))
self.bind(
'<ButtonRelease-1>', lambda evt: self.on_release(evt=evt, dlg=dlg))
self.bind('<Enter>', self.on_hover)
self.bind('<Leave>', self.on_unhover)
self.config(anchor='center', borderwidth=1, relief='raised',
takefocus=1, width=width)
def show_focus(self, evt, dlg):
self.config(borderwidth=2)
def unshow_focus(self, evt, dlg):
self.config(borderwidth=1)
def on_press(self, evt, dlg):
self.config(relief='sunken', bg=dlg.formats["head_bg"])
def on_release(self, evt, dlg):
self.config(relief='raised', bg=dlg.formats["bg"])
def on_hover(self, evt):
self.config(relief='groove')
def on_unhover(self, evt):
self.config(relief='raised')
class LabelDots(tk.Label):
""" Display clickable dots if there's more info, no dots if none. """
def __init__(
self, master, tree, treebard, dialog_class, assertion_id=None,
linked_element=None, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
self.tree = tree
self.treebard = treebard
self.dialog_class = dialog_class
self.assertion_id = assertion_id
self.linked_element = linked_element
self.event_id = None
self.header = []
dlg = self.winfo_toplevel()
self.bind('<Button-1>', self.open_dialog)
self.bind('<Return>', self.open_dialog)
self.bind('<space>', self.open_dialog)
self.bind('<FocusIn>', self.show_focus)
self.bind('<FocusOut>', self.unshow_focus)
self.bind(
'<Button-1>', lambda evt: self.on_press(evt=evt, dlg=dlg), add="+")
self.bind(
'<ButtonRelease-1>', lambda evt: self.on_release(evt=evt, dlg=dlg))
self.bind('<Enter>', self.on_hover)
self.bind('<Leave>', self.on_unhover)
self.config(
anchor='center',
borderwidth=1,
relief='raised',
takefocus=1,
width=5)
def show_focus(self, evt):
self.config(borderwidth=2)
def unshow_focus(self, evt):
self.config(borderwidth=1)
def on_press(self, evt, dlg):
self.config(relief='sunken', bg=dlg.formats["head_bg"])
def on_release(self, evt, dlg):
self.config(relief='raised', bg=dlg.formats["bg"])
def on_hover(self, evt):
self.config(relief='groove')
def on_unhover(self, evt):
self.config(relief='raised')
def open_dialog(self, evt):
dlg = self.dialog_class(
self.tree,
self.treebard,
header=self.header,
inwidg=evt.widget,
event_id=self.event_id,
assertion_id=self.assertion_id,
linked_element=self.linked_element)
""" Compound widgets have other widgets inside them. Some need special methods
that have to be run separately along with the general recolorization scheme.
"""
# * * * * * * * * THE AMAZING TOYKINTER COMBOBOX * * * * * * * *
'''
Replaces ttk.Combobox with an easily configurable widget.
Configuration is done tkinter style, instead of pitting ttk.Style
and Windows themes against each other to see which one wins, as is
the norm when trying to configure ttk widgets. The ttk.Combobox uses
a tk.Listbox as its dropdown window, which is like hiring your ex-wife's
homeless brother to run your business.
Unlike ttk.Combobox...
...dropdown items are selected with mouse or spacebar
...colors including Entry background and dropdown background are easily
configured
...clicking either Entry or Arrow opens then closes dropdown on
alternate clicks
...FocusOut event can be bound to the dropdown items
...arrow traversal thru dropdown items loops to top or bottom when
bottom or top is reached
...dropdown opens with either top or bottom item highlighted depending
on whether Up or Down arrow key is used to open the dropdown
...a dropdown item with text longer than the window displays in
multiple lines.
This combobox was created by the authors of Treebard Genealogy Software, June
2024 after years of rewritings and revisions. The license is public domain, and
please give credit to the Treebard project whence this woundrous widget
laboriously sprang into existence.
Not recommended for large values lists much over 100. It runs fine but after
a large dropdown closes, something else is taking time to process before the
next procedure will be handled. For example a click to open a different
combobox dropdown seems unresponsive but is just waiting for the large
dropdown to finish doing something behind the scenes after it's been closed.
I think I've tested the pertinent methods and it seems to be Tkinter
internals that is causing this slowdown.
'''
font = ("Courier", 12)
class ComboboxDropdown(tk.Toplevel):
''' This is only a widget constructor. All code for manipulating this
dropdown and its values goes in the `Combobox` class.
'''
def __init__(self, master, formats, *args, **kwargs):
tk.Toplevel.__init__(self, master, *args, **kwargs)
self["bd"] = 0
self.master = master
self.formats = formats
self.make_combobox_dropdown()
def make_combobox_dropdown(self):
def fill_canvas(evt):
""" Make the content frame expand. """
self.canvas.itemconfig(frame_id, width=evt.width)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.canvas = CanvasHilited(self, self.formats, bd=0, highlightthickness=0)
self.sbv = Scrollbar(self, formats=self.formats, command=self.canvas.yview)
self.canvas.config(yscrollcommand=self.sbv.set)
self.canvas.grid(column=0, row=0, sticky="news")
self.sbv.grid(column=1, row=0, sticky="ns")
self.content = FrameHilited2(self.canvas, self.formats, bd=0)
frame_id = self.canvas.create_window(0, 0, anchor='nw', window=self.content)
self.canvas.bind("<Configure>", fill_canvas)
self.wm_overrideredirect(1)
self.withdraw()
class Combobox(tk.Frame):
def __init__(
self, master, formats, tree, values=None, dialog_in=None,
*args, **kwargs):
tk.Frame.__init__(self, master, *args, *kwargs)
self.master = master
self.formats = formats
self.tree = tree
self.values = values
self.dialog_in = dialog_in
self.screen_height = self.winfo_screenheight()
self.config(bd=0)
self.dropdown_is_open = False
self.selected = None
self.make_combodrop_and_variable()
self.lenval = 1
if self.values:
self.lenval = len(self.values)
self.current = 0
self.make_widgets()
self.config_values()
self.master.bind('<ButtonRelease-1>', self.close_dropdown, add='+')
# self.focus_set = self.entry.focus_set() # Don't do this.
configall(self, self.formats)
def make_combodrop_and_variable(self):
self.dropdown = ComboboxDropdown(self, self.formats)
self.tree.comboboxes[self] = tk.StringVar()
def make_widgets(self):
self.entry = tk.Entry(self, textvariable=self.tree.comboboxes[self])
self.arrow = LabelHilited2(self, self.formats, text='\u25BC', width=2)
self.columnconfigure(0, weight=1)
self.entry.grid(column=0, row=0, sticky="ew")
self.arrow.grid(column=1, row=0, sticky="e")
self.tree.bind('<Escape>', self.hide_dropdown, add='+')
self.dropdown.bind('<FocusIn>', self.focus_dropdown)
self.dropdown.bind('<Unmap>', lambda evt, formats=self.formats:
self.unhighlight_all_drop_items(evt, formats))
self.entry.bind('<KeyPress>', self.open_or_close_dropdown)
self.entry.bind('<Tab>', self.open_or_close_dropdown)
for widg in (self.entry, self.arrow):
widg.bind('<Button-1>', self.open_or_close_dropdown, add='+')
self.arrow.bind('<Button-1>', self.focus_entry_on_arrow_click, add='+')
def format_comboboxes(self, evt=None, new_formats=None):
""" Format all the widgets that are part of the composite Combobox
widget when `configall` runs, i.e. when the user changes the
color scheme.
"""
if new_formats:
self.formats = formats = new_formats
else:
formats = self.formats
self.entry["bg"] = formats["highlight_bg"]
self.arrow["bg"] = formats["head_bg"]
self.arrow["fg"] = formats["fg"]
self.dropdown["bg"] = formats["highlight_bg"]
for button in self.dropdown.content.winfo_children():
button["bg"] = formats["highlight_bg"]
button["fg"] = formats["fg"]
button["font"] = formats["font"]
button["activebackground"] = formats["fg"]
button["activeforeground"] = formats["bg"]
def open_or_close_dropdown(self, evt=None):
""" Set `dialog_in` to a containing Toplevel widget only if that
Toplevel is modal. Without `dialog_in`, the Combobox dropdown is
disabled by a containing dialog's use of `grab_set`.
"""
if self.values is None:
return
if evt is None:
# dropdown item clicked--no evt because of Button command option
self.dropdown.withdraw()
if self.dialog_in:
self.dialog_in.grab_set()
self.dropdown_is_open = False
return
if len(self.dropdown.content.winfo_children()) == 0:
return
evt_type = evt.type
evt_sym = evt.keysym
if evt_sym == 'Tab':
self.dropdown.withdraw()
if self.dialog_in:
self.dialog_in.grab_set()
self.dropdown_is_open = False
return
elif evt_sym == 'Escape':
self.hide_dropdown()
if self.dialog_in:
self.dialog_in.grab_set()
return
first = None
last = None
dropdown_buttons = self.dropdown.content.winfo_children()
if len(dropdown_buttons) != 0:
first = dropdown_buttons[0]
last = dropdown_buttons[-1]
if evt_type == '4':
if self.dropdown_is_open:
self.arrow["bg"] = self.formats["highlight_bg"]
self.dropdown.withdraw()
if self.dialog_in:
self.dialog_in.grab_set()
self.dropdown_is_open = False
return
else:
self.arrow["bg"] = self.formats["head_bg"]
elif evt_type == '2':
if evt_sym not in ('Up', 'Down'):
return
elif first is None or last is None:
pass
elif evt_sym == 'Down':
first.config(bg=self.formats["bg"])
first.focus_set()
self.combobox_selected()
self.dropdown.canvas.yview_moveto(0.0)
elif evt_sym == 'Up':
last.config(bg=self.formats["bg"])
last.focus_set()
self.combobox_selected()
self.dropdown.canvas.yview_moveto(1.0)
self.dropdown.deiconify()
self.fly_up_or_drop_down(evt)
if self.dialog_in:
self.dialog_in.grab_release()
self.dropdown_is_open = True
def fly_up_or_drop_down(self, evt):
# def fly_up_or_drop_down(self, evt, width):
""" Set the dropdown position to either drop down or fly up depending on
the position of the combobox in relation to the top and bottom edges
of the screen. Before it can be decided whether the dropdown must fly
up instead of dropping down, its height has to be limited if it has
too many values to fit into the screen height or a set portion thereof.
"""
if self.max_dropdown_height < self.fit_height:
self.dropdown.sbv.grid()
height = self.max_dropdown_height
else:
height = self.fit_height
self.dropdown.sbv.grid_remove()
combo_height = self.winfo_reqheight()
over_in_screen = self.entry.winfo_rootx()
down_in_screen = self.entry.winfo_rooty()
down = down_in_screen + combo_height
combo_width = self.winfo_reqwidth()
# Get vertical position of mouse click:
click_from_screen_top = evt.y_root
click_from_combo_top = evt.y
fly_up = False
combo_to_screen_top = click_from_screen_top - click_from_combo_top
combo_to_screen_bottom = (
self.screen_height - (combo_to_screen_top + combo_height))
if combo_to_screen_bottom < height:
fly_up = True
fly_up_clearance = combo_to_screen_top - height
if fly_up is False:
drop_y = down
else:
drop_y = fly_up_clearance
self.dropdown.geometry(f"{combo_width}x{height}+{over_in_screen}+{drop_y}")
def config_values(self, new_values=None):
def scroll_start(evt):
self.dropdown.canvas.scan_mark(evt.x, evt.y)
self.fixed_x = evt.x
def on_mousewheel(event):
self.dropdown.canvas.yview_scroll(-1 * (event.delta // 120), "units")
if self.dropdown_is_open:
self.dropdown_is_open = False
return
if new_values:
self.values = new_values
if self.values is None:
return
for button in self.dropdown.content.winfo_children():
button.destroy()
self.update_idletasks()
wraplength = self.winfo_reqwidth()
scridth = self.dropdown.sbv.winfo_reqwidth()
self.max_dropdown_height = int(self.screen_height * 0.33)
self.dropdown.content.columnconfigure(0, weight=1)
for text in self.values:
item = ComboboxDropdownButton(
self.dropdown.content, wraplength, text=text, anchor='w')
item.grid(sticky="ew")
for evt_stg in ('<Button-1>', '<space>'):
item.bind(evt_stg, self.get_clicked, add='+')
item.bind('<Enter>', self.highlight_button, add='+')
item.bind('<Leave>', self.unhighlight_button, add='+')
item.bind('<Tab>', self.tab_out_of_dropdown_fwd)
item.bind('<Shift-Tab>', self.tab_out_of_dropdown_back)
item.bind('<KeyPress>', self.traverse_on_arrow)
item.bind('<FocusOut>', self.unhighlight_button, add='+')
item.bind("<FocusOut>", self.bind_to_dropdown_items, add='+')
item.bind("<FocusIn>", self.combobox_selected, add="+")
configall(self.dropdown, self.formats)
self.update_idletasks()
self.fit_height = height = self.dropdown.content.winfo_reqheight()
if self.fit_height >= self.max_dropdown_height:
self.dropdown.canvas.config(
height=self.max_dropdown_height, width=wraplength - scridth,
scrollregion=(0, 0, 0, self.fit_height))
else:
self.dropdown.canvas.config(
width=wraplength, height=self.fit_height,
scrollregion=(0, 0, 0, self.fit_height))
def combobox_selected(self, evt=None):
''' Do something on focus into a dropdown item? Better to trigger on
events that make a selection in the combobox (not just highlight
item): `Button-1` or `space`. Make it work with `trace_add`. This
used to work. This method had to be overridden to use it.
'''
if len(self.tree.comboboxes[self].get()) == 0:
return
# # # ("self.tree.comboboxes[self].get()", self.tree.comboboxes[self].get())
pass
def focus_entry_on_arrow_click(self, evt):
self.focus_set()
self.entry.select_range(0, 'end')
def bind_to_dropdown_items(self, evt):
# # # print("evt.widget['text'] on FocusOut:", evt.widget['text'])
pass
def highlight_button(self, evt):
for widg in self.dropdown.content.winfo_children():
widg.config(bg=self.formats["highlight_bg"])
widget = evt.widget
widget.config(bg=self.formats["bg"])
self.selected = widget
widget.focus_set()
self.combobox_selected()
def unhighlight_button(self, evt):
x, y = self.winfo_pointerxy()
hovered = self.winfo_containing(x,y)
if hovered in self.dropdown.content.winfo_children():
evt.widget.config(bg=self.formats["highlight_bg"])
def unhighlight_all_drop_items(self, evt, formats):
for child in self.dropdown.content.winfo_children():
child.config(bg=formats["highlight_bg"])
def focus_dropdown(self, evt):
for widg in self.dropdown.content.winfo_children():
widg.config(takefocus=1)
def handle_tab_out_of_dropdown(self, go):
for idx,widg in enumerate(self.dropdown.content.winfo_children()):
widg.config(takefocus=0)
if widg == self.selected:
text = self.values[idx]
break
self.entry.delete(0, 'end')
self.entry.insert(0, text)
self.dropdown.withdraw()
self.dropdown_is_open = False
self.entry.focus_set()
self.combobox_selected()
if go == 'fwd':
goto = self.entry.tk_focusNext()
elif go == 'back':
goto = self.entry.tk_focusPrev()
goto.focus_set()
def tab_out_of_dropdown_fwd(self, evt):
self.selected = evt.widget
self.handle_tab_out_of_dropdown('fwd')
def tab_out_of_dropdown_back(self, evt):
self.selected = evt.widget
self.handle_tab_out_of_dropdown('back')
def get_clicked(self, evt):
self.selected = evt.widget
children = self.dropdown.content.winfo_children()
for idx,widg in enumerate(children):
if widg == self.selected:
text = self.values[idx]
break
self.current = children.index(self.selected)
self.entry.delete(0, 'end')
self.entry.insert(0, text)
self.entry.select_range(0, 'end')
self.open_or_close_dropdown()
def get_typed(self):
self.typed = self.tree.combovar.get()
def highlight_on_traverse(self, evt, next_item=None, prev_item=None):
evt_type = evt.type # '2' is key press, '4' is button press
evt_sym = evt.keysym
for widg in self.dropdown.content.winfo_children():
widg.config(bg=self.formats["highlight_bg"])
if evt_type == '4':
self.selected = evt.widget
elif evt_type == '2' and evt_sym == 'Down':
self.selected = next_item
elif evt_type == '2' and evt_sym == 'Up':
self.selected = prev_item
self.selected.config(bg=self.formats["bg"])
self.widg_height = int(self.fit_height / self.lenval)
widg_screenpos = self.selected.winfo_rooty()
widg_listpos = self.selected.winfo_y()
win_top = self.dropdown.winfo_rooty()
win_bottom = win_top + self.fit_height
list_ratio = widg_listpos / self.fit_height
widg_ratio = self.widg_height / self.fit_height
up_ratio = list_ratio - widg_ratio
if widg_screenpos > win_bottom - 0.75 * self.widg_height:
self.dropdown.canvas.yview_moveto(float(list_ratio))
elif widg_screenpos < win_top:
self.dropdown.canvas.yview_moveto(float(up_ratio))
self.selected.focus_set()
self.combobox_selected()
def traverse_on_arrow(self, evt):
if evt.keysym not in ('Up', 'Down'):
return
widg = evt.widget
sym = evt.keysym
self.widg_height = int(self.fit_height / self.lenval)
next_item = widg.tk_focusNext()
prev_item = widg.tk_focusPrev()
if sym == 'Down':
if next_item in self.dropdown.content.winfo_children():
self.highlight_on_traverse(evt, next_item=next_item)
else:
next_item = self.dropdown.content.winfo_children()[0]
next_item.focus_set()
self.combobox_selected()
next_item.config(bg=self.formats["bg"])
self.dropdown.canvas.yview_moveto(0.0)
elif sym == 'Up':
if prev_item in self.dropdown.content.winfo_children():
self.highlight_on_traverse(evt, prev_item=prev_item)
else:
prev_item = self.dropdown.content.winfo_children()[-1]
prev_item.focus_set()
self.combobox_selected()
prev_item.config(bg=self.formats["bg"])
self.dropdown.canvas.yview_moveto(1.0)
def hide_dropdown(self, evt=None):
self.dropdown.withdraw()
self.dropdown_is_open = False
def close_dropdown(self, evt):
""" This callback is bound to all widgets on ButtonRelease so that a
click anywhere will close the dropdown. But if the combobox has
been destroyed, then referencing the nonexistent dropdown would
cause an error.
"""
try:
self.dropdown.withdraw()
except:
# print("The dropdown no longer exists.")
pass
finally:
self.dropdown_is_open = False
class ComboboxDropdownButton(tk.Button):
def __init__(self, master, wraplength, *args, **kwargs):
tk.Button.__init__(self, master, *args, **kwargs)
self.config(
justify="left", anchor='w', bd=0, relief="flat", overrelief="flat",
wraplength=wraplength)
def highlight(self, evt):
self.config(
bg=formats['highlight_bg'],
fg=formats['fg'],
activebackground=formats['fg'],
activeforeground=formats['bg'])
class Scrollbar(tk.Canvas):
""" A scrollbar is gridded as a sibling of what it's scrolling. Set the
command attribute during construction. It's a Python keyword argument
but not a Tkinter option so vscroll.config(command=self.yview) won't
work. This scrollbar works well and can be made any size or color. It's
lacking the little arrows at the ends of the trough.
"""
def __init__(
self, master, formats, width=16, orient='vertical',
hideable=False, pack_it=False, **kwargs):
self.command = kwargs.pop('command', None)
tk.Canvas.__init__(self, master, **kwargs)
self.width = width
self.orient = orient
self.hideable = hideable
self.pack_it = pack_it
self.x0 = 0
self.y0 = 0
self.x1 = 0
self.y1 = 0
self.new_start_y = 0
self.new_start_x = 0
self.first_y = 0
self.first_x = 0
if orient == 'vertical':
self.config(width=width)
elif orient == 'horizontal':
self.config(height=width)
self.config(bd=0, highlightthickness=0)
self.make_widgets(formats)
def make_widgets(self, formats):
self.thumb = self.create_rectangle(
0, 0, 1, 1,
fill=formats["bg"], # slidercolor
width=1, # this is border width
outline=formats["highlight_bg"], # bordercolor
tags=('slider',))
self.bind('<ButtonPress-1>', self.move_on_click)
self.bind('<ButtonPress-1>', self.start_scroll, add='+')
self.bind('<B1-Motion>', self.move_on_scroll)
self.bind('<ButtonRelease-1>', self.end_scroll)
def format_scrollbars(self, formats, new_formats=None):
if new_formats:
formats = new_formats
else:
formats = formats
self.itemconfig(
self.thumb, fill=formats["bg"], outline=formats["highlight_bg"])
def set(self, lo, hi):
""" For resizing & repositioning the slider. """
lo = float(lo)
hi = float(hi)
if self.hideable and not self.pack_it:
if lo <= 0.0 and hi >= 1.0:
self.grid_remove()
return
else:
self.grid()
elif self.hideable and self.pack_it:
pass
self.height = self.winfo_height()
width = self.winfo_width()
if self.orient == 'vertical':
x0 = 0
y0 = max(int(self.height * lo), 0)
x1 = width - 1
y1 = min(int(self.height * hi), self.height)
elif self.orient == 'horizontal':
x0 = max(int(width * lo), 0)
y0 = 0
x1 = min(int(width * hi), width)
y1 = self.height -1
self.coords('slider', x0, y0, x1, y1)
self.x0 = x0
self.y0 = y0
self.x1 = x1
self.y1 = y1
def move_on_click(self, event):
if self.orient == 'vertical':
y = event.y / self.winfo_height()
if event.y < self.y0 or event.y > self.y1:
self.command('moveto', y)
else:
self.first_y = event.y
elif self.orient == 'horizontal':
x = event.x / self.winfo_width()
if event.x < self.x0 or event.x > self.x1:
self.command('moveto', x)
else:
self.first_x = event.x
def start_scroll(self, event):
if self.orient == 'vertical':
self.last_y = event.y
self.y_move_on_click = int(event.y - self.coords('slider')[1])
elif self.orient == 'horizontal':
self.last_x = event.x
self.x_move_on_click = int(event.x - self.coords('slider')[0])
def end_scroll(self, event):
if self.orient == 'vertical':
self.new_start_y = event.y
elif self.orient == 'horizontal':
self.new_start_x = event.x
def move_on_scroll(self, event):
jerkiness = 3
if self.orient == 'vertical':
if abs(event.y - self.last_y) < jerkiness:
return
delta = 1 if event.y > self.last_y else -1
self.last_y = event.y
self.command('scroll', delta, 'units')
mouse_pos = event.y - self.first_y
if self.new_start_y != 0:
mouse_pos = event.y - self.y_move_on_click
self.command('moveto', mouse_pos/self.winfo_height())
elif self.orient == 'horizontal':
if abs(event.x - self.last_x) < jerkiness:
return
delta = 1 if event.x > self.last_x else -1
self.last_x = event.x
self.command('scroll', delta, 'units')
mouse_pos = event.x - self.first_x
if self.new_start_x != 0:
mouse_pos = event.x - self.x_move_on_click
self.command('moveto', mouse_pos/self.winfo_width())
class ScrolledText(tk.Frame):
def __init__(self, master, formats, treebard, mode=None, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.text = tk.Text(self)
self.text.grid(column=0, row=0)
self.ysb = Scrollbar(
self, formats,
width=16,
orient='vertical',
hideable=True,
command=self.text.yview)
self.text.configure(yscrollcommand=self.ysb.set, wrap="word")
self.ysb.grid(column=1, row=0, sticky='ns')
self.text.bind("<Tab>", self.focus_next_window)
self.text.bind("<Shift-Tab>", self.focus_prev_window)
self.text.config(wrap='word')
# Make the Text widget use the TAB key for traversal like other widgets.
# `return('break')` prevents the built-in binding to TAB.
def focus_next_window(self, evt):
evt.widget.tk_focusNext().focus()
return('break')
def focus_prev_window(self, evt):
evt.widget.tk_focusPrev().focus()
return('break')
class TabBook(tk.Frame):
related_tabbooks = {}
def resize_scrolled_dialog_with_tabbook(dialog, canvas, window):
""" `related_tabbooks` references one or more TabBook instances that
are related by being in the same dialog, to determine a
value for the highest and widest tab content so the dialog
can be resized to fit those dimensions
"""
def config_canvas(widest, tallest):
""" Run this after a delay to prevent app freezing
when larger font sizes are used.
"""
canvas.config(scrollregion=(0, 0, widest, tallest))
def resize_window_and_scrollbar():
if type(dialog).__name__ == "FamilyTree":
bar_height = 96 # menubar + ribbon + statusbar
scridth = 30
else:
bar_height = 27 # statusbar
scridth = 26
page_x = scridth + widest
page_y = scridth + bar_height + tallest
dialog.geometry(f"{page_x}x{page_y}")
dialog.after(500, lambda: config_canvas(
widest=widest, tallest=tallest))
related_tabbooks = TabBook.related_tabbooks[dialog]
tabbook_references = TabBook.related_tabbooks[dialog]["tabbooks"]
for tabbook in tabbook_references:
for idx,tab_key in enumerate(tabbook.store):
if idx == 0:
top_tab = tab_key
tabbook.active = tabbook.tabdict[tab_key][1]
tabbook.make_active()
dialog.update_idletasks()
ht = TabBook.related_tabbooks[dialog]["tallest"][2]
wd = TabBook.related_tabbooks[dialog]["widest"][2]
new_ht = window.winfo_reqheight()
if new_ht > ht:
TabBook.related_tabbooks[dialog]["tallest"] = [
tabbook, tab_key, new_ht]
new_wd = window.winfo_reqwidth()
if new_wd > wd:
TabBook.related_tabbooks[dialog]["widest"] = [
tabbook, tab_key, new_wd]
for tabbook in tabbook_references:
top_tab = tabbook.tabs[0][0]
tabbook.active = tabbook.tabdict[top_tab][1]
tabbook.make_active()
tallest = TabBook.related_tabbooks[dialog]["tallest"][2]
widest = TabBook.related_tabbooks[dialog]["widest"][2]
resize_window_and_scrollbar()
def __init__(
self, master, formats, dialog=None, side='nw', bd=0, tabwidth=13,
tabs=[], minx=0.90, miny=0.85, case='upper',
takefocus=1, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
""" To add widgets grid them with
instance.store[page] as the master. For example:
inst.store['place'] where 'place' is a string from
the original tabs parameter (list of tuples containing
tab title and accelerator e.g.:
`[('images', 'I'), ('attributes', 'A')]`). The default
value of `selected` should not be an empty string since
a string here is not optional, it has to be the title
of one of the tabs, the one that is to be open by default.
`minx` and `miny` are minimum sizes as proportions of the
screen size. If the accelerators don't work, check `dialog`.
Accelerator symbols can be shared by two TabBooks that aren't
both visible, but any TabBooks that are visible at the same
time have to use unique symbols, e.g. "GRAPHICS" and "GENERAL"
tabs are on different TabBooks but can't both use ALT+G since
their tabs are visible at the same time.
"""
self.master = master
self.formats = formats
self.dialog = dialog
self.side = side
self.bd = bd
self.tabwidth = tabwidth
self.tabs = tabs
self.minx = self.master.winfo_screenwidth() * minx
self.miny = self.master.winfo_screenheight() * miny
self.case = case
self.takefocus = takefocus
self.tabdict = {}
for tab in tabs:
self.tabdict[tab[0]] = [tab[1]]
self.selected = tabs[0][0]
self.store = {}
self.active = None
self.open_tab_alt(dialog)
if self.winfo_name() == "rightpanel":
pass
elif TabBook.related_tabbooks.get(dialog) is None:
TabBook.related_tabbooks[dialog] = {
"tallest": [None, "", 0], "widest": [None, "", 0],
"tabbooks": [self]}
else:
TabBook.related_tabbooks[dialog]["tabbooks"].append(self)
self.make_widgets()
def make_widgets(self):
self.tab_base = tk.Frame(self)
self.border_base = tk.Frame(self)
self.notebook = tk.Frame(self.border_base)
self.tab_frame = tk.Frame(self.tab_base)
self.tabless = tk.Frame(self.tab_base)
self.spacer = tk.Frame(self.tabless)
self.top_border = tk.Frame(self.tabless, height=1)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
self.border_base.columnconfigure(0, weight=1)
self.notebook.columnconfigure(0, weight=1, minsize=self.minx)
self.notebook.rowconfigure(0, weight=1, minsize=self.miny)
self.grid_tabs()
c = 0
for tab in self.tabdict:
lab = tk.Label(
self.tab_frame,
width=int(self.tabwidth),
takefocus=self.takefocus)
if c == 0:
lab.chosen = True
if self.case == 'title':
lab.config(text=tab.title())
elif self.case == 'lower':
lab.config(text=tab.lower())
elif self.case == 'upper':
lab.config(text=tab.upper())
self.tabdict[tab].append(lab)
if self.side in ('ne', 'nw'):
lab.grid(column=c, row=0, padx=1, pady=(1, 0))
elif self.side in ('se', 'sw'):
lab.grid(column=c, row=0, padx=1, pady=(0, 1))
lab.bind('<Button-1>', self.make_active)
create_tooltip(lab, f"Alt + {self.tabdict[tab][0]}", self.formats)
page = tk.Frame(self.notebook)
page.grid(column=0, row=0, sticky='news')
page.grid_remove()
self.tabdict[tab].append(page)
self.store[tab] = page
c += 1
selected_page = self.tabdict[self.selected][2] # page
selected_page.grid()
self.active = self.tabdict[self.selected][1] # tab
self.make_active()
def grid_tabs(self):
""" Put the tabs in the right place. ttk.Notebook can't do this. """
if self.side in ('nw', 'ne'):
pady = (0, 1)
tab_row = 0
body_row = 1
spacer_row = 0
border_row = 1
elif self.side in ('sw', 'se'):
pady=(1, 0)
tab_row = 1
body_row = 0
spacer_row = 1
border_row = 0
if self.side in ('nw', 'sw'):
tab_col = 0
tabless_col = 1
elif self.side in ('ne', 'se'):
tab_col = 1
tabless_col = 0
# self.notebook switches pady
self.notebook.grid(column=0, row=0, padx=1, pady=pady, sticky='news')
# self.tab_base and borderbase switch rows
self.tab_base.grid(column=0, row=tab_row, sticky='news')
self.tab_base.columnconfigure(tabless_col, weight=1)
self.tab_base.rowconfigure(0, weight=1)
self.border_base.grid(column=0, row=body_row, sticky='news')
# self.tab_frame and self.tabless switch cols
self.tab_frame.grid(column=tab_col, row=0, sticky='ew')
self.tabless.grid(column=tabless_col, row=0, sticky='news')
self.tabless.columnconfigure(0, weight=1)
self.tabless.rowconfigure(spacer_row, weight=1)
# self.spacer and self.top_border switch rows
self.spacer.grid(column=0, row=tab_row, sticky='ns')
self.top_border.grid(column=0, row=border_row, sticky='ew')
def make_active(self, evt=None):
""" Open the selected tab & reconfigure it to look open. """
# Run on mouse click.
if evt:
self.active = evt.widget
self.active.focus_set()
# Run on alt key accelerator.
for k,v in self.tabdict.items():
if evt.keysym in (v[0], v[0].lower()):
self.active = v[1]
self.active.focus_set()
self.grid_tab_content()
for tab in self.tabdict.values():
if tab[1] == self.active:
tab[1].chosen = True
else:
tab[1].chosen = False
self.format_tabs()
def format_tabs(self, new_formats=None):
""" Unhighlight all the tabs, highlight the active tab,
grid the right content, colorize the border.
"""
if new_formats:
self.formats = formats = new_formats
else:
formats = self.formats
for tab in self.tabdict.values():
tab[1]["bg"] = formats["highlight_bg"]
tab[1]["font"] = formats["tab_font"]
self.active["bg"] = formats["bg"]
self.grid_tab_content()
for widg in (
self.tab_frame,
self.border_base,
self.top_border):
widg["bg"] = formats["head_bg"]
def grid_tab_content(self):
""" Remove all the pages and regrid the right one. """
for val in self.tabdict.values():
if self.active == val[1]:
for widg in self.tabdict.values():
widg[2].grid_remove()
val[2].grid()
def open_tab_alt(self, dialog):
""" Bindings for notebook tab accelerators. """
for val in self.tabdict.values():
key_combo_upper = f"<Alt-Key-{val[0]}>"
dialog.bind(key_combo_upper, self.make_active)
key_combo_lower = f"<Alt-Key-{val[0].lower()}>"
dialog.bind(key_combo_lower, self.make_active)
def add_star(self, add_to):
for stg in add_to:
for k,v in self.tabdict.items():
if stg == k:
labeltab = v[1]
labeltab.config(text=f"{stg.upper()}*")
def get_default_formats(query):
colors = []
for color in (
'default_bg', 'default_highlight_bg', 'default_head_bg',
'default_fg'):
stg = query.select(color)
colors.append(stg)
return colors + [f"{FONT}", 12]
def get_tree_formats(color_scheme_id, tree_id):
conx = sqlite3.connect(tbard_path)
curx = conx.cursor()
curx.execute(
''' SELECT color1, color2, color3, color4
FROM color_scheme
WHERE color_scheme_id = ?
''',
(color_scheme_id,))
colors = [i for i in curx.fetchone()]
font_formats = ["verdana", 12]
curx.execute(
''' SELECT font, font_size
FROM family_tree
WHERE family_tree_id = ?
''',
(tree_id,))
results = curx.fetchone()
if results:
font_formats = list(results)
user_formats = colors + font_formats
curx.close()
conx.close()
return user_formats
def get_color_scheme_id(tree_id):
conx = sqlite3.connect(tbard_path)
curx = conx.cursor()
color_scheme_id = 1
curx.execute(
''' SELECT color_scheme_id
FROM family_tree
WHERE family_tree_id = ?
''',
(tree_id,))
result = curx.fetchone()
if result:
color_scheme_id = result[0]
curx.close()
conx.close()
return color_scheme_id
FONT = "verdana"
FORMAT_KEYS = (
'bg', 'highlight_bg', 'head_bg', 'fg', 'font', 'heading1', 'heading2',
'heading3', 'heading4', 'status', 'boilerplate', 'show_font',
'unshow_font', 'tab_font')
def make_formats_dict(tree_id, color_scheme_id=None):
""" To add a style, add a string to the end of FORMAT_KEYS
and an item to the end of FORMAT_VALUES.
"""
# Use the new color scheme:
if tree_id and color_scheme_id:
prefs_to_use = list(get_tree_formats(color_scheme_id, tree_id))
# Use the default color scheme:
elif tree_id is None or color_scheme_id is None:
query = Query()
prefs_to_use = list(get_default_formats(query))
else:
print("line", look(seeline()).lineno, "case not handled in widgets.py")
color1, color2, color3, color4, font_family, font_size = prefs_to_use
FORMAT_VALUES = (
color1,
color2,
color3,
color4,
(font_family, font_size),
(font_family, font_size * 2, 'bold'),
(font_family, int(font_size * 1.5), 'bold'),
(font_family, int(font_size * 1.125), 'bold'),
(font_family, int(font_size * 0.75), 'bold'),
(font_family, int(font_size * 0.83)),
(font_family, int(font_size * 0.66)),
(font_family, font_size, 'italic'),
(font_family, int(font_size * .75), 'italic'),
(font_family, int(font_size * 0.75)))
formats = dict(zip(FORMAT_KEYS, FORMAT_VALUES))
return formats
""" WIDGETS THAT CAN BE INHERITED TO MAKE DIALOGS; MESSAGE & ERROR DIALOGS
The dialogs have embedded widgets but `configall` catches them, they don't
respond to events, so these are simple widgets following the usual pattern
for colorization, except that passing `formats` to these widgets would do no
good in this case. If the user changes the color scheme and then opens a
new dialog, the new dialog has to go get the new color scheme. The dialog
has to use `tree_id` to get the right color scheme for the current
tree. All Treebard dialogs have to have a master, and that master
should be the toplevel that spawned the new toplevel, so that if an ancestor
dialog closes, its descendant dialogs will close automatically.
"""
class ScrolledDialog(tk.Toplevel):
""" Dialogs which inherit from this class add themselves to `main_canvases`
so they will scroll with the mousewheel. The root app (`treebard`) and
any other scrolled dialog that doesn't inherit from this class has to
be added, e.g....
`ScrolledDialog.main_canvases.append(treebard.canvas)`
...and scrolled areas that are not dialogs have to be added, e.g....
`ScrolledDialog.nested_canvases.append(canvas)`
...and this has to be called once for content created on load...
`ScrolledDialog.bind_canvases_to_mousewheel()`
...and this has to be called for each new scrolled dialog that did not
exist when the app loaded...
`ScrolledDialog.bind_canvas_to_mousewheel(self.canvas)`
"""
main_canvases = []
nested_canvases = []
def bind_canvases_to_mousewheel():
for canv in ScrolledDialog.main_canvases + ScrolledDialog.nested_canvases:
ScrolledDialog.bind_canvas_to_mousewheel(canv)
def bind_canvas_to_mousewheel(canv):
canv.bind("<Enter>", ScrolledDialog.bind_it)
canv.bind("<Leave>", ScrolledDialog.unbind_it)
canv.bind(
"<Destroy>",
lambda evt, canv=canv:
ScrolledDialog.remove_canvas_from_list(evt, canv))
def on_mousewheel(evt, canvas):
canvas.yview_scroll(-1 * int(evt.delta / 120), "units")
def bind_it(event=None, canvas=None):
""" There are two events...
...the Enter/Leave `event`...
...and the Mousewheel `evt`.
"""
if event:
canvas = event.widget
canvas.bind_all(
"<MouseWheel>",
lambda evt, canvas=canvas: ScrolledDialog.on_mousewheel(evt, canvas))
def unbind_it(event):
canvas = event.widget
canvas.unbind_all("<MouseWheel>")
if canvas in (ScrolledDialog.nested_canvases):
top = canvas.winfo_toplevel()
ScrolledDialog.bind_it(canvas=top.canvas)
def remove_canvas_from_list(event, canvas):
if canvas in ScrolledDialog.main_canvases:
ScrolledDialog.main_canvases.remove(canvas)
elif canvas in ScrolledDialog.nested_canvases:
ScrolledDialog.nested_canvases.remove(canvas)
def __init__(
self, master, scrollbar_width=16, has_statusbar=True,
highlight_canvas=False, *args, **kwargs):
tk.Toplevel.__init__(self, master, *args, **kwargs)
self.treebard = master
self.scridth = scrollbar_width
self.has_statusbar = has_statusbar
self.maxsize(
width=int(self.winfo_screenwidth() * 0.9),
height=int(self.winfo_screenheight() * 0.85))
self.dialog = self
self.formats = formats = make_formats_dict(None)
cur = None
conn = None
self.ok_was_pressed = False
self.canvas = tk.Canvas(self, bd=0, highlightthickness=0)
self.window = tk.Frame(self.canvas)
self.canvas.create_window(0, 0, anchor='nw', window=self.window)
if self.has_statusbar:
self.statusbar = StatusbarTooltips(self, self.formats)
self.make_scrollbars()
# children of self
self.columnconfigure(1, weight=1)
self.rowconfigure(1, weight=1)
self.scridth_n.grid(column=0, row=0, sticky='ew', columnspan=2)
self.scridth_w.grid(column=0, row=1, sticky='ns')
self.canvas.grid(column=1, row=1, sticky="news")
self.vsb.grid(column=2, row=1, sticky='ns')
self.hsb.grid(column=1, row=2, sticky='ew')
ScrolledDialog.main_canvases.append(self.canvas)
if self.has_statusbar:
self.statusbar.grid(column=1, row=3, sticky='ew')
def make_scrollbars(self):
self.scridth_n = tk.Frame(self, height=self.scridth)
self.scridth_w = tk.Frame(self, width=self.scridth)
self.vsb = Scrollbar(
self,
self.formats,
hideable=True,
command=self.canvas.yview,
width=self.scridth)
self.hsb = Scrollbar(
self,
self.formats,
hideable=True,
width=self.scridth,
command=self.canvas.xview,
orient='horizontal')
self.canvas.config(
xscrollcommand=self.hsb.set,
yscrollcommand=self.vsb.set)
def resize_scrolled_content(self, toplevel, canvas, add_x=0, add_y=0):
""" Run this in the instance after all widgets are made.
Try setting `add_x=0` and `add_y=18` if `has_statusbar` is False
or `add_y=36 if `has_statusbar` is True, or `add_x=16` and
`add_y=24`. Or rewrite this part and make it more scientifical,
but I've been fighting this part for years. At least this
way you can adjust each dialog according to its needs.
"""
toplevel.update_idletasks()
x = self.window.winfo_reqwidth()
y = self.window.winfo_reqheight()
page_x = x + self.scridth + add_x
page_y = y + self.scridth + add_y
toplevel.geometry('{}x{}'.format(page_x, page_y))
canvas.config(scrollregion=canvas.bbox('all'))
def open_message(
master, message, dialog_title, buttlab, inwidg=None, formats=None):
def close():
""" Override this if more needs to be done on close. """
msg.destroy()
msg = tk.Toplevel(master)
msg.title(dialog_title)
lab = tk.Label(
msg, text=message, justify='left', wraplength=600, bd=1, relief="raised")
button = tk.Button(msg, text=buttlab, command=close, width=6)
lab.grid(column=0, row=0, sticky='news', padx=12, pady=12, ipadx=6, ipady=3)
button.grid(column=0, row=1, padx=6, pady=(0,12))
if formats is None:
formats = make_formats_dict(None, color_scheme_id=1)
configall(msg, formats)
button.focus_set()
return msg, lab, button
def make_rc_menus(rcm_widgets, rc_menu, rcm_msg):
""" To include a widget in the right-click context help, list the widget
in rcm_widgets in the instance and store each widget's message and
title in messages_context_help.py. Example of usage from notes.py:
after making all widgets, do this...
`rcm_widgets = (self.subtopic_input.ent, self.note_input.text)`
`make_rc_menus(
rcm_widgets,
self.rc_menu,
note_dlg_help_msg)`
...and in `__init__` before making widgets, do this...
`self.rc_menu = RightClickMenu(self.treebard)`
... and import:
`from widgets import RightClickMenu, make_rc_menus`
Use this if the widgets were made in a loop and should all have the
same right-click message:
In the loop where the widgets such as `editx` are made:
`self.rc_menu.loop_made[editx] = role_edit_help_msg`
At the bottom of messages_context_help.py, store the message and
dialog title, e.g.:
`role_edit_help_msg = (
'Clicking the Edit button will open a row of edit inputs... ',
'Roles Dialog: Edit Existing Role Button')`
Import the message & title text to the module where it will be used:
`from messages_context_help import role_edit_help_msg`
The normal procedure described above this loopy one still needs to be
done even if there are no normal widgets in the module (widgets made
one-at-a-time instead of in a loop). In this case `rcm_widgets` and
`note_dlg_help_msg` can both = `()` but they have to exist.
"""
rc_menu.help_per_context = dict(zip(rcm_widgets, rcm_msg))
for widg in rcm_widgets:
widg.bind("<Button-3>", rc_menu.attach_rt_clk_menu)
for k,v in rc_menu.loop_made.items():
k.bind("<Button-3>", rc_menu.attach_rt_clk_menu)
rc_menu.help_per_context[k] = v
class RightClickMenu(tk.Menu):
""" This is how you config() the menu items post-constructor, or do it in
the instance, see below:
self.entryconfigure('Copy', state='disabled')
"""
def __init__(self, master, treebard, *args, **kwargs):
tk.Menu.__init__(self, master, *args, **kwargs)
self.master = master
self.treebard = treebard
self.message = ''
self.help_title = ''
self.widg = None
self.config(tearoff=0)
self.help_per_context = {}
self.loop_made = {}
self.formats = make_formats_dict(None, color_scheme_id=1)
self.make_widgets()
def make_widgets(self):
self.add_command(label='Copy', command=self.copy)
self.add_command(label='Paste', command=self.paste)
self.add_separator()
self.add_command(label='Context Help', command=self.context_help)
def copy(self):
print('Copied')
def paste(self):
print('Pasted')
def context_help(self):
def cancel():
msg.destroy()
msg = ScrolledDialog(self.master, has_statusbar=False)
msg.title(self.help_title)
content = tk.Frame(msg.window)
headlab = tk.Label(
content, text=self.message, bd=1, relief="raised", justify="left",
wraplength=480)
cancel_button = tk.Button(content, text="DONE", command=cancel)
content.grid(column=0, row=0, sticky="news")
headlab.grid(column=0, row=0, ipadx=6, ipady=6)
cancel_button.grid(column=0, row=2, sticky="e", pady=12)
cancel_button.focus_set()
configall(msg, self.formats)
msg.resize_scrolled_content(msg, msg.canvas, add_y=18, add_x=24)
def attach_rt_clk_menu(self, evt):
self.widg = evt.widget
self.post(evt.x_root, evt.y_root)
for k,v in self.help_per_context.items():
if k == self.widg:
self.message = v[0]
self.help_title = v[1]
break
self.widg.update_idletasks()
# AUTOFILL ENTRIES
class EntryAuto(tk.Entry):
def __init__(
self, master, autofill=False, values=None, *args, **kwargs):
tk.Entry.__init__(self, master, *args, **kwargs)
self.master = master
self.autofill = autofill
self.values = values
self.config(bd=0)
if autofill is True:
self.bind("<KeyPress>", self.detect_pressed)
self.bind("<KeyRelease>", self.get_typed)
self.bind("<FocusIn>", self.deselect, add="+")
def detect_pressed(self, evt):
""" Runs on every key press. """
if self.autofill is False:
return
key = evt.keysym
if len(key) == 1:
self.pos = self.index('insert')
keep = self.get()[0:self.pos]
self.delete(0, 'end')
self.insert(0, keep)
def get_typed(self, evt):
""" Run on every key release; filters out most non-alpha-numeric
keys; runs the functions not triggered by events.
"""
def do_it():
hits = self.match_string()
self.show_hits(hits, self.pos)
if self.autofill is False:
return
key = evt.keysym
# allow alphanumeric characters
if len(key) == 1:
do_it()
# allow hyphens and apostrophes
elif key in ('minus', 'quoteright'):
do_it()
# look for other chars that should be allowed in nested names
else:
pass
def match_string(self):
hits = []
got = self.get()
use_list = self.values
for item in use_list:
if item.lower().startswith(got.lower()):
hits.append(item)
return hits
def show_hits(self, hits, pos):
cursor = pos + 1
if len(hits) != 0:
self.autofilled = hits[0]
self.delete(0, 'end')
self.insert(0, self.autofilled)
self.icursor(cursor)
def deselect(self, evt):
""" Since this is an autofill, replacement of selected text doesn't
work as expected, so clear the selection as a workaround.
"""
self.select_clear()
class EntryAutoEventType(EntryAuto):
autofill_values = []
def create_lists():
EntryAutoEventType.autofill_values = [i[1] for i in EVENT_TYPES]
def __init__(self, master, *args, **kwargs):
EntryAuto.__init__(self, master, *args, **kwargs)
pass
class EntryAutoSinglePlace(EntryAuto):
def __init__(self, master, tree, *args, **kwargs):
EntryAuto.__init__(self, master, *args, **kwargs)
self.tree = tree
self.tree.single_place_autofill_inputs.append(self)
class EntryAutoSource(EntryAuto):
def __init__(self, master, tree, *args, **kwargs):
EntryAuto.__init__(self, master, *args, **kwargs)
self.tree = tree
self.tree.source_autofill_inputs.append(self)
# *******************************************************
styles = {
("Button",): {
"bg": "bg", "fg": "fg", "font": "font",
"activebackground": "highlight_bg"},
("ButtonPlain",): {
"bg": "fg", "fg": "bg", "font": "font",
"activebackground": "head_bg"},
("ButtonQuiet",): {
"bg": "bg", "fg": "bg", "font": "boilerplate",
"activebackground": "head_bg"},
("Cell", "CellAutofill", "CellAutoEventType", "CellAutoPlace", "CellNote"): {
"bg": "bg", "fg": "fg", "insertbackground": "fg", "font": "font",
"selectbackground": "highlight_bg", "selectforeground": "fg" },
("CellHilited",): {
"bg": "head_bg", "fg": "fg", "insertbackground": "fg", "font": "font",
"selectbackground": "highlight_bg", "selectforeground": "fg"},
("Checkbox", ): {"bg": "fg"},
("Checkbutton",): {
"bg": "bg", "fg": "fg", "selectcolor": "bg",
"activebackground": "highlight_bg"},
("Combobox", "Separator", "FrameHilited2", "CanvasHilited",
"ComboboxDropdown"): {"bg": "highlight_bg"},
("ComboboxDropdownButton",): {"bg": "highlight_bg", "fg": "fg",
"font": "font", "activebackground": "fg", "activeforeground": "bg"},
("Entry", "Text", "EntryAuto", "EntryAutoEventType",
"EntryAutoPlace", "EntryAutoSource"): {
"bg": "highlight_bg", "fg": "fg", "insertbackground": "fg",
"selectbackground": "head_bg", "selectforeground": "fg",
"font": "font"},
("EntryHilited", "EntryAutoPerson", "EntryLabel"): {
"bg": "bg", "fg": "fg", "insertbackground": "fg",
"selectbackground": "head_bg", "selectforeground": "fg",
"font": "font"},
("Frame", "Canvas", "Toplevel", "FrameHilited5", "FrameBordered",
"EventsTable", "TabBook", "ScrolledDialog", "ScrolledText",
"StatusbarTooltips", "PlacesTab", "DoList"): {"bg": "bg"},
("Label", "LabelFrame", "MessageCopiable"): {
"bg": "bg", "fg": "fg", "font": "font"},
("LabelDots", "LabelButtonText", "LabelEntry", "LabelHover"): {
"bg": "bg", "fg": "fg", "font": "font"},
("LabelH2",): {"bg": "bg", "fg": "fg", "font": "heading2"},
("LabelH3",): {"bg": "bg", "fg": "fg", "font": "heading3"},
("LabelHeader",): {"bg": "highlight_bg", "fg": "fg", "font": "heading3"},
("LabelHilited",): {"bg": "highlight_bg", "fg": "fg", "font": "font"},
("LabelHilited2", "Scaler",): {"bg": "head_bg", "fg": "fg", "font": "font"},
("LabelHeaderBgHead",): {"bg": "head_bg", "fg": "fg", "font": "heading3"},
("LabelNegative", ): {"bg": "fg", "fg": "bg", "font": "font"},
("LabelStatusbar",): {"bg": "bg", "fg": "fg", "font": "status"},
("LabelStay", "FrameStay",): {},
("LabelTip",): {"bg": "head_bg", "fg": "fg", "font": "status"},
("Menu",): {"bg": "highlight_bg", "fg": "fg", "selectcolor": "head_bg",
"font": "font", "activebackground": "bg", "activeforeground": "fg"},
("Menubutton",): {"bg": "highlight_bg", "fg": "fg", "highlightcolor": "bg",
"font": "font", "highlightbackground": "head_bg",
"activebackground": "bg", "activeforeground": "fg"},
("Radiobutton",): {
"bg": "bg", "fg": "fg", "selectcolor": "highlight_bg",
"activebackground": "highlight_bg"},
("RightClickMenu",): {
"bg": "highlight_bg", "fg": "fg", "selectcolor": "head_bg",
"activebackground": "bg", "activeforeground": "fg", "font": "font"},
("Scale",): {"bg": "bg", "fg": "fg", "highlightcolor": "head_bg",
"font": "font", "highlightbackground": "bg",
"activebackground": "head_bg", "troughcolor": "highlight_bg"},
("Scrollbar", ): {"bg": "head_bg"},
("LabelTip",): {"bg": "bg", "fg": "fg", "font": "tab_font"},}
def get_all_descendants (ancestor, deep_list):
""" List every widget in the app by running recursively. """
lst = ancestor.winfo_children()
for item in lst:
deep_list.append(item)
get_all_descendants(item, deep_list)
return deep_list
def configall(dlg, formats):
descendants = []
tabbooks = []
scrollbars = []
comboboxes = []
cells = []
get_all_descendants(dlg, descendants)
for widget in descendants:
subclass = type(widget).__name__
if subclass == "TabBook":
tabbooks.append(widget)
elif subclass == "Scrollbar":
scrollbars.append(widget)
elif subclass == "Combobox":
comboboxes.append(widget)
elif subclass in (
"CellAutofill", "CellAutoPlace", "CellAutoEventType",
"MessageCopiable", "Cell", "CellHilited"):
cells.append(widget)
for tup in styles:
if subclass in tup:
opts = styles[tup]
break
for key, val in opts.items():
# # For errors here, uncomment this:
# print("The last line that prints is the one with the problem:", key, subclass)
widget[key] = formats[val]
dlg["bg"] = formats["bg"]
for widg in tabbooks:
widg.format_tabs(formats)
widg.formats = formats
for widg in scrollbars:
widg.format_scrollbars(formats)
for widg in comboboxes:
widg.format_comboboxes(formats)
for widg in cells:
widg.set_height()
""" SIMPLE WIDGETS
All these widgets follow the same reconfiguration pattern.
"""
class Button(tk.Button):
def __init__(self, master, formats, *args, **kwargs):
tk.Button.__init__(self, master, *args, **kwargs)
self.config(overrelief=tk.GROOVE)
class ButtonQuiet(tk.Button):
""" Same color as background, no text. """
def __init__(self, master, formats, *args, **kwargs):
tk.Button.__init__(self, master, *args, **kwargs)
self.config(overrelief=tk.GROOVE)
class Canvas(tk.Canvas):
def __init__(self, master, formats, *args, **kwargs):
tk.Canvas.__init__(self, master, *args, **kwargs)
self.config(bd=0, highlightthickness=0)
class CanvasHilited(tk.Canvas):
def __init__(self, master, formats, *args, **kwargs):
tk.Canvas.__init__(self, master, *args, **kwargs)
self.config(bd=0, highlightthickness=0)
class Checkbutton(tk.Checkbutton):
def __init__(self, master, formats, *args, **kwargs):
tk.Checkbutton.__init__(self, master, *args, **kwargs)
self.config(padx=6, pady=6)
class EntryLabel(tk.Entry):
def __init__(self, master, formats, *args, **kwargs):
tk.Entry.__init__(self, master, *args, **kwargs)
self.config(bd=1, relief="sunken")
class FrameBordered(tk.Frame):
def __init__(self, master, formats, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.config(bd=1, relief="solid")
class FrameHilited(tk.Frame):
def __init__(self, master, formats, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.config(bd=3, relief="groove")
class FrameHilited2(tk.Frame):
def __init__(self, master, formats, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
class FrameHilited4(tk.Frame):
def __init__(self, master, formats, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.config(bd=2, relief='sunken')
class FrameHilited5(tk.Frame):
def __init__(self, master, formats, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.bd = 2
self.config(bd=self.bd, relief='sunken')
class LabelH2(tk.Label):
def __init__(self, master, formats, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
class LabelH3(tk.Label):
def __init__(self, master, formats, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
class LabelHeader(tk.Label):
def __init__(self, master, formats, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
self.config(bd=1, relief="raised")
class LabelHilited(tk.Label):
def __init__(self, master, formats, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
class LabelHilited2(tk.Label):
def __init__(self, master, formats, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
class LabelNegative(tk.Label):
def __init__(self, master, formats, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
class MessageCopiable(tk.Text):
""" To use as a Label whose text can be selected with mouse, set the state
to disabled after constructing the widget and giving it text. Enable
temporarily to change color or text, for example.
"""
def __init__(self, master, formats, *args, **kwargs):
tk.Text.__init__(self, master, *args, **kwargs)
self.config(bd=0)
def set_height(self):
# answer is wrong first time thru mainloop so update:
self.update_idletasks()
lines = self.count('1.0', 'end', 'displaylines')
self.config(height=lines)
self.tag_configure('left', justify='left')
self.tag_add('left', '1.0', 'end')
self.config(state='disabled')
# How to use:
# www = MessageCopiable(root, width=32)
# www.insert(1.0,
# 'Maecenas quis elit eleifend, lobortis turpis at, iaculis '
# 'odio. Phasellus congue, urna sit amet posuere luctus, mauris '
# 'risus tincidunt sapien, vulputate scelerisque ipsum libero at '
# 'neque. Nunc accumsan pellentesque nulla, a ultricies ex '
# 'convallis sit amet. Etiam ut sollicitudi felis, sit amet '
# 'dictum lacus. Mauris sed mattis diam. Pellentesque eu malesuada '
# 'ipsum, vitae sagittis nisl Morbi a mi vitae nunc varius '
# 'ullamcorper in ut urna. Maecenas auctor ultrices orci. '
# 'Donec facilisis a tortor pellentesque venenatis. Curabitur '
# 'pulvinar bibendum sem, id eleifend lorem sodales nec. Mauris '
# 'eget scelerisque libero. Lorem ipsum dolor sit amet, consectetur '
# 'adipiscing elit. Integer vel tellus nec orci finibus ornare. '
# 'Praesent pellentesque aliquet augue, nec feugiat augue posuere ')
# www.grid()
# www.set_height()
class Radiobutton(tk.Radiobutton):
""" To see selection set the selectcolor option to either bg or highlight_bg.
"""
def __init__(self, master, formats, *args, **kwargs):
tk.Radiobutton.__init__(self, master, *args, **kwargs)
self.config(padx=6, pady=6)
class Scale(tk.Scale):
def __init__(self, master, formats, *args, **kwargs):
tk.Scale.__init__(self, master, *args, **kwargs)
self.config(highlightthickness=1)
class Separator(tk.Frame):
def __init__(self, master, formats, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.config(height=2)
class LabelStatusbar(tk.Label):
""" Statusbar messages on focus-in to listed widgets,
tooltips in statusbars, use with StatusbarTooltips.
"""
def __init__(self, master, formats, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
class LabelStatusbar(tk.Label):
""" Statusbar messages on focus-in to listed widgets,
tooltips in statusbars, use with StatusbarTooltips.
"""
def __init__(self, master, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
class StatusbarTooltips(tk.Frame):
""" Grid in last row of dialog, generally column 1 since scridth_w is in
column 0:
self.statusbar = StatusbarTooltips(self, self.formats)
self.statusbar.grid(column=1, row=2, sticky='ew')
After widgets have been made:
visited = (
(self.widget1,
'status bar message on focus in',
'tooltip message on mouse hover.'),
(self.widget2,
'status bar message on focus in',
'tooltip message on mouse hover.'))
run_statusbar_tooltips(
visited,
self.statusbar.status_label,
self.statusbar.tooltip_label,
self.formats)
"""
def __init__(self, master, formats, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.master = master # root or toplevel
self.formats = formats
self.make_widgets()
def make_widgets(self):
self.columnconfigure(0, weight=1)
frm = tk.Frame(self, bd=0)
frm.grid(column=0, row=0, sticky='news')
frm.columnconfigure(0, weight=1)
self.status_label = LabelStatusbar(frm, cursor='arrow', anchor='w')
self.tooltip_label = LabelStatusbar(frm, bd=2, relief='sunken', anchor='e')
self.status_label.grid(column=0, row=0, sticky='w')
self.tooltip_label.grid(column=1, row=0, sticky='e', padx=(6,24))
self.tooltip_label.grid_remove()
def run_statusbar_tooltips(visited, status_label, tooltip_label, toplevel):
""" There will be an error if the widget pointed at has been destroyed,
so don't use these with destroyable widgets. The tooltips by Foord
are more generic.
"""
def handle_statusbar_tooltips(event):
font_size = toplevel.formats["status"][1]
for tup in visited:
if tup[0] is event.widget:
if event.type == '9': # FocusIn
status_label.config(text=tup[1])
elif event.type == '10': # FocusOut
status_label.config(text='')
elif event.type == '7': # Enter
tooltip_label.grid()
tooltip_label.config(
text=tup[2],
bg='black',
fg='white',
font=("verdana", font_size))
elif event.type == '8': # Leave
tooltip_label.config(
text="",
bg=toplevel.formats['bg'],
fg=toplevel.formats['bg'])
tooltip_label.grid_remove()
statusbar_events = ['<FocusIn>', '<FocusOut>', '<Enter>', '<Leave>']
for tup in visited:
widg, status, tooltip = tup
for event_pattern in statusbar_events:
widg.bind(event_pattern, handle_statusbar_tooltips, add='+')
status_label.config(font=toplevel.formats['status'])
class Text(tk.Text):
def __init__(self, master, formats, *args, **kwargs):
tk.Text.__init__(self, master, *args, **kwargs)
self.bind("<Tab>", self.focus_next_window)
self.bind("<Shift-Tab>", self.focus_prev_window)
self.config(wrap='word')
# Make the Text widget use the TAB key for traversal like other widgets.
# `return('break')` prevents the built-in binding to TAB.
def focus_next_window(self, evt):
evt.widget.tk_focusNext().focus()
return('break')
def focus_prev_window(self, evt):
evt.widget.tk_focusPrev().focus()
return('break')
class Toplevel(tk.Toplevel):
def __init__(self, master, formats, *args, **kwargs):
tk.Toplevel.__init__(self, master, *args, **kwargs)
""" WIDGETS THAT DON'T CHANGE COLOR """
class FrameStay(tk.Frame):
def __init__(self, master, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
pass
class LabelStay(tk.Label):
def __init__(self, master, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
pass
class FrameStay(tk.Frame):
""" Don't delete this. It's needed for testing so background color can
be changed.
"""
def __init__(self, master, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
pass