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\app\python\opening.py Last Changed 2024-04-16
# opening.py
from os import path, rename, mkdir, listdir, remove, rmdir
from os.path import isfile, join
from shutil import copy2, copytree
import time
import datetime
import tkinter as tk
from tkinter import filedialog
import sqlite3
from PIL import Image, ImageTk
from files import (
appwide_db_path, set_current_file, get_recent_files, tree_path,
get_current_tree, current_drive, get_current_file, app_path,
save_recent_tree, default_new_tree, treebard_path)
from new_tree import populate_tables
from gedcom_export import ExportGEDCOM
from redraw import redraw_gui
from widgets import (
configall, make_formats_dict, run_statusbar_tooltips, TabBook,
create_tooltip, ButtonBigPic, open_message, RightClickMenu,
make_rc_menus, ScrolledDialog, Combobox, Scrollbar)
from utilities import resize_scrolled_content
from main import Main, ABOUT_TREEBARD
from persons import open_new_person_dialog
from messages import opening_msg, persons_msg, treebard_msg
from messages_context_help import opening_dlg_help_msg
from person_maker import open_person_maker
from query_strings import (
select_image_setting_openpic_dir, select_closing_state_openpic,
update_closing_state_openpic, update_closing_state_recent_files,
insert_family_tree_new, update_family_tree, delete_family_tree,
select_preference_combo_scroll_width, select_family_tree_id_by_title,
select_family_tree_everything, select_family_tree_title,
select_all_family_tree_titles, select_family_tree_by_title,
select_family_tree_details, select_all_place_names, select_family_tree_open,
select_all_nested_place_strings_and_ids, update_closing_state_tree,
select_all_names_all_details_order_hierarchy, select_family_tree_directory,
update_family_tree_all_trees_closed, select_family_tree_path,
select_preference_backups_folder,
update_preference_backups_folder, select_all_sources,
select_all_repository_strings, select_all_place_names_ordered)
import dev_tools as dt
from dev_tools import look, seeline
ICONS = (
'open', 'cut', 'copy', 'paste', 'print', 'home', 'first',
'previous', 'next', 'last', 'search', 'add', 'settings',
'note', 'back', 'forward')
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 get_combobox_scrollbar_width():
conn = sqlite3.connect(appwide_db_path)
cur = conn.cursor()
cur.execute(select_preference_combo_scroll_width)
scrollbar_width = cur.fetchone()[0]
cur.close()
conn.close()
return scrollbar_width
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.
"""
dropdown = tk.Toplevel()
canvas = tk.Canvas(dropdown, 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 = tk.Frame(canvas)
canvas.create_window(0, 0, anchor='nw', window=content)
dropdown.wm_overrideredirect(1)
dropdown.withdraw()
scrollbar_width = get_combobox_scrollbar_width()
return dropdown, canvas, sbv, content, scrollbar_width
class FamilyTree(ScrolledDialog):
def __init__(self, master, user_tree_title, *args, **kwargs):
ScrolledDialog.__init__(self, master, *args, **kwargs)
""" Pass `family_tree_id` to downstream dialogs so the dialogs and their
hover-responsive widgets will have access to the current color scheme
for that family tree.
"""
self.treebard = master
self.user_tree_title = user_tree_title
self.person_autofills = []
self.place_autofill_inputs = []
self.place_autofill_values = []
self.source_autofill_inputs = []
self.source_autofill_values = []
self.repository_autofill_inputs = []
self.repository_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)
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
cur.execute(select_family_tree_everything, (user_tree_title,))
(self.family_tree_id, user_tree_title, directory, tree_is_open, full_path,
filename, colors_type_id) = cur.fetchone()
self.treebard.family_trees[self.family_tree_id] = self
self.formats = make_formats_dict(colors_type_id=colors_type_id)
(self.combobox_dropdown, self.combobox_dropdown.canvas,
self.combobox_dropdown.sbv, self.combobox_dropdown.content,
self.combobox_dropdown.scrollbar_width) = make_combobox_dropdown(
self.formats)
self.protocol(
"WM_DELETE_WINDOW", lambda family_tree=self,
family_tree_id=self.family_tree_id:
close_tree(family_tree, self.family_tree_id))
self.geometry("+2+2")
self.title(f"Treebard Genealogy Software {user_tree_title}")
self.open_family_tree(full_path, directory)
self.person_autofill_values = self.make_all_names_dict_for_person_select()
cur.execute("ATTACH ? as tree", (self.current_tree,))
self.create_single_place_lists(cur)
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.execute("DETACH tree")
cur.close()
conn.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, full_path, directory):
self.ribbon = tk.Frame(self)
self.make_icon_menu()
self.main = Main(
self.canvas, self.formats, self.treebard, self, self.family_tree_id,
self.user_tree_title)
current_file = (f"{current_drive}{full_path}", directory)
self.menu_frame = tk.Frame(self)
self.dropdown = DropdownMenu(
self.menu_frame, self.formats, self.family_tree_id, self.treebard, self.main,
current_file)
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):
file = '{}/images/{}.gif'.format(app_path, name)
pil_img = Image.open(file)
tk_img = ImageTk.PhotoImage(pil_img, master=self.canvas)
icon = tk.Button(
self.ribbon,
image=tk_img,
command=lambda name=name: placeholder(name),
takefocus=0, bd=0, cursor="hand2")
icon.image = tk_img
icon.grid(column=col, row=0)
create_tooltip(icon, name.title())
ribbon_data[name] = icon
ribbon_data['open'].config(
command=lambda: open_tree(tbard=self.treebard))
ribbon_data['add'].config(
command=lambda: open_new_person_dialog(
family_tree=self, family_tree_id=self.family_tree_id,
treebard=self.treebard))
def make_all_names_dict_for_person_select(self):
""" Make a name dict for use in all name autofills. """
conn = sqlite3.connect(appwide_db_path)
cur = conn.cursor()
self.current_tree = get_current_tree(self.family_tree_id, cur)[0]
cur.execute("ATTACH ? AS tree", (self.current_tree,))
cur.execute(select_all_names_all_details_order_hierarchy)
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.execute("DETACH tree")
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, current_tree=None):
""" 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. More commonly, this duplication will occur
because when a new single place is created in the places tab, a
nesting with a single nest is auto-created at the same time. The
fewer nests in a nesting, the more likely it is to be duplicated by
a different place spelled the same.
"""
place_data = []
nestings = []
prepended = None
conn = sqlite3.connect(current_tree)
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:
if new_place and len(self.place_autofill_values) > 5:
prepended = self.place_autofill_values[0:6]
prepended.reverse()
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)
place_autofill_values = [i[0] for i in nestings]
if prepended:
for stg in prepended:
idx = place_autofill_values.index(stg)
popped = place_autofill_values.pop(idx)
place_autofill_values.insert(0, popped)
self.place_autofill_values = place_autofill_values
else:
pass
cur.close()
conn.close()
return place_data
def create_single_place_lists(self, cur=None):
new_connex = False
if cur is None:
conn = sqlite3.connect(self.current_tree)
cur = conn.cursor()
new_connex = True
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
if new_connex:
cur.close()
conn.close()
def create_repository_lists(self, cur=None):
new_connex = False
if cur is None:
conn = sqlite3.connect(self.current_tree)
cur = conn.cursor()
new_connex = True
cur.execute(select_all_repository_strings)
self.repository_autofill_values = [i[0] for i in cur.fetchall()]
for widg in self.repository_autofill_inputs:
widg.values = self.repository_autofill_values
if new_connex:
cur.close()
conn.close()
def create_source_lists(self, cur=None):
new_connex = False
if cur is None:
conn = sqlite3.connect(self.current_tree)
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.
"""
(self.treebard.combobox_dropdown,
self.treebard.combobox_dropdown.canvas,
self.treebard.combobox_dropdown.sbv,
self.treebard.combobox_dropdown.content,
self.treebard.combobox_dropdown.scrollbar_width) = make_combobox_dropdown(
self.formats)
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):
directory = get_current_file()[1]
conn = sqlite3.connect(appwide_db_path)
cur = conn.cursor()
cur.execute(select_family_tree_title, (directory,))
user_tree_title = cur.fetchone()[0]
user_tree_titles = get_all_tree_titles(cur)
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: {user_tree_title}.")
buttons = tk.Frame(self.content)
prior = tk.Label(
self.content, text=text, justify="left", wraplength=360)
# The `family_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, family_tree=self.treebard, entry_width=48,
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()
cur.close()
conn.close()
self.big_button = big_button
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=default_new_tree,
open_new_tree=True))
exporter = tk.Button(
buttons, text='EXPORT', command=self.export_gedcom)
open_sample = tk.Button(
buttons,
text='SAMPLE TREE',
command=lambda treebard=self.treebard,
user_tree_title="Sample Tree":
open_tree(treebard, user_tree_title))
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.get()) == 0:
self.treebard.file_selector.insert(0, "Select an existing tree.")
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):
conn = sqlite3.connect(appwide_db_path)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(update_closing_state_openpic, (self.openpic,))
conn.commit()
cur.close()
conn.close()
def get_pic_dimensions(self):
img_stg = ''.join(self.openpic)
new_stg = '{}/{}/{}'.format(app_path, 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):
conn = sqlite3.connect(appwide_db_path)
cur = conn.cursor()
cur.execute(select_image_setting_openpic_dir)
openpic_dir = cur.fetchone()
userpath = False
if openpic_dir[0] is None:
self.openpic_dir = openpic_dir[1]
else:
self.openpic_dir = openpic_dir[0]
userpath = True
# user can input any desired path to pictures in a settings tab
# if user's path is no good, use openpic_dir[1] after all
if userpath:
print("line", look(seeline()).lineno, "provide full path including drive:")
else:
tbardpath = "{}/images/openpic/".format(app_path)
all_openpics = [f for f in listdir(tbardpath) if isfile(
join(tbardpath, f))]
cur.execute(select_closing_state_openpic)
last_openpic = cur.fetchone()[0]
cur.close()
conn.close()
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 open_prior_tree(treebard):
""" Select current tree based on the last tree closed. If tree is already
open, set focus to it and refuse to open another copy.
"""
tree, directory = get_current_file()
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
cur.execute("ATTACH ? as tree", (tree,))
if path.exists(tree) is False:
msg = open_message(
treebard, opening_msg[0], "Missing File Error", "OK")
msg[0].grab_set()
return
cur.execute(select_family_tree_details, (directory,))
family_tree_id, user_tree_title, tree_is_open = cur.fetchone()
if tree_is_open == 1:
focus_open_tree(family_tree_id, treebard)
cur.execute("DETACH tree")
cur.close()
conn.close()
return
family_tree = FamilyTree(treebard, user_tree_title)
update_family_trees_db(family_tree_id, conn, cur, 1)
cur.execute("DETACH tree")
cur.close()
conn.close()
def focus_open_tree(family_tree_id, treebard):
for child in treebard.winfo_children():
if type(child).__name__ == "FamilyTree":
if child.family_tree_id == family_tree_id:
child.lift()
child.focus_set()
break
def open_tree(treebard, user_tree_title=None):
if user_tree_title is None:
user_tree_title = treebard.file_selector.get()
elif len(user_tree_title) == 0:
return
selector_text = "Select an existing tree."
# Dropdown menu or icon menu was used:
if (user_tree_title is None or len(user_tree_title) == 0 or
user_tree_title == selector_text):
# Show the main window file selector:
treebard.deiconify()
treebard.file_selector.delete(0, "end")
treebard.file_selector.entry["fg"] = "yellow"
treebard.file_selector.insert(0, selector_text)
treebard.file_selector.focus_set()
return
conn = sqlite3.connect(appwide_db_path)
cur = conn.cursor()
cur.execute(select_family_tree_by_title, (user_tree_title,))
family_tree_id, filename, tree_is_open = cur.fetchone()
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
if tree_is_open == 1:
focus_open_tree(family_tree_id, treebard)
cur.close()
conn.close()
return
set_current_file(family_tree_id, conn, cur)
recent_files = get_recent_files()
if recent_files and len(filename) != 0:
save_recent_tree(user_tree_title, recent_files, conn, cur)
family_tree = FamilyTree(treebard, user_tree_title)
update_family_trees_db(family_tree_id, conn, cur, 1)
cur.close()
conn.close()
treebard.file_selector.delete(0, "end")
def make_tree(treebard, copy_this=None, open_new_tree=False):
""" 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.
"""
user_tree_title = open_new_tree_dialog(
treebard, opening_msg[1],
"Give the Tree a Unique Title")
directory = user_tree_title.strip().replace(" ", "_").replace(".", "").replace("'", "").lower()
if len(directory) == 0:
return
dir_path = f"{tree_path}/{directory}"
mkdir(dir_path)
mkdir(f"{dir_path}/images")
filename = f"{directory}.tbd"
file_path = f"{dir_path}/{filename}"
full_path = f"treebard/data/{directory}/{filename}"
copy2(copy_this, file_path)
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
database_file = f"{current_drive}{full_path}"
cur.execute("ATTACH ? as tree", (database_file,))
cur.execute(
insert_family_tree_new,
(user_tree_title, directory, full_path, filename))
conn.commit()
new_family_tree_id = cur.lastrowid
populate_tables(database_file)
cur.execute("DETACH tree")
new_family_tree = FamilyTree(treebard, user_tree_title)
if open_new_tree:
set_current_file(new_family_tree_id, conn, cur)
recent_files = get_recent_files()
if len(user_tree_title) != 0 and recent_files:
save_recent_tree(user_tree_title, recent_files, conn, cur)
user_tree_titles = get_all_tree_titles(cur)
treebard.file_selector.config_values(new_values=user_tree_titles)
cur.close()
conn.close()
return new_family_tree, new_family_tree_id, user_tree_title
def close_tree(family_tree, family_tree_id):
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
update_family_trees_db(family_tree_id, conn, cur, 0)
del TabBook.related_tabbooks[family_tree]
family_tree.destroy()
cur.close()
conn.close()
def exit_app(treebard):
def close_app():
treebard.after(1500, treebard.quit)
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
cur.execute(update_family_tree_all_trees_closed)
conn.commit()
cur.close()
conn.close()
close_app()
def update_family_trees_db(family_tree_id, conn, cur, opening):
cur.execute(update_family_tree, (opening, family_tree_id))
conn.commit()
def delete_current_tree(family_tree, family_tree_id, formats, 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")
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
cur.execute(select_family_tree_directory, (family_tree_id,))
directory, title = cur.fetchone()
recent_files = get_recent_files()
idx = recent_files.index(title)
fixed_files = list(recent_files)
del fixed_files[idx]
most_recent_opened_tree_title = fixed_files[0]
cur.execute(select_family_tree_id_by_title, (most_recent_opened_tree_title,))
prior_tree_id, prior_directory, prior_full_path = cur.fetchone()
cur.execute(update_closing_state_tree, (prior_tree_id,))
conn.commit()
cur.execute(delete_family_tree, (family_tree_id,))
conn.commit()
stored_string = "_+_".join(fixed_files)
deletable_file = f"{tree_path}/{directory}/{directory}.tbd"
project_path = f"{tree_path}/{directory}"
images_path = f"{project_path}/images"
images = listdir(images_path)
thumbnail_path = f"{images_path}/thumbnails"
cur.execute(select_preference_backups_folder)
backups_folder = cur.fetchone()[0]
save_path = f"{backups_folder}/{directory}_deleted_{time_stamp}"
copytree(project_path, save_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):
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)
cur.execute(update_closing_state_recent_files, (stored_string,))
conn.commit()
treebard.family_trees[family_tree_id] = None
cur.execute(select_family_tree_open)
open_trees = [i[0] for i in cur.fetchall()]
for tree_id in open_trees:
tree_widget = treebard.family_trees[tree_id]
treebard.family_trees[tree_id].dropdown.destroy()
current_file = (f"{current_drive}{prior_full_path}", prior_directory)
new_dropdown_menu = DropdownMenu(
tree_widget.menu_frame, formats, tree_id, treebard, family_tree.main,
current_file)
new_dropdown_menu.grid(column=0, row=0, sticky="ew", columnspan=3)
configall(new_dropdown_menu, formats)
user_tree_titles = get_all_tree_titles(cur)
treebard.file_selector.config_values(new_values=user_tree_titles)
cur.close()
conn.close()
del TabBook.related_tabbooks[family_tree]
family_tree.destroy()
def go_to_about(family_tree):
family_tree.main.main_tabs.active = family_tree.main.main_tabs.tabdict[
"preferences"][1]
family_tree.main.main_tabs.make_active()
family_tree.main.options_tabs.active = family_tree.main.options_tabs.tabdict[
"general"][1]
family_tree.main.options_tabs.make_active()
# scroll to top so controls are seen when tab opens
family_tree.main.canvas.yview_moveto(0.0)
class DropdownMenu(tk.Frame):
def __init__(
self, master, formats, family_tree_id, treebard, main, current_file,
*args, **kwargs):
tk.Frame.__init__(self, master, *args, **kwargs)
self.family_tree = master.master
self.formats = formats
self.family_tree_id = family_tree_id
self.treebard = treebard
self.main = main
self.current_file = current_file
self.recent_trees = get_recent_files()
self.make_widgets()
def make_widgets(self):
CASCADES = {
"Recent Trees": self.recent_trees, "Import Tree": IMPORT_TYPES,
"Export Tree": EXPORT_TYPES}
COMMANDS = (
{"New": lambda: make_tree(
treebard=self.treebard, copy_this=default_new_tree,
open_new_tree=True),
"Open": lambda: open_tree(treebard=self.treebard),
"Redraw": lambda: redraw_gui(
main=self.main, formats=self.formats,
family_tree=self.family_tree, family_tree_id=self.family_tree_id,
current_file=self.current_file),
"Save As": lambda: save_as(
family_tree=self.family_tree, family_tree_id=self.family_tree_id,
treebard=self.treebard),
"Save Copy As": lambda: save_copy_as(
family_tree_id=self.family_tree_id, treebard=self.treebard),
"Rename": lambda: rename(
family_tree=self.family_tree, family_tree_id=self.family_tree_id,
formats=self.formats, treebard=self.treebard),
"Recent Trees": None, "Import Tree": None, "Export Tree": None,
"Delete Current Tree": lambda: delete_current_tree(
family_tree=self.family_tree, family_tree_id=self.family_tree_id,
formats=self.formats, treebard=self.treebard),
"Close": lambda: close_tree(
family_tree=self.family_tree, family_tree_id=self.family_tree_id),
"Exit": lambda: exit_app(treebard=self.treebard)},
{"Cut": checktest, "Copy": checktest, "Paste": checktest},
{"Add Person": lambda: open_new_person_dialog(
family_tree=self.family_tree, family_tree_id=self.family_tree_id,
treebard=self.treebard),
"Add Place": checktest,
"Add Conclusion": checktest, "Add Assertion": checktest,
"Add Source": checktest, "Add Image": checktest,
"Delete Current Person From Tree": checktest,
"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},
{"Relationship Calculator": checktest, "Date Calculator": checktest,
"Duplicates & Matches Detector": checktest,
"Unlikelihood Detector": checktest,
"Certainty Calculator": checktest,
"Person Maker": lambda: open_person_maker(master=self.treebard)},
{"About Treebard": lambda: go_to_about(family_tree=self.family_tree),
"Users' Manual": checktest, "Genealogy Tips & Tricks": checktest,
"Treebard Website": checktest, "Forum & Blog": checktest,
"Help Search": checktest, "Contact": checktest,
"Feature Requests": checktest, "Source Code": checktest,
"Donations": checktest})
for col in range(7):
self.columnconfigure(col, weight=1)
drop_items = {}
for column, text0 in enumerate(MENUBUTTONS):
mb = tk.Menubutton(self, text=text0)#, width=9
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 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:
open_tree(treebard=self.treebard, user_tree_title=text))
else:
mb.menu1.add_command(label=text, command=checktest)
def open_new_tree_dialog(master, message, title):
def ok():
cancel()
def cancel():
msg.destroy()
def show():
gotten = gotvar.get().strip()
return gotten
def focus(evt):
msg.focus_force()
filename_input.focus_set()
gotvar = tk.StringVar()
msg = tk.Toplevel(master)
msg.bind("<Visibility>", focus)
msg.columnconfigure(0, weight=1)
msg.protocol("WM_DELETE_WINDOW", cancel)
# Do not do `msg.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.
msg.title(title)
lab = tk.Label(
msg, text=message, justify='left',
font=("courier", 14, "bold"), wraplength=1200)
filename_input = tk.Entry(
msg, textvariable=gotvar, font=("dejavu sans mono", 14))
buttons = tk.Frame(msg)
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(colors_type_id=1)
configall(msg, formats)
# children of msg
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')
master.wait_window(msg)
gotten = show()
return gotten
def save_as(family_tree, family_tree_id, treebard):
""" Copy the current tree to a new directory with a user-input name. Close
the current tree and open the new tree.
"""
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
cur.execute(select_family_tree_path, (family_tree_id,))
result = cur.fetchone()[0]
full_path = f"{current_drive}{result}"
make_tree(treebard, copy_this=full_path, open_new_tree=True)
cur.close()
conn.close()
close_tree(family_tree, family_tree_id)
def save_copy_as(family_tree_id, treebard):
""" Copy the current tree to a new directory with a user-input name. Do not
close the current tree and do not open the new tree.
"""
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
cur.execute(select_family_tree_path, (family_tree_id,))
result = cur.fetchone()[0]
full_path = f"{current_drive}{result}"
new_family_tree, new_family_tree_id = make_tree(
treebard, copy_this=full_path)[0:2]
del TabBook.related_tabbooks[new_family_tree]
new_family_tree.destroy()
update_family_trees_db(new_family_tree_id, conn, cur, 0)
cur.close()
conn.close()
def rename(family_tree, family_tree_id, formats, treebard):
""" Copy the current tree to a new directory with a user-input name. Delete
the current tree and open the new tree.
"""
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
cur.execute(select_family_tree_path, (family_tree_id,))
result = cur.fetchone()[0]
full_path = f"{current_drive}{result}"
make_tree(treebard, copy_this=full_path, open_new_tree=True)
cur.close()
conn.close()
delete_current_tree(family_tree, family_tree_id, formats, treebard)
def get_all_tree_titles(cur):
cur.execute(select_all_family_tree_titles)
return [i[0] for i in cur.fetchall()]
def validate_backups_folder(backups_folder, cur, conn, changing=False):
print("line", look(seeline()).lineno, "backups_folder", backups_folder)
title = "Select a backups folder OUTSIDE OF your `{drive}:/treebard` folder."
if backups_folder is None or len(backups_folder) == 0:
backups_folder = filedialog.askdirectory(title=title)
cur.execute(update_preference_backups_folder, (backups_folder,))
conn.commit()
print("line", look(seeline()).lineno, "backups_folder", backups_folder)
return backups_folder
elif changing:
cur.execute(update_preference_backups_folder, (backups_folder,))
conn.commit()
print("line", look(seeline()).lineno, "backups_folder", backups_folder)
return backups_folder
elif backups_folder is not None and len(backups_folder) != 0:
print("line", look(seeline()).lineno, "backups_folder", backups_folder)
return backups_folder
def get_backups_folder(backups_folder=None, cur=None, conn=None, changing=False):
""" Treebard is a portable program. If the user deletes a tree, it isn't
stored in the Windows Recycle Bin, it is gone forever. So when the
user deletes a tree, Treebard stores a copy in in the user's backups
folder which must be outside of the Treebard folder. Treebard won't
open if a backups folder is not stored in the database.
"""
new_connex = False
if cur is None:
conn = sqlite3.connect(appwide_db_path)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
new_connex = True
if changing is False:
cur.execute(select_preference_backups_folder)
backups_folder_stored = cur.fetchone()[0]
backups_folder = validate_backups_folder(backups_folder_stored, cur, conn)
else:
backups_folder = validate_backups_folder(
backups_folder, cur, conn, changing=changing)
if new_connex:
cur.close()
conn.close()
if backups_folder is None:
return False
elif backups_folder is False:
return False
elif len(backups_folder) == 0:
return False
elif backups_folder.lower().startswith(treebard_path.lower()):
return False
else:
return backups_folder