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\app\python\widgets.py Last Changed 2024-04-16
# widgets.py
import tkinter as tk
import sqlite3
from files import get_current_file, appwide_db_path
from type_elements import get_all_event_types
from new_tree import EVENT_TYPES
from utilities import COLOR_STRINGS
from query_strings import (
select_family_tree_colors_type_id, select_colors_type_colors_default,
select_colors_type_colors_by_id, select_font_preference)
import dev_tools as dt
from dev_tools import look, seeline
NEUTRAL_COLOR = "#454545"
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):
""" 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.
"""
# Set width:
min_width = 6
max_width = 32
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.update_idletasks()
lines = self.count('1.0', 'end', 'displaylines')
self["height"] = lines
self.tag_configure('left', justify='left')
self.tag_add('left', '1.0', 'end')
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 CellAutoPlace(Cell):
def __init__(
self, master, family_tree, values=[], *args, **kwargs):
Cell.__init__(self, master, *args, **kwargs)
self.master = master
self.family_tree = family_tree
self.values = values
self.family_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 = []
got = self.get("1.0", "end-1c")
use_list = self.family_tree.place_autofill_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 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):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0
def showtip(self, text):
""" Display text in tooltip window. """
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)
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=1,
bg=NEUTRAL_COLOR, fg="white")
self.label.pack(ipadx=6)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
def create_tooltip(widget, text):
""" Call w/ arguments to use M. Foord's ToolTip class. """
def enter(event):
toolTip.showtip(text)
def leave(event):
toolTip.hidetip()
toolTip = ToolTip(widget)
widget.bind('<Enter>', enter, add="+")
widget.bind('<Leave>', leave, add="+")
""" Widgets whose colors or fonts change in response to events need their own
highlight methods and their `formats` attribute has to be reconfigured in
the colorizer `apply` method. Since passing `formats` to these widgets
would leave them with obsolete values every time the colorizer is applied,
instead their toplevel is detected and its formats values are used. The
toplevel and all its widgets share the same formats values so only the
family_tree i.e. main toplevel `formats` attribute is updated in the
colorizer method.
"""
class ButtonBigPic(tk.Button):
""" Used for top_pic on person tab and tree pics on opening window. """
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):
""" Handles extra complexity of compound events in notes.py. """
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. Displays text. """
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,
family_tree,
family_tree_id,
treebard,
dialog_class,
current_person_id=None,
assertion_id=None,
linked_element=None,
*args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
self.family_tree = family_tree
self.family_tree_id = family_tree_id
self.treebard = treebard
self.dialog_class = dialog_class
self.current_person_id = current_person_id
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.family_tree,
self.family_tree_id,
self.treebard,
header=self.header,
inwidg=evt.widget,
current_person_id=self.current_person_id,
event_id=self.event_id,
assertion_id=self.assertion_id,
linked_element=self.linked_element)
class LabelEntryDynamic(tk.Label):
""" Highlights on focus. Used for table cells that will be overlaid by an
input in order to edit their contents.
"""
def __init__(
self, master, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
self.initial_text = ""
dlg = self.winfo_toplevel()
self.bind("<Enter>", lambda evt: self.highlight(evt, dlg=dlg))
self.bind("<Leave>", lambda evt: self.unhighlight(evt, dlg=dlg))
self.bind("<FocusIn>", lambda evt: self.highlight(evt, dlg=dlg))
self.bind("<FocusOut>", lambda evt: self.unhighlight(evt, dlg=dlg))
def highlight(self, evt, dlg):
self.orig_bg = self["bg"]
widg = evt.widget
widg.config(bg=dlg.formats["head_bg"])
self.next_focus = self.tk_focusNext()
self.current = widg
def unhighlight(self, evt, dlg):
evt.widget.config(bg=dlg.formats["bg"])
def return_focus(self):
""" If content is redrawn or focus returns to the top of the tab
traversal for any reason, the user might appreciate not having to
try and figure out which widget is now in focus. Call this to put
focus where it's expected.
"""
self.next_focus.focus_set()
def return_focus_back(self):
""" Like `return_focus` but focus returns to the current widget instead
of the next widget in the traversal.
"""
self.current.focus_set()
class LabelMovable(tk.Label):
""" A label that can be moved to a different grid position
by trading places with another widget on press of an
arrow key. The master can't contain anything but LabelMovables.
The ipadx, ipady, padx, pady, and sticky grid options can
be used as long as they're the same for every LabelMovable in
the master. With some more coding, columnspan and rowspan
could be set too but as it is, the spans should be left at
their default values which is 1.
"""
def __init__(self, master, first_column=0,
first_row=0, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
self.master = master
self.first_column = first_column
self.first_row = first_row
dlg = self.winfo_toplevel()
self.bind(
'<FocusIn>', lambda evt: self.highlight_on_focus(evt=evt, dlg=dlg))
self.bind(
'<FocusOut>',
lambda evt: self.unhighlight_on_unfocus(evt=evt, dlg=dlg))
self.bind('<Key>', self.locate)
self.bind('<Key>', self.move)
self.config(takefocus=1)
def locate(self, evt):
""" Get the grid position of the two widgets that will trade places. """
self.mover = evt.widget
mover_dict = self.mover.grid_info()
self.old_col = mover_dict['column']
self.old_row = mover_dict['row']
self.ipadx = mover_dict['ipadx']
self.ipady = mover_dict['ipady']
self.pady = mover_dict['pady']
self.padx = mover_dict['padx']
self.sticky = mover_dict['sticky']
self.less_col = self.old_col - 1
self.less_row = self.old_row - 1
self.more_col = self.old_col + 1
self.more_row = self.old_row + 1
self.last_column = self.master.grid_size()[0] - 1
self.last_row = self.master.grid_size()[1] - 1
def move(self, evt):
""" Determine which arrow key was pressed and make the trade. """
def move_left():
if self.old_col > self.first_column:
for child in self.master.winfo_children():
if (child.grid_info()['column'] == self.less_col and
child.grid_info()['row'] == self.old_row):
movee = child
movee.grid_forget()
movee.grid(
column=self.old_col, row=self.old_row,
ipadx=self.ipadx, ipady=self.ipady, padx=self.padx,
pady=self.pady, sticky=self.sticky)
self.mover.grid_forget()
self.mover.grid(
column=self.less_col, row=self.old_row, ipadx=self.ipadx,
ipady=self.ipady, padx=self.padx, pady=self.pady,
sticky=self.sticky)
def move_right():
if self.old_col < self.last_column:
for child in self.master.winfo_children():
if (child.grid_info()['column'] == self.more_col and
child.grid_info()['row'] == self.old_row):
movee = child
movee.grid_forget()
movee.grid(
column=self.old_col, row=self.old_row,
ipadx=self.ipadx, ipady=self.ipady, padx=self.padx,
pady=self.pady, sticky=self.sticky)
self.mover.grid_forget()
self.mover.grid(
column=self.more_col, row=self.old_row, ipadx=self.ipadx,
ipady=self.ipady, padx=self.padx, pady=self.pady,
sticky=self.sticky)
def move_up():
if self.old_row > self.first_row:
for child in self.master.winfo_children():
if (child.grid_info()['column'] == self.old_col and
child.grid_info()['row'] == self.less_row):
movee = child
movee.grid_forget()
movee.grid(
column=self.old_col, row=self.old_row,
ipadx=self.ipadx, ipady=self.ipady, padx=self.padx,
pady=self.pady, sticky=self.sticky)
self.mover.grid_forget()
self.mover.grid(
column=self.old_col, row=self.less_row, ipadx=self.ipadx,
ipady=self.ipady, padx=self.padx, pady=self.pady,
sticky=self.sticky)
def move_down():
if self.old_row < self.last_row:
for child in self.master.winfo_children():
if (child.grid_info()['column'] == self.old_col and
child.grid_info()['row'] == self.more_row):
movee = child
movee.grid_forget()
movee.grid(
column=self.old_col, row=self.old_row,
ipadx=self.ipadx, ipady=self.ipady, padx=self.padx,
pady=self.pady, sticky=self.sticky)
self.mover.grid_forget()
self.mover.grid(
column=self.old_col, row=self.more_row, ipadx=self.ipadx,
ipady=self.ipady, padx=self.padx, pady=self.pady,
sticky=self.sticky)
self.locate(evt)
keysyms = {
'Left' : move_left,
'Right' : move_right,
'Up' : move_up,
'Down' : move_down}
for k,v in keysyms.items():
if evt.keysym == k:
v()
self.fix_tab_order()
def fix_tab_order(self):
new_order = []
for child in self.master.winfo_children():
new_order.append((
child,
child.grid_info()['column'],
child.grid_info()['row']))
new_order.sort(key=lambda i: (i[1], i[2]))
for tup in new_order:
widg = tup[0]
widg.lift()
def highlight_on_focus(self, evt, dlg):
evt.widget.config(bg=dlg.formats["head_bg"])
def unhighlight_on_unfocus(self, evt, dlg):
evt.widget.config(bg=dlg.formats["highlight_bg"])
""" Compound widgets have other widgets inside them. Some need special methods
that have to be run separately along with the general recolorization scheme.
"""
'''
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, Return key, 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 a tooltip
that shows the whole text.
Improvements needed...
...stretching the buttons in the dropdown has proved difficult, currently
you can set the dropdown width individually on the combobox to fit the
expected content instead.
...dropdown width is set by the longest content, which works magically compared
to anything else that was tried, but then dropdown width is enforced by
truncating button text for items where text is too long. So you shouldn't
get selected values from the text of the selected button, but rather have
the buttons point to their corresponding text in the collection from which
the buttons were made.
This combobox was created by Scott Robertson, the author of Treebard Genealogy Software, May 2023 after years of rewritings and revisions. The license is "Unlicense" i.e. public domain, and please give credit to the Treebard project whence this woundrous widget sprang into existence after much labor. Oh--and it's not finished or perfected yet.
'''
values1 = (
"Jed", "FredNedJedPhilBillJillTiffSpiffNewtNewtNewt", "Ned", "Phil",
"Jill", "Bill", "Biff", "Tiff", "Spiff", "NNNNNNNNNNNNNNNNNNNN00",
"Jed", "Fred", "Ned", "Phil", "Jill", "Bill", "Biff", "Tiff", "Spiff")
values2 = COLOR_STRINGS
values3 = ("red", "white", "blue")
class Combobox(tk.Frame):
""" Only one combobox dropdown is shown at a time, so only one dropdown
exists per family tree. This is `self.combobox_dropdown` in the
FamilyTree class, referenced here by its alias `self.dropdown`.
"""
def __init__(
self, master, formats, family_tree,
# self, master, formats, family_tree=None,
entry_width=20, arrow_width=2, callback=None, values=[],
*args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.master = master
self.formats = formats
self.family_tree = family_tree
self.entry_width = entry_width
self.arrow_width = arrow_width
self.callback = callback
self.values = values
self.dropdown_is_open = False
self.font_size = self.formats["input_font"][1]
self.dropdown = self.family_tree.combobox_dropdown
self.selected = None
self.result_string = ''
self.entered = None
if self.values:
self.lenval = len(self.values)
self.resize_dropdown()
self.owt = None
self.scrollbar_clicked = False
self.typed = None
self.fixed_x = 0
self.screen_height = self.winfo_screenheight()
self.config(bd=0)
# simulate <<ComboboxSelected>>:
self.var = tk.StringVar()
self.var.trace_add('write', lambda *args, **kwargs: self.combobox_selected())
# simulate ttk.Combobox.current()
self.current = 0
self.make_widgets()
self.master.bind_all('<ButtonRelease-1>', self.close_dropdown, add='+')
# Expose only unique methods of Entry e.g. not `self.config`; `self` is
# a Frame, but the Entry, Toplevel, Canvas, and canvas window Frame
# have to be configured together. To size the entry, use
# `config_combo_width()`.
self.insert = self.entry.insert
self.delete = self.entry.delete
self.get = self.entry.get
# self.focus_set = self.entry.focus_set() # Don't do this.
configall(self, self.formats)
def make_widgets(self):
self.entry = tk.Entry(self, textvariable=self.var, width=self.entry_width)
self.arrow = tk.Label(self, text='\u25BC', width=self.arrow_width)
self.columnconfigure(0, weight=1)
self.entry.grid(column=0, row=0, sticky="ew")
self.arrow.grid(column=1, row=0, sticky="e")
self.make_dropdown()
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='+')
self.current_combo_parts = [self, self.entry, self.arrow, self.dropdown.sbv]
for part in self.current_combo_parts:
part.bind('<Enter>', self.unbind_combo_parts, add="+")
part.bind('<Leave>', self.rebind_combo_parts, add="+")
def make_dropdown(self):
""" The Escape binding is currently not working. It worked in the prior
version but don't break or complicate everything else to fix this
one minor feature.
"""
self.dropdown.content.bind('<Escape>', self.hide_dropdown, add='+')
self.dropdown.bind('<FocusIn>', self.focus_dropdown)
self.dropdown.bind('<Unmap>', self.unhighlight_all_drop_items)
self.resize_dropdown()
def resize_dropdown(self):
gauge = tk.Button(
self.dropdown.content, text='', bd=0,
relief="flat", overrelief="flat", font=self.formats["input_font"])
one_height = gauge.winfo_reqheight()
gauge.destroy()
if self.values:
self.lenval = len(self.values)
self.fit_height = one_height * self.lenval
else:
self.fit_height = 1
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):
""" The `//` is integer division. """
self.dropdown.canvas.yview_scroll(-1 * (event.delta // 120), "units")
if new_values:
self.values = new_values
for button in self.dropdown.content.winfo_children():
button.destroy()
char_width = self.entry_width + self.arrow_width
px_width = self.winfo_reqwidth()
fat_sb = int(self.font_size * 1.66)
skinny_sb = int(self.font_size * 0.75)
self.max_dropdown_height = int(self.screen_height * 0.33)
truncate = False
if self.fit_height >= self.max_dropdown_height:
truncate = True
if self.dropdown.scrollbar_width == "fat":
self.dropdown.sbv["width"] = fat_sb
char_width -= 2
deduct = fat_sb
elif self.dropdown.scrollbar_width == "skinny":
self.dropdown.sbv["width"] = skinny_sb
char_width -= 1
deduct = skinny_sb
self.dropdown.canvas.config(
height=self.max_dropdown_height, width=px_width - deduct,
scrollregion=(0, 0, 0, self.fit_height))
else:
self.dropdown.canvas.config(
width=px_width, height=self.fit_height,
scrollregion=(0, 0, 0, self.fit_height))
for text in self.values:
bt = tk.Button(
self.dropdown.content, text=text[0:char_width],
anchor='w', bd=0, relief="flat", overrelief="flat")
bt.pack(side="top", fill="x")
for evt_stg in ('<Button-1>', '<Return>', '<space>'):
bt.bind(evt_stg, self.get_clicked, add='+')
bt.bind('<Enter>', self.highlight_button)
bt.bind('<Leave>', self.unhighlight_button)
bt.bind('<Tab>', self.tab_out_of_dropdown_fwd)
bt.bind('<Shift-Tab>', self.tab_out_of_dropdown_back)
bt.bind('<KeyPress>', self.traverse_on_arrow)
bt.bind('<FocusOut>', self.unhighlight_button)
bt.bind('<FocusOut>', self.get_tip_widg, add='+')
bt.bind('<FocusIn>', self.get_tip_widg)
bt.bind('<Enter>', self.get_tip_widg, add='+')
bt.bind('<Leave>', self.get_tip_widg, add='+')
if truncate:
bt.bind("<MouseWheel>", on_mousewheel)
for item in self.dropdown.content.winfo_children():
item.config(command=self.callback)
configall(self.dropdown, self.formats)
if new_values:
self.resize_dropdown()
def get_tip_widg(self, evt):
""" '10' is `FocusOut`, '9' is `FocusIn`, '7' is `Enter`, '8' is `Leave` """
widg = evt.widget
evt_type = evt.type
idx = self.dropdown.content.winfo_children().index(widg)
if self.entry_width < len(self.values[idx]):
if evt_type in ('7', '9'):
self.show_overwidth_tip(widg, idx)
elif evt_type in ('8', '10'):
self.hide_overwidth_tip()
def show_overwidth_tip(self, widg, idx):
""" Instead of a horizontal scrollbar, if a dropdown item doesn't all
show in the space allotted, the full text will appear in a tooltip
on highlight. Tooltips code borrowed from Michael Foord.
"""
text = self.values[idx]
if self.owt:
return
x, y, cx, cy = widg.bbox()
x = x + widg.winfo_rootx() + 32
y = y + cy + widg.winfo_rooty() + 32
self.owt = tk.Toplevel(self)
self.owt.wm_overrideredirect(1)
l = tk.Label(
self.owt, bg=NEUTRAL_COLOR, fg="bisque", text=text, bd=1,
relief='solid')
l.pack(ipadx=6, ipady=3)
self.owt.wm_geometry(f"+{x}+{y}")
def hide_overwidth_tip(self):
tip = self.owt
self.owt = None
if tip:
tip.destroy()
def close_dropdown(self, evt):
widg = evt.widget
self.dropdown.withdraw()
self.dropdown_is_open = False
def focus_entry_on_arrow_click(self, evt):
self.focus_set()
self.entry.select_range(0, 'end')
def open_or_close_dropdown(self, evt=None):
if self.values:
self.config_values()
else:
return
self.hide_overwidth_tip()
if evt is None:
# dropdown item clicked--no evt because of Button command option
if self.callback:
self.callback(self.selected)
self.dropdown.withdraw()
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()
self.dropdown_is_open = False
return
elif evt_sym == 'Escape':
self.hide_dropdown()
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()
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.dropdown.canvas.yview_moveto(0.0)
elif evt_sym == 'Up':
last.config(bg=self.formats["bg"])
last.focus_set()
self.dropdown.canvas.yview_moveto(1.0)
self.fly_up_or_drop_down(evt)
self.dropdown.deiconify()
self.dropdown_is_open = True
def fly_up_or_drop_down(self, evt):
""" 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
fly_up, fly_up_clearance = self.get_vertical_pos(
combo_height, height, evt)
if fly_up is False:
drop_y = down
else:
drop_y = fly_up_clearance
self.dropdown.geometry(f"+{over_in_screen}+{drop_y}")
def get_vertical_pos(self, combo_height, height, evt):
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
return fly_up, fly_up_clearance
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()
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 unbind_combo_parts(self, evt):
self.master.unbind_all('<ButtonRelease-1>')
def rebind_combo_parts(self, evt):
self.master.bind_all('<ButtonRelease-1>', self.close_dropdown, add='+')
def unhighlight_all_drop_items(self, evt):
for child in self.dropdown.content.winfo_children():
child.config(bg=self.formats["highlight_bg"])
def hide_drops_on_title_bar_click(self, evt):
x, y = self.winfo_pointerxy()
hovered = self.winfo_containing(x,y)
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()
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() # WHY NOT JUST CLOSE THE DROPDOWN?
def get_typed(self):
self.typed = self.var.get()
def highlight_on_traverse(self, evt, next_item=None, prev_item=None):
evt_type = evt.type
evt_sym = evt.keysym # 2 is key press, 4 is button press
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()
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()
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()
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 callback(self):
""" A function specified on instantiation. """
print('this will not print if overridden (callback)')
# pass
def combobox_selected(self):
""" A function specified on instantiation will run when
the selection is made. Similar to ttk's <<ComboboxSelected>>
but instead of binding to a virtual event, whatever that is
(in order to use Tkinter I should not have to be as smart as
Tkinter's creators).
"""
# print('this will not print if overridden (combobox_selected)')
pass
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, *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]}")
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. """
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()}*")
select_all_family_trees = "SELECT * FROM family_tree"
def get_default_formats(cur):
cur.execute(select_colors_type_colors_default)
default_formats = list(cur.fetchone()) + ["courier", "dejavu sans mono", 12]
return default_formats
def get_tree_formats(cur, colors_type_id):
cur.execute(select_colors_type_colors_by_id, (colors_type_id,))
colors_type = list(cur.fetchone())
cur.execute(select_font_preference)
font_scheme = list(cur.fetchone()[0:2])
user_formats = colors_type + font_scheme
user_formats.insert(5, INPUT_FONT)
return user_formats
def get_colors_type_id(family_tree_id):
conn = sqlite3.connect(appwide_db_path)
cur = conn.cursor()
cur.execute(select_family_tree_colors_type_id, (family_tree_id,))
colors_type_id = cur.fetchone()[0]
cur.close()
conn.close()
return colors_type_id
def make_formats_dict(root=False, colors_type_id=None):
""" To add a style, add a string to the end of FORMAT_KEYS
and an item to the end of FORMAT_VALUES.
"""
tree = get_current_file()[0] # can't use get_current_tree here?
conn = sqlite3.connect(appwide_db_path)
cur = conn.cursor()
cur.execute("ATTACH ? AS tree", (tree,))
if colors_type_id == 0: # added 20240226
colors_type_id = 1
# A new `colors_type_id` was specified by the named parameter:
if colors_type_id is not None:
prefs_to_use = list(get_tree_formats(cur, colors_type_id))
# Use the default color scheme:
elif colors_type_id is None or root is True:
prefs_to_use = list(get_default_formats(cur))
else:
prefs_to_use = list(get_tree_formats(cur, colors_type_id))
FORMAT_VALUES = (
prefs_to_use[0],
prefs_to_use[1],
prefs_to_use[2],
prefs_to_use[3],
(prefs_to_use[4], prefs_to_use[6]),
(prefs_to_use[5], prefs_to_use[6]),
(prefs_to_use[4], prefs_to_use[6] * 2, 'bold'),
(prefs_to_use[4], int(prefs_to_use[6] * 1.5), 'bold'),
(prefs_to_use[4], int(prefs_to_use[6] * 1.125), 'bold'),
(prefs_to_use[4], int(prefs_to_use[6] * 0.75), 'bold'),
(prefs_to_use[5], int(prefs_to_use[6] * 0.83)),
(prefs_to_use[5], int(prefs_to_use[6] * 0.66)),
(prefs_to_use[5], prefs_to_use[6], 'italic'),
(prefs_to_use[5], int(prefs_to_use[6] * .75), 'italic'),
(prefs_to_use[4], int(prefs_to_use[6] * 0.75)))
formats = dict(zip(FORMAT_KEYS, FORMAT_VALUES))
cur.execute("DETACH tree")
cur.close()
conn.close()
return formats
INPUT_FONT = "dejavu sans mono"
FORMAT_KEYS = (
'bg', 'highlight_bg', 'head_bg', 'fg',
'output_font', 'input_font',
'heading1', 'heading2', 'heading3', 'heading4',
'status', 'boilerplate', 'show_font',
'unshow_font', 'tab_font')
def get_colors_type(tree):
conn = sqlite3.connect(tree)
cur = conn.cursor()
new_colors_type_id = cur.fetchone()
cur.close()
conn.close()
return new_colors_type_id
""" 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 `family_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()
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 five 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, title, buttlab, inwidg=None):
def close():
""" Override this if more needs to be done on close. """
msg.destroy()
msg = tk.Toplevel(master)
msg.title(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))
formats = make_formats_dict(colors_type_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.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)
self.formats = make_formats_dict(colors_type_id=1)
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 # added 20221120
self.widg.update_idletasks()
# AUTOFILL ENTRIES
""" Possibly can't use `get_current_tree` in these classes since the class level
variables have no access to self.family_tree_id. Use `get_current_file` till
it gets ironed out.
"""
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 EntryAutoRepository(EntryAuto):
def __init__(self, master, family_tree, *args, **kwargs):
EntryAuto.__init__(self, master, *args, **kwargs)
self.family_tree = family_tree
self.family_tree.repository_autofill_inputs.append(self)
class EntryAutoSinglePlace(EntryAuto):
def __init__(self, master, family_tree, *args, **kwargs):
EntryAuto.__init__(self, master, *args, **kwargs)
self.family_tree = family_tree
self.family_tree.single_place_autofill_inputs.append(self)
class EntryAutoSource(EntryAuto):
def __init__(self, master, family_tree, *args, **kwargs):
EntryAuto.__init__(self, master, *args, **kwargs)
self.family_tree = family_tree
self.family_tree.source_autofill_inputs.append(self)
# ***************************************************************************************
styles = {
("Button",): {
"bg": "bg", "fg": "fg", "font": "output_font",
"activebackground": "highlight_bg"},
("ButtonPlain",): {
"bg": "bg", "fg": "fg", "font": "input_font",
"activebackground": "head_bg"},
("ButtonQuiet",): {
"bg": "bg", "fg": "fg", "font": "boilerplate",
"activebackground": "head"},
("Cell", "CellAutofill", "CellAutoEventType", "CellAutoPlace"): {
"bg": "bg", "fg": "fg", "insertbackground": "fg", "font": "output_font",
"selectbackground": "highlight_bg", "selectforeground": "fg" },
("Checkbox", ): {"bg": "fg"},
("Checkbutton",): {
"bg": "bg", "fg": "fg", "selectcolor": "bg",
"activebackground": "highlight_bg"},
("Combobox", "Separator"): {"bg": "highlight_bg"},
("ComboboxDropdownButton",): {"bg": "highlight_bg", "fg": "fg",
"font": "input_font", "activebackground": "fg", "activeforeground": "bg"},
("Entry", "Text", "EntryAuto", "EntryAutoEventType", "EntryAutoPerson"): {
"bg": "highlight_bg", "fg": "fg", "insertbackground": "fg",
"selectbackground": "head_bg", "selectforeground": "fg",
"font": "input_font"},
("EntryHilited",): {
"bg": "bg", "fg": "fg", "insertbackground": "fg",
"selectbackground": "head_bg", "selectforeground": "fg",
"font": "input_font"},
("Frame", "Canvas", "Toplevel", "FrameHilited5", "FrameBordered",
"EventsTable", "TabBook", "ScrolledDialog", "ScrolledText",
"StatusbarTooltips", "PlacesTab"): {"bg": "bg"},
("Label", "LabelFrame", "MessageCopiable"): {
"bg": "bg", "fg": "fg", "font": "output_font"},
("LabelDots", "LabelButtonText", "LabelEntry", "LabelEntryDynamic",
"LabelHover"): {"bg": "bg", "fg": "fg", "font": "input_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": "input_font"},
("LabelHeaderBgHead",): {"bg": "head_bg", "fg": "fg", "font": "heading3"},
("LabelMovable",): {"bg": "highlight_bg", "fg": "fg", "font": "output_font"},
("LabelNegative"): {"bg": "fg", "fg": "bg", "font": "output_font"},
("LabelStatusbar",): {"bg": "bg", "fg": "fg", "font": "status"},
("LabelStay",): {},
("LabelTip",): {"bg": "head_bg", "fg": "fg", "font": "status"},
("Menu",): {"bg": "highlight_bg", "fg": "fg", "selectcolor": "head_bg",
"font": "input_font", "activebackground": "bg", "activeforeground": "fg"},
("Menubutton",): {"bg": "highlight_bg", "fg": "fg", "highlightcolor": "bg",
"font": "input_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": "input_font"},
("Scale",): {"bg": "bg", "fg": "fg", "highlightcolor": "head_bg",
"font": "output_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 = []
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)
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)
""" 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(text='', width=3, 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 Entry(tk.Entry):
def __init__(self, master, formats, *args, **kwargs):
tk.Entry.__init__(self, master, *args, **kwargs)
class Frame(tk.Frame):
def __init__(self, master, formats, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
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 FrameHilited3(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 Label(tk.Label):
def __init__(self, master, formats, *args, **kwargs):
tk.Label.__init__(self, master, *args, **kwargs)
class LabelFrame(tk.LabelFrame):
def __init__(self, master, formats, *args, **kwargs):
tk.LabelFrame.__init__(self, master, *args, **kwargs)
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 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)
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, *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=("dejavu sans mono", 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