opening.py
Nov 25, 2022 4:00:33 GMT -8
Post by Uncle Buddy on Nov 25, 2022 4:00:33 GMT -8
<drive>:\treebard\opening.py Last Changed 2024-07-25
# opening.py
from tkinter import Label
from os import path, mkdir, listdir, remove, rmdir, rename, makedirs
from os.path import isfile, join
from shutil import copy2, copytree
import time
import datetime
import webbrowser
import tkinter as tk
import sqlite3
from PIL import Image, ImageTk
from base import (
Query, current_drive, unigeds_path, tree_path, image_path, trash_path,
tbard_path)
from new_tree import populate_tables
from gedcom_export import ExportGEDCOM
from redraw import Redraw
from widgets import (
configall, make_formats_dict, run_statusbar_tooltips, TabBook,
create_tooltip, ButtonBigPic, open_message, RightClickMenu, FrameHilited2,
make_rc_menus, ScrolledDialog, Combobox, Scrollbar, CanvasHilited)
from base import resize_scrolled_content
from user_formats import get_treebard_fonts
from main import Main, ABOUT_TREEBARD
from persons import open_new_person_dialog, delete_current_person
from messages import opening_msg
from messages_context_help import opening_dlg_help_msg
from person_maker import open_person_maker
from unigeds_queries import (
select_all_place_names, select_all_nested_place_strings_and_ids,
select_all_sources, select_name_data, select_all_place_names_ordered)
import dev_tools as dt
from dev_tools import look, seeline
""" I tried using `title` for the title given the family tree by the user, and
I was sorry because it's a reserved word in both Python and Tkinter. I
replaced this globally with `dub` and every so often I find a place where it
should still be `title`. I think I caught them all?
"""
SELECTOR_TEXT = "Select..."
ICONS = (
'open', 'cut', 'copy', 'paste', 'print', 'home person', 'prior person', 'next person',
'search', 'add', 'settings', 'note')
IMPORT_TYPES = ("From a Treebard .tbd file", "From a GEDCOM .ged file")
EXPORT_TYPES = ("To a Treebard .tbd file", "To a GEDCOM .ged file")
MENUBUTTONS = (
"File", "Edit", "Elements", "Output", "Research", "Tools", "Help")
MOD_KEYS = ("Ctrl", "Alt", "Shift", "Ctrl+Alt", "Ctrl+Shift", "Alt+Shift")
def checktest():
print("ok")
def make_combobox_dropdown(formats):
""" Only one combobox shows a dropdown at a time so there's only one per
family tree. The root app `treebard` needs its own dropdown. The
dropdown is used in the Combobox class.
"""
def fill_canvas(evt):
""" Make the content frame expand. """
canvas.itemconfig(frame_id, width=evt.width)
dropdown = tk.Toplevel()
canvas = CanvasHilited(dropdown, formats, bd=0, highlightthickness=0)
sbv = Scrollbar(dropdown, formats=formats,command=canvas.yview)
canvas.config(yscrollcommand=sbv.set)
canvas.grid(column=0, row=0, sticky="news")
sbv.grid(column=1, row=0, sticky="ns")
content = FrameHilited2(canvas, formats)
frame_id = canvas.create_window(0, 0, anchor='nw', window=content)
canvas.bind("<Configure>", fill_canvas)
dropdown.wm_overrideredirect(1)
dropdown.withdraw()
scrollbar_width = 16
return dropdown, canvas, sbv, content, scrollbar_width
def placeholder(evt=None, name=""):
print('menu test:', name.upper())
print('evt:', evt)
class FamilyTree(ScrolledDialog):
def __init__(self, master, dub, tree_id, *args, **kwargs):
ScrolledDialog.__init__(self, master, *args, **kwargs)
self.treebard = master
self.dub = (f"Treebard Genealogy Software {dub}")
self.tree_id = tree_id
self.comboboxes = {}
self.person_autofills = []
self.place_autofill_inputs = []
self.place_autofill_values = []
self.prepended_places = []
self.source_autofill_inputs = []
self.source_autofill_values = []
self.single_place_autofill_inputs = []
self.single_place_autofill_values = []
self.place_data = []
self.existing_place_names = set()
self.open_assertions_dialogs_tracker = {"event_ids": {}, "name_ids": {}}
self.maxsize(
width=self.winfo_screenwidth() - 12,
height=self.winfo_screenheight() - 36)
self.treebard.family_trees[self.tree_id]["tree"] = self
self.file = f"{tree_path}/{self.tree_id}/{self.tree_id}.tbd"
if path.exists(self.file):
self.proceed_open_tree()
else:
print("ERROR MESSAGE: The selected tree no longer exists. It had "
"been deleted or moved outside of Treebard's controls. So Treebard "
"has now deleted all reference to the tree and you can re-open "
"Treebard.")
delete_current_tree(self, self.treebard)
exit_app(self.treebard)
def proceed_open_tree(self):
conn = sqlite3.connect(self.file)
cur = conn.cursor()
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 = ?
''',
(self.tree_id,))
result = curx.fetchone()
if result:
color_scheme_id = result[0]
self.formats = make_formats_dict(
self.tree_id, color_scheme_id=color_scheme_id)
self.comboboxes = {}
self.protocol(
"WM_DELETE_WINDOW",
lambda tree=self, treebard=self.treebard: close_tree(tree, treebard))
self.geometry("+2+2")
self.forward_stack = []
self.backward_stack = []
self.open_family_tree()
self.person_autofill_values = self.make_all_names_dict_for_person_select()
self.create_single_place_lists()
self.prior_nested_place = None
self.title(f"Treebard Genealogy Software {self.dub}")
configall(self, self.formats)
self.resize_families_table_scrollbar()
TabBook.resize_scrolled_dialog_with_tabbook(
self, self.main.canvas, self.main)
ScrolledDialog.bind_canvases_to_mousewheel()
cur.close()
conn.close()
curx.close()
conx.close()
self.focus_force()
self.main.current_person_input.focus_set()
def resize_families_table_scrollbar(self):
self.update_idletasks()
ht = self.main.right_panel.winfo_reqheight()
wd = self.main.nukefam_table.nukefam_window.winfo_reqwidth()
self.main.nukefam_table.nukefam_canvas.config(
width=wd, height=ht,
scrollregion=self.main.nukefam_table.nukefam_canvas.bbox('all'))
def open_family_tree(self):
self.ribbon = tk.Frame(self)
self.make_icon_menu()
self.main = Main(self.canvas, self.formats, self.treebard, self)
self.menu_frame = tk.Frame(self)
self.dropdown = DropdownMenu(
self.menu_frame, self.formats, self.treebard, self.main)
self.canvas.create_window(0, 0, anchor='nw', window=self.main)
# children of self (Override the parent class griddings to make
# room for the dropdown menu and the ribbon menu.)
self.rowconfigure(0, weight=0)
self.rowconfigure(1, weight=0)
self.rowconfigure(3, weight=1)
self.menu_frame.grid(column=0, row=0, sticky="ew", columnspan=2)
self.ribbon.grid(column=1, row=1, sticky="w", columnspan=2)
self.scridth_n.grid(column=0, row=2, sticky='ew', columnspan=2)
self.scridth_w.grid(column=0, row=3, sticky='ns', rowspan=2)
self.canvas.grid(column=1, row=3, sticky="news")
self.vsb.grid(column=2, row=0, sticky='ns', rowspan=4)
self.hsb.grid(column=1, row=4, sticky='ew')
self.statusbar.grid(column=1, row=5, sticky='ew')
# children of self.menu_frame
self.dropdown.pack(side="left", fill="x", expand="true")
self.update_idletasks()
minwidth = self.ribbon.winfo_reqwidth() + 16
self.columnconfigure(1, minsize=minwidth)
self.treebard.iconify()
def make_icon_menu(self):
ribbon_data = {}
for col, name in enumerate(ICONS):
text = '_'.join(name.split())
file = f"{image_path}/{text}.gif"
pil_img = Image.open(file)
tk_img = ImageTk.PhotoImage(pil_img, master=self.canvas)
icon = tk.Button(
self.ribbon,
text=text,
image=tk_img,
command=lambda text=text: placeholder(text),
takefocus=0, bd=0, cursor="hand2")
icon.image = tk_img
icon.grid(column=col, row=0)
create_tooltip(icon, name.title(), self.formats)
ribbon_data[text] = icon
ribbon_data['open'].config(
command=lambda treebard=self.treebard:open_tree(treebard))
home_icon = ribbon_data["home_person"]
home_icon.config(command=lambda key="tree":
self.main.change_current_person(key))
prior_icon = ribbon_data["prior_person"]
prior_icon.config(command=lambda key="backward":
self.main.change_current_person(key))
next_icon = ribbon_data["next_person"]
next_icon.config(command=lambda key="forward":
self.main.change_current_person(key))
ribbon_data['add'].config(
command=lambda tree=self, treebard=self.treebard:
open_new_person_dialog(tree, treebard))
def make_all_names_dict_for_person_select(self):
""" Make a name dict for use in all name autofills. """
conn = sqlite3.connect(self.file)
cur = conn.cursor()
cur.execute(select_name_data)
results = cur.fetchall()
person_ids = [i[0] for i in results]
values = [list(i[1:]) for i in results]
values = [i + [False] for i in values]
inner_dict = []
PERSON_DATA = (
"name", "name type", "name id", "sort order", "used by", "dupe name")
for tup in values:
indict = dict(zip(PERSON_DATA, tup))
inner_dict.append(indict)
cur.close()
conn.close()
values = list(zip(person_ids, inner_dict))
new_values = {}
for tup in values:
idnum, name_dict = tup
if new_values.get(idnum):
new_values[idnum].append(name_dict)
else:
new_values[idnum] = [name_dict]
all_names = []
dupes = []
for lst in new_values.values():
for dkt in lst:
for k,v in dkt.items():
if k == "name":
stg = v
if stg in all_names and stg not in dupes:
dupes.append(stg)
else:
all_names.append(stg)
person_autofill_values = new_values
a = 0
for k,v in new_values.items():
b = 0
for dkt in v:
c = 0
for kk,vv in dkt.items():
if dkt["name"] in dupes:
person_autofill_values[k][b]["dupe name"] = True
c += 1
b += 1
a += 1
return person_autofill_values
def update_person_autofill_values(self):
people = self.make_all_names_dict_for_person_select()
for ent in self.person_autofills:
ent.values = people
return people
def get_place_values(self, new_place=False):
""" Make a list of dicts for place data including IDs.
Make a non-unique list of nestings so treebard knows if there are
multiple whole nestings spelled the same for the user to choose
among. This rare duplication might for example be a place, likely
a country, with the same name but two identities that need to be
tracked separately, maybe because of radically different epoch
and/or governance.
"""
place_data = []
nestings = []
conn = sqlite3.connect(self.file)
cur = conn.cursor()
cur.execute(select_all_place_names)
self.existing_place_names = set([i[0] for i in cur.fetchall()])
if new_place or len(self.place_autofill_values) == 0:
cur.execute(select_all_nested_place_strings_and_ids)
tups = cur.fetchall()
for tup in tups:
nestings.append(
(", ".join([i for i in tup[0:9] if i != "unknown"]), tup[9]))
nestings = sorted(nestings, key=lambda f: f[0])
for tup in nestings:
nesting, nesting_id = tup
dkt = {nesting: {"nested_place_id": nesting_id}}
place_data.append(dkt)
self.place_autofill_values = [i[0] for i in nestings]
else:
pass
cur.close()
conn.close()
return place_data
def create_single_place_lists(self):
conn = sqlite3.connect(self.file)
cur = conn.cursor()
cur.execute(select_all_place_names_ordered)
self.single_place_autofill_values = ([i[0] for i in cur.fetchall()])
for widg in self.single_place_autofill_inputs:
# This doesn't work for autofills that don't exist yet.
widg.values = self.single_place_autofill_values
cur.close()
conn.close()
def create_source_lists(self, cur=None):
new_connex = False
if cur is None:
conn = sqlite3.connect(self.file)
cur = conn.cursor()
new_connex = True
cur.execute(select_all_sources)
self.source_autofill_values = [i[0] for i in cur.fetchall()]
for widg in self.source_autofill_inputs:
widg.values = self.source_autofill_values
if new_connex:
cur.close()
conn.close()
class OpeningChoices(tk.Frame):
def __init__(self, master, formats, treebard, filebook, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.content = master
self.formats = formats
self.treebard = treebard
self.filebook = filebook
self.rc_menu = RightClickMenu(self.treebard, self.treebard)
self.make_widgets()
def make_widgets(self):
""" The opening screen buttons exist because a tree should not open
automatically. If the user has two trees that are similar,
he might start working on the wrong tree without realizing it. Or
if the wrong tree opens automatically and it's very large, he
might have to wait for it to open just so he can close it. So the
user has to select a tree to open every time the app loads. The
big picture button that opens the prior tree opens in focus to
make this effortless.
"""
big_button = ButtonBigPic(
self.filebook.store["FILES"],
command=lambda: open_prior_tree(treebard=self.treebard))
big_button.grid(column=0, row=1, padx=12, pady=12, rowspan=3)
self.make_inputs(big_button)
def make_inputs(self, big_button):
query = Query()
tree_title = query.select("recent_files").split("_+_")[0]
conx = sqlite3.connect(tbard_path)
curx = conx.cursor()
curx.execute("SELECT dub FROM family_tree")
user_tree_titles = [i[0] for i in curx.fetchall()]
text = (f"Select a tree to open (below) or click the big picture "
f"(left) to open the same tree that was last open. The prior tree "
f"was: {tree_title}.")
buttons = tk.Frame(self.content)
prior = tk.Label(
self.content, text=text, justify="left", wraplength=360)
# The `tree` arg provides a reference to the combobox dropdown
# needed in the opening screen before any family trees or their
# corresponding combobox dropdowns become available.
self.treebard.file_selector = Combobox(
self.content, self.formats, tree=self.treebard,
values=user_tree_titles)
self.about = tk.Label(
self.content,
text=ABOUT_TREEBARD,
justify='left',
wraplength=360, bd=1, relief="raised")
# children of self.content
buttons.grid(column=0, row=0, sticky="ew")
prior.grid(column=1, row=1, sticky="ew", padx=(0,36), pady=(12,0))
self.treebard.file_selector.grid(
column=1, row=2, sticky="n", padx=(0,36), pady=(12,0))
self.about.grid(
column=1, row=3, sticky="sew", padx=(0,36), pady=12,
ipadx=6, ipady=12)
opener, new, exporter, open_sample, cancel = self.make_button_menu(
buttons)
self.update_idletasks()
self.picwidth = buttons.winfo_reqwidth()
self.show_openpic(big_button)
self.make_help_widgets(
opener, new, exporter, open_sample, cancel, big_button)
resize_scrolled_content(
self.treebard, self.treebard.canvas, self.treebard.window)
self.store_last_openpic()
self.big_button = big_button
curx.close()
conx.close()
def make_button_menu(self, buttons):
opener = tk.Button(
buttons,
text='OPEN TREE',
command=lambda treebard=self.treebard:
open_tree(treebard))
opener.bind("<Enter>", self.hint)
new = tk.Button(
buttons, text='NEW TREE',
command=lambda: make_tree(treebard=self.treebard,
copy_this=unigeds_path))
exporter = tk.Button(
buttons, text='EXPORT', command=self.export_gedcom)
open_sample = tk.Button(
buttons,
text='SAMPLE TREE',
command=lambda treebard=self.treebard, dub="Sample Tree":
open_tree(treebard, dub))
cancel = tk.Button(buttons, text='CANCEL')
opener.grid(column=0, row=0, pady=24, padx=(36,24))
new.grid(column=1, row=0, padx=24, pady=24)
exporter.grid(column=2, row=0, padx=24, pady=24)
open_sample.grid(column=3, row=0, padx=24, pady=24)
cancel.grid(column=4, row=0, pady=24, padx=(24,36))
return opener, new, exporter, open_sample, cancel
def hint(self, evt):
if len(self.treebard.file_selector.entry.get()) == 0:
self.treebard.file_selector.entry.insert(0, SELECTOR_TEXT)
def make_help_widgets(
self, opener, new, exporter, open_sample, cancel, big_button):
visited = (
(opener,
"Open Tree...",
"Select an existing tree before pressing the OPEN TREE button."),
(new,
"New Tree...",
"Create a new tree."),
(exporter,
"Export GEDCOM...",
"Create a new GEDCOM file from an existing Treebard tree."),
(open_sample,
"Open Sample Tree...",
"Open the tree that comes with Treebard."),
(cancel,
"Close Dialog",
"Close this dialog leaving Treebard open."),
(big_button,
"Open Prior Tree",
"Re-open the last tree that was used."))
run_statusbar_tooltips(
visited,
self.treebard.statusbar.status_label,
self.treebard.statusbar.tooltip_label,
self.treebard)
rcm_widgets = (
opener, new, exporter, open_sample, cancel, big_button)
make_rc_menus(
rcm_widgets,
self.rc_menu,
opening_dlg_help_msg)
def store_last_openpic(self):
query = Query()
query.update("openpic", self.openpic)
def get_pic_dimensions(self):
img_stg = ''.join(self.openpic)
new_stg = f'{self.openpic_dir}/{img_stg}'
self.current_image = Image.open(new_stg)
resize_factor = self.picwidth / self.current_image.width
self.picwidth = int(resize_factor * self.current_image.width)
self.picheight = int(resize_factor * self.current_image.height)
self.current_image = self.current_image.resize(
(self.picwidth, self.picheight),
Image.LANCZOS)
return self.current_image
def show_openpic(self, big_button):
self.select_opening_image()
self.current_image = self.get_pic_dimensions()
img1 = ImageTk.PhotoImage(self.current_image, master=self.treebard)
big_button.config(image=img1)
big_button.image = img1
bd_ht = 2
frm_ht = 80
big_button.config(width=self.picwidth)
def select_opening_image(self):
self.openpic_dir = f"{image_path}/openpic/"
all_openpics = [part for part in listdir(self.openpic_dir) if isfile(
join(self.openpic_dir, part))]
query = Query()
last_openpic = query.select("openpic")
last_in_list = len(all_openpics) - 1
p = 0
for pic in all_openpics:
if p + 1 > last_in_list:
self.openpic = all_openpics[0]
break
elif pic == last_openpic:
self.openpic = all_openpics[p + 1]
break
else:
p += 1
def export_gedcom(self):
self.export_gedcom_dialog = ExportGEDCOM(self.treebard)
def get_current_file():
""" As soon as a file opens, set it as the prior file. So the prior file is
the current file if a file is open and is the last one opened.
"""
query = Query()
tree_id = query.select("prior_family_tree")
prior_file = f"{tree_id}.tbd"
if prior_file == "unigeds.db":
db = prior_file
settings_dir = f"{tree_path}/settings"
return db, settings_dir
elif len(prior_file) > 0:
prior_path = f"{tree_path}/{tree_id}/{prior_file}"
return prior_path, tree_id
else:
return "", tree_id
def open_prior_tree(treebard):
""" Select current tree based on the last tree closed. If tree is already
open, set focus to it and don't open another copy.
"""
prior_tree, tree_id = get_current_file()
if path.exists(prior_tree) is False:
msg = open_message(
treebard, opening_msg[0], "Missing File Error", "OK",
formats=treebard.formats)
msg[0].grab_set()
return
conx = sqlite3.connect(tbard_path)
curx = conx.cursor()
curx.execute(
''' SELECT dub
FROM family_tree
WHERE family_tree_id = ?
''',
(tree_id,))
dub = curx.fetchone()[0]
stop = focus_open_tree(tree_id, treebard)
if stop: return
treebard.family_trees[tree_id]["open"] = True
handle_recent_files(treebard, tree_id, dub, curx, mode="open")
tree = FamilyTree(treebard, dub, tree_id, name=tree_id)
treebard.family_trees[tree_id]["tree"] = tree
curx.close()
conx.close()
def focus_open_tree(tree_id, treebard):
""" Focus an open tree if user tries to open or create the same tree. """
for child in treebard.winfo_children():
if type(child).__name__ == "FamilyTree" and child.winfo_name() == tree_id:
tree = child
if tree.state() in ("iconic", "withdrawn"):
tree.deiconify()
tree.lift()
tree.focus_force()
return True
return False
def open_tree(treebard, dub=None):
if dub is None:
dub = treebard.file_selector.entry.get()
elif len(dub) == 0:
return
# Dropdown menu or icon menu was used:
if (dub is None or len(dub) == 0 or
dub == SELECTOR_TEXT):
# Show the main window file selector:
treebard.deiconify()
treebard.file_selector.entry.delete(0, "end")
treebard.file_selector.entry["fg"] = "yellow"
treebard.file_selector.entry.insert(0, SELECTOR_TEXT)
treebard.file_selector.entry.focus_set()
return
conx = sqlite3.connect(tbard_path)
conx.execute('PRAGMA foreign_keys = 1')
curx = conx.cursor()
curx.execute(
''' SELECT family_tree_id
FROM family_tree
WHERE dub = ?
''',
(dub,))
result = curx.fetchone()
if result is None:
return
else:
tree_id = result[0]
filename = f"{tree_id}.tbd"
stop = focus_open_tree(tree_id, treebard)
if stop: return
if treebard.family_trees[tree_id]["open"]:
tree = treebard.family_trees[tree_id]["tree"]
handle_recent_files(treebard, tree_id, dub, curx, mode="open")
else:
treebard.family_trees[tree_id]["open"] = True
handle_recent_files(treebard, tree_id, dub, curx, mode="open")
tree = FamilyTree(treebard, dub, tree_id, name=tree_id)
if treebard.family_trees.get(tree_id):
treebard.family_trees[tree_id]["tree"] = tree
treebard.file_selector.entry.delete(0, "end")
redraw_dropdown_menus(treebard, tree, dub)
curx.close()
conx.close()
def make_tree(treebard, copy_this=None, open_new_tree=True, renamed_tree=None):
""" The user can keep copies of his files anywhere but for the app to be
portable, everything it needs has to be kept in one folder. Treebard
creates the program files and folders based on a title chosen by the
user when he makes a new tree.
"""
dub = open_new_tree_dialog(
treebard, opening_msg[1],
"Give the Tree a Unique Title")
if len(dub) == 0:
print("CANCEL button was pressed")
return None
tree_id = dub.strip().replace(" ", "_").replace(
".", "").replace("'", "").lower()
if tree_id in treebard.family_trees or len(tree_id) == 0:
print("error message: tree already exists or name is blank")
if treebard.family_trees.get(tree_id):
if treebard.family_trees[tree_id]["open"] is False:
print("error message: that tree already exists")
return True
stop = focus_open_tree(tree_id, treebard)
if stop: return None
if renamed_tree:
new_tree_id = tree_id
new_title = dub
return new_tree_id, new_title
dir_path = f"{tree_path}/{tree_id}"
mkdir(dir_path)
mkdir(f"{dir_path}/images")
file_path = f"{dir_path}/{tree_id}.tbd"
file = f"{tree_path}/{tree_id}/{tree_id}.tbd"
copy2(copy_this, file_path)
if copy_this == unigeds_path:
populate_tables(file)
conx = sqlite3.connect(tbard_path)
conx.execute("PRAGMA foreign_keys = 1")
curx = conx.cursor()
curx.execute(
''' INSERT INTO family_tree (family_tree_id, dub)
VALUES (?, ?)
''',
(tree_id, dub,))
conx.commit()
curx.execute("SELECT dub FROM family_tree")
user_tree_titles = [i[0] for i in curx.fetchall()]
treebard.file_selector.config_values(new_values=user_tree_titles)
treebard.family_trees[tree_id] = {}
if open_new_tree:
treebard.family_trees[tree_id]["open"] = True
handle_recent_files(
treebard, tree_id, dub, curx, mode="open")
new_family_tree = FamilyTree(treebard, dub, tree_id, name=tree_id)
treebard.family_trees[tree_id]["tree"] = new_family_tree
redraw_dropdown_menus(treebard, new_family_tree, dub)
curx.close()
conx.close()
return new_family_tree, tree_id, dub
elif open_new_tree is False:
treebard.family_trees[tree_id]["open"] = False
treebard.family_trees[tree_id]["tree"] = None
curx.close()
conx.close()
return tree_id
def close_tree(tree, treebard):
del TabBook.related_tabbooks[tree]
treebard.family_trees[tree.tree_id]["open"] = False
treebard.family_trees[tree.tree_id]["tree"] = None
tree.destroy()
def exit_app(treebard):
treebard.quit()
def handle_recent_files(treebard, tree_id, dub, curx, mode):
""" Run this callback
1) every time the recent files menu is clicked,
2) every time a tree is opened, and
3) every time a new tree is made (if the new tree is also opened).
Every time this callback runs,
1) edit the value of the recent trees string,
2) store the new value of the recent trees string in config.txt,
3) redraw the dropdown menu in all open trees,
4) open tree clicked in recent tree menu if the tree isn't open,
5) lift/focus tree clicked in recent tree menu if the tree is open.
"""
if tree_id is None or len(dub) == 0:
return
query = Query()
trees = query.select("recent_files")
if trees:
treebard.recent_files = trees.split("_+_")
if mode == "open":
if dub not in treebard.recent_files:
treebard.recent_files.insert(0, dub)
else:
treebard.recent_files.insert(
0, treebard.recent_files.pop(
treebard.recent_files.index(dub)))
if len(treebard.recent_files) > 20:
treebard.recent_files = treebard.recent_files[0:20]
elif mode == "delete":
idx = treebard.recent_files.index(dub)
del treebard.recent_files[idx]
curx.execute(
''' SELECT family_tree_id
FROM family_tree
WHERE dub = ?
''',
(treebard.recent_files[0],))
tree_id = curx.fetchone()[0]
stored_string = "_+_".join(treebard.recent_files)
query.update("recent_files", stored_string)
query.update("prior_family_tree", tree_id)
trees = query.select("recent_files")
if trees:
treebard.recent_files = trees.split("_+_")
def delete_current_tree(tree, treebard):
""" Delete the current tree. Python's built-in `os.path.exists()` method
should prevent errors in case the user has manually deleted the tree
or its directories.
"""
now = datetime.datetime.now()
time_stamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
conx = sqlite3.connect(tbard_path)
conx.execute('PRAGMA foreign_keys = 1')
curx = conx.cursor()
curx.execute(
''' SELECT dub
FROM family_tree
WHERE family_tree_id = ?
''',
(tree.tree_id,))
dub = curx.fetchone()[0]
handle_recent_files(treebard, tree.tree_id, dub, curx, mode="delete")
curx.execute(
''' DELETE FROM family_tree
WHERE family_tree_id = ?
''',
(tree.tree_id,))
conx.commit()
deletable_file = f"{tree_path}/{tree.tree_id}/{tree.tree_id}.tbd"
project_path = f"{tree_path}/{tree.tree_id}"
recycle_path = f"{trash_path}/{tree.tree_id}"
if not path.exists(recycle_path):
makedirs(recycle_path)
images_path = f"{project_path}/images"
thumbnail_path = f"{images_path}/thumbnails"
if path.exists(project_path) and path.exists(recycle_path):
copytree(project_path, recycle_path, dirs_exist_ok=True)
if path.exists(thumbnail_path):
thumbs = listdir(thumbnail_path)
for img in thumbs:
thumb_path = f"{thumbnail_path}/{img}"
remove(thumb_path)
rmdir(thumbnail_path)
if path.exists(images_path):
images = listdir(images_path)
for img in images:
image_path = f"{images_path}/{img}"
if path.isfile(image_path):
remove(image_path)
rmdir(images_path)
if path.exists(deletable_file):
remove(deletable_file)
if path.exists(project_path):
rmdir(project_path)
redraw_dropdown_menus(treebard, tree, dub)
del treebard.family_trees[tree.tree_id]
curx.execute("SELECT dub FROM family_tree")
user_tree_titles = [i[0] for i in curx.fetchall()]
treebard.file_selector.config_values(new_values=user_tree_titles)
if TabBook.related_tabbooks.get(tree):
del TabBook.related_tabbooks[tree]
tree.destroy()
curx.close()
conx.close()
def go_to_about(tree):
tree.main.main_tabs.active = tree.main.main_tabs.tabdict[
"preferences"][1]
tree.main.main_tabs.make_active()
tree.main.options_tabs.active = tree.main.options_tabs.tabdict[
"general"][1]
tree.main.options_tabs.make_active()
# scroll to top so controls are seen when tab opens
tree.main.canvas.yview_moveto(0.0)
class DropdownMenu(tk.Frame):
def __init__(self, master, formats, treebard, main, *args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.tree = master.master
self.formats = formats
self.treebard = treebard
self.main = main
self.make_widgets()
def make_widgets(self):
rdw = Redraw(main=self.main, formats=self.formats, tree=self.tree)
CASCADES = {
"Recent Trees": self.treebard.recent_files,
"Import Tree": IMPORT_TYPES, "Export Tree": EXPORT_TYPES}
COMMANDS = (
{"New": lambda treebard=self.treebard, copy_this=unigeds_path:
make_tree(treebard, copy_this),
"Open": lambda treebard=self.treebard: open_tree(treebard),
"Redraw": rdw.redraw_gui,
"Save As": lambda tree=self.tree, dub=self.tree.dub,
treebard=self.treebard, mode="save_as": save_as(
tree, treebard, mode),
"Save Copy As": lambda tree=self.tree, dub=self.tree.dub,
treebard=self.treebard, mode="save_copy_as":
save_as(tree, treebard, mode),
"Rename": lambda tree=self.tree, dub=self.tree.dub,
treebard=self.treebard, main=self.main : rename_tree(
tree, dub, treebard, main),
"Recent Trees": None, "Import Tree": None, "Export Tree": None,
"Delete Current Tree": lambda tree=self.tree, treebard=self.treebard:
delete_current_tree(tree, treebard),
"Close": lambda tree=self.tree, treebard=self.treebard:
close_tree(tree, treebard),
"Exit": lambda treebard=self.treebard: exit_app(treebard)},
{"Cut": checktest, "Copy": checktest, "Paste": checktest},
{"Add Person": lambda tree=self.tree, treebard=self.treebard:
open_new_person_dialog(tree, treebard),
"Add Place": checktest,
"Add Conclusion": checktest, "Add Assertion": checktest,
"Add Source": checktest, "Add Image": checktest,
"Delete Current Person From Tree": lambda tree=self.tree,
main=self.main: delete_current_person(tree, main),
"Delete Current Place From Tree": checktest,
"Delete Current Source From Tree": checktest,
"Merge Two Persons": checktest,
"Merge Two Single Places": checktest,
"Merge Two Sources": checktest,
"Merge Two Citations": checktest},
{"Charts": checktest, "Reports": checktest},
{"Do List": checktest, "Add Research Goals": checktest,
"Contacts": checktest, "Correspondence": checktest},
{"Person Maker": lambda master=self.treebard:
open_person_maker(master),
"Relationship Calculator": checktest, "Date Calculator": checktest,
"Duplicates & Matches Detector": checktest,
"Unlikelihood Detector": checktest,
"Certainty Calculator": checktest},
{"About Treebard": lambda tree=self.tree: go_to_about(tree),
"Video Channel":
lambda url="https://www.youtube.com/@treebardgenealogysoftware2577/playlists":
self.open_web_page(url),
"Genealogy Research":
lambda url="https://www.familysearch.org/search":
self.open_web_page(url),
"Treebard Website":
lambda url="https://treebard.com": self.open_web_page(url),
"Forum & Blog":
lambda url="https://treebard.proboards.com":
self.open_web_page(url),
"Contact":
lambda url="https://treebard.com/about.html":
self.open_web_page(url),
"Source Code":
lambda url="https://treebard.com/download.html":
self.open_web_page(url),
"Donations":
lambda url="https://treebard.com/donate.html":
self.open_web_page(url)})
for col in range(7):
self.columnconfigure(col, weight=1)
drop_items = {}
for column, text0 in enumerate(MENUBUTTONS):
mb = tk.Menubutton(self, text=text0)
mb.grid(column=column, row=0, sticky="ew", pady=1)
drop_items[text0] = mb
mb.menu = tk.Menu(mb, tearoff=0)
mb.config(menu=mb.menu)
for text1, command in COMMANDS[column].items():
if text1 not in CASCADES:
mb.menu.add_command(label=text1, command=command)
else:
self.make_cascade_menu(mb, text1, CASCADES)
def open_web_page(self, url):
webbrowser.open_new(url)
def make_cascade_menu(self, mb, text1, CASCADES):
mb.menu1 = tk.Menu(mb.menu, tearoff=0)
mb.menu.add_cascade(label=text1, menu=mb.menu1)
for text in CASCADES[text1]:
if text1 == "Recent Trees":
mb.menu1.add_command(
label=text,
command=lambda treebard=self.treebard, dub=text:
open_tree(treebard, dub))
else:
mb.menu1.add_command(label=text, command=checktest)
def open_new_tree_dialog(master, message, dub):
def ok():
cancel()
def cancel():
# cancel_was_pressed = True
dlg.destroy()
def show():
return gotvar.get().strip()
def focus(evt):
dlg.focus_force()
filename_input.focus_set()
portable_font = "TkFixedFont"
portable_font = ("verdana", 12)
gotvar = tk.StringVar()
dlg = tk.Toplevel(master)
dlg.bind("<Visibility>", focus)
dlg.columnconfigure(0, weight=1)
dlg.protocol("WM_DELETE_WINDOW", cancel)
# Do not do `dlg.grab_set()` here. It breaks lots of functionalities in Colorizer
# and FontPicker, I don't know why, but it was hard to guess what was causing
# whole groups of Buttons to not run their callbacks at all, Combobox scrollbars
# to go wild, Sliders to become belligerent. If there's something(s) that must
# not be touched while this dialog is open, maybe those things can just be
# temporarily disabled while this dialog is open.
dlg.title(dub)
lab = tk.Label(
dlg, text=message, justify='left',
font=("verdana", 14, "bold"), wraplength=1200)
filename_input = tk.Entry(
dlg, textvariable=gotvar, font=portable_font)
buttons = tk.Frame(dlg)
ok_butt = tk.Button(buttons, text="OK", command=cancel, width=7)
cancel_butt = tk.Button(buttons, text="CANCEL", command=cancel, width=7)
formats = make_formats_dict(None, color_scheme_id='1')
# children of dlg
lab.grid(
column=0, row=0, sticky='news', padx=12, pady=12,
columnspan=2, ipadx=6, ipady=3)
filename_input.grid(column=0, row=1, padx=12, sticky="ew")
buttons.grid(column=0, row=2, sticky='e', padx=(0,12), pady=12)
# children of buttons
ok_butt.grid(column=0, row=0, sticky='e')
cancel_butt.grid(column=1, row=0, padx=(3,0), sticky='e')
configall(dlg, formats)
master.wait_window(dlg)
return show()
def save_as(tree, treebard, mode):
""" `save_as`: Copy the current tree to a new tree_id with a user-input
name. Close the current tree and open the new tree.
`save_copy_as`: Copy the current tree to a new tree_id with a
user-input name. Do not close the current tree and do not open the
new tree.
"""
conx = sqlite3.connect(tbard_path)
conx.execute("PRAGMA foreign_keys = 1")
curx = conx.cursor()
if mode == "save_copy_as":
new_tree_id = make_tree(treebard, copy_this=tree.file, open_new_tree=False)
else:
new_family_tree, new_tree_id, new_title = make_tree(
treebard, copy_this=tree.file)
curx.execute(
''' SELECT color_scheme_id
FROM family_tree
WHERE family_tree_id = ?
''',
(tree.tree_id,))
color_scheme_id = curx.fetchone()[0]
curx.execute(
''' UPDATE family_tree
SET color_scheme_id = ?
WHERE family_tree_id = ?
''',
(color_scheme_id, new_tree_id))
conx.commit()
treebard.family_trees[new_tree_id] = {}
if mode == "save_as":
treebard.family_trees[new_tree_id]["open"] = True
handle_recent_files(
treebard, new_tree_id, new_title, curx, mode="open")
treebard.family_trees[new_tree_id]["tree"] = new_family_tree
redraw_dropdown_menus(treebard, new_family_tree, tree.dub)
formats = make_formats_dict(new_tree_id, color_scheme_id=color_scheme_id)
configall(new_family_tree, formats)
close_tree(tree, treebard)
elif mode == "save_copy_as":
treebard.family_trees[new_tree_id]["open"] = False
treebard.family_trees[new_tree_id]["tree"] = None
curx.close()
conx.close()
def rename_tree(tree, dub, treebard, main):
""" Copy the current tree to a new tree_id with a user-input name. Delete
the current tree and open the new tree.
"""
conx = sqlite3.connect(tbard_path)
conx.execute("PRAGMA foreign_keys = 1")
curx = conx.cursor()
new_tree_id, new_title = make_tree(
treebard, renamed_tree=tree, open_new_tree=False)
tree.file = (f"{tree_path}/{new_tree_id}/{new_tree_id}.tbd")
tree.tree_id = new_tree_id
rename(
f"{tree_path}/{tree.tree_id}/{tree.tree_id}.tbd",
f"{tree_path}/{tree.tree_id}/{new_tree_id}.tbd")
rename(
f"{tree_path}/{tree.tree_id}",
f"{tree_path}/{new_tree_id}")
conx = sqlite3.connect(tbard_path)
conx.execute("PRAGMA foreign_keys = 1")
curx = conx.cursor()
curx.execute(
''' UPDATE family_tree
SET (family_tree_id, dub) = (?, ?)
WHERE family_tree_id = ?
''',
(new_tree_id, new_title, tree.tree_id))
conx.commit()
curx.execute("SELECT dub FROM family_tree")
user_tree_titles = [i[0] for i in curx.fetchall()]
treebard.file_selector.config_values(new_values=user_tree_titles)
del treebard.family_trees[tree.tree_id]
treebard.family_trees[new_tree_id] = {}
treebard.family_trees[new_tree_id]["open"] = True
treebard.family_trees[new_tree_id]["tree"] = tree
# Handle_recent_files:
query = Query()
tree_string = query.select("recent_files")
if tree_string:
recent_files = tree_string.split("_+_")
if dub in recent_files:
idx = recent_files.index(dub)
del recent_files[idx]
recent_files.insert(0, new_title)
if len(recent_files) > 20:
recent_files = recent_files[0:20]
stored_string = "_+_".join(recent_files)
treebard.recent_files = stored_string.split("_+_")
else:
stored_string = new_title
treebard.recent_files = [stored_string]
query.update("recent_files", stored_string)
query.update("prior_family_tree", new_tree_id)
redraw_dropdown_menus(treebard, tree, new_title)
curx.execute(
''' SELECT color_scheme_id
FROM family_tree
WHERE family_tree_id = ?
''',
(new_tree_id,))
color_scheme_id = curx.fetchone()[0]
tree.title(f"Treebard Genealogy Software {new_title}")
formats = make_formats_dict(new_tree_id, color_scheme_id=color_scheme_id)
configall(tree, formats)
curx.close()
conx.close()
def redraw_dropdown_menus(treebard, tree, dub):
""" Redraw the dropdown menu of each open tree when `recent_files` changes,
using that tree's color scheme, with the order of `recent_files` updated.
"""
query = Query()
result = query.select("recent_files")
if result:
treebard.recent_files = result.split("_+_")
for tree_id, dkt in treebard.family_trees.items():
tree = dkt["tree"]
if tree is None:
continue
else:
tree.dropdown.destroy()
tree.dropdown = DropdownMenu(
tree.menu_frame, tree.formats, treebard, tree.main)
tree.dropdown.pack(side="left", fill="x", expand="true")
configall(tree.dropdown, tree.formats)