Root Module
Sept 29, 2020 6:54:27 GMT -8
Post by Uncle Buddy on Sept 29, 2020 6:54:27 GMT -8
# treebard_root_017 (this is the main app and current person stuff)
# _016 had roles and notes dialogs pretty much finished, and right-click menu and statusbar tooltips refactored and working well, getting ready to finally finish the last 3 columns on person tab events table
import files
import tkinter as tk
from PIL import Image, ImageTk
import sqlite3
import styles as st
import utes
import widgets as wdg
import names
import notes
import dates
import gallery as gl
import colorizer as clz
import right_click_menu as rcm
import message_strings as ms
from os import path, rename
import messages as msg
import pedigree_chart as pc
formats = st.make_formats_dict()
current_file = files.get_current_file()[0]
class SplashScreen(wdg.Toplevel):
def __init__(self, *args, **kwargs):
wdg.Toplevel.__init__(self, *args, **kwargs)
root.iconify()
self.overrideredirect(True)
width = self.winfo_screenwidth()
height = self.winfo_screenheight()
splash_file = 'images/splash_small.gif'
pil_img = Image.open(splash_file)
tk_img = ImageTk.PhotoImage(pil_img)
splash_canvas = wdg.Canvas(
self,
height=height*0.33,
width=width*0.33,
bg=formats['bg'])
splash_canvas.create_image(width*0.33/2, height*0.33/2, image=tk_img)
splash_canvas.pack()
dlg_pos = utes.center_dialog(self)
self.geometry('{}x{}+{}+{}'.format(
str(int(width*0.33)),
str(int(height*0.33)),
dlg_pos[0],
dlg_pos[1]))
self.after(500, self.destroy)
root.wait_window(self)
root.deiconify()
class Treebard(wdg.Frame):
def __init__(self, parent, *args, **kwargs):
wdg.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(2, weight=1)
self.files = files.Files(self)
self.open_treebard()
self.icon_menu = IconMenu(self)
grip = wdg.Sizer(parent)
grip.place(relx=1.0, rely=1.0, anchor='se')
grip.bind('<B1-Motion>', grip.resize_se_corner)
self.vscrollbar = wdg.AutoScrollbar(self)
self.hscrollbar = wdg.AutoScrollbar(self, orient='horizontal')
self.root_statusbar = wdg.StatusbarTooltips(self)
self.rc_menu = rcm.RightClickMenu(self)
# Main has to instantiate after AutoScrollbar and StatusBarTooltips
self.main = Main(self, root)
self.main.canvas.grid_remove()
self.top_menu = TopMenu(self)
self.top_menu.grid(column=0, row=0)
self.icon_menu.grid(column=0, row=1, sticky='ew')
self.main.grid(column=0, row=2, sticky='news')
self.vscrollbar.grid(column=1, row=2, sticky='ns')
self.hscrollbar.grid(column=0, row=3, sticky='ew')
self.root_statusbar.grid(column=0, row=4, sticky='ew')
ST.create_custom_theme()
self.resize_scrollbar()
self.resize_window()
self.open_tbard.grab_set()
self.open_tbard.lift()
def open_treebard(self):
''' This opening window is important because you never want to open
a tree 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 really huge, he
might have to wait for it to open just so he can close it. So
this opening dialog might be a bit of a nuisance to deal
with on each loading of the app, but it's not optional. '''
self.open_tbard = wdg.Toplevel()
self.open_tbard.title('Open, Create, or Copy a Tree')
open = wdg.Button(
self.open_tbard,
text='OPEN TREE',
command=lambda: self.files.open_tree(root, dialog=self.open_tbard))
open.grid(column=0, row=0, padx=24, pady=24)
open.focus_set()
new = wdg.Button(
self.open_tbard,
text='NEW TREE',
command=lambda: self.files.make_tree(root, dialog=self.open_tbard))
new.grid(column=1, row=0, padx=24, pady=24)
saveas = wdg.Button(
self.open_tbard,
text='SAVE AS',
command=lambda: self.files.save_as(root))
saveas.grid(column=2, row=0, padx=24, pady=24)
savecopyas = wdg.Button(
self.open_tbard,
text='SAVE COPY AS',
command=lambda: self.files.save_copy_as())
savecopyas.grid(column=3, row=0, padx=24, pady=24)
rename = wdg.Button(
self.open_tbard,
text='RENAME TREE',
command=lambda: self.files.rename_tree(root))
rename.grid(column=4, row=0, padx=24, pady=24)
ST.config_generic(self.open_tbard)
dlg_pos = utes.center_dialog(self.open_tbard)
self.open_tbard.geometry("+{}+{}".format(dlg_pos[0], dlg_pos[1]))
def resize_scrollbar(self):
root.update_idletasks()
self.main.canvas.config(
scrollregion=self.main.canvas.bbox("all"))
def resize_window(self):
root.update_idletasks()
page_x = self.main.content.winfo_reqwidth()
page_y = self.main.content.winfo_reqheight()+24
root.geometry('{}x{}'.format(page_x, page_y))
class IconMenu(wdg.Frame):
def __init__(self, parent, *args, **kwargs):
wdg.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
menu_names = (
'open', 'cut', 'copy', 'paste', 'print', 'home', 'first',
'previous', 'next', 'last', 'search', 'add', 'settings',
'note', 'back', 'forward')
ribbon = {}
r = 0
for name in menu_names:
file = 'images/{}.gif'.format(name)
pil_img = Image.open(file)
tk_img = ImageTk.PhotoImage(pil_img)
icon = wdg.Button(
self,
image=tk_img,
command=placeholder,
takefocus=0)
icon.image = tk_img
icon.grid(column=r, row=0)
utes.create_tooltip(icon, name.title())
ribbon[name] = icon
r += 1
ribbon['open'].config(
command=lambda: files.open_tree(root))
ribbon['home'].config(command=root.quit)
class Main(wdg.Frame):
def __init__(self, parent, root, *args, **kwargs):
wdg.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.root = root
self.tab_names = (('person', 0), ('place', 1), ('source', 0),
('names', 0), ('chart', 0), ('report', 0), ('project', 6),
('contact', 1), ('graphics', 0), ('preferences', 3))
self.tabbook_x = 300
self.tabbook_y = 300
self.make_widgets()
def make_widgets(self):
# containers
self.canvas = wdg.Canvas(
self,
bd=0,
highlightthickness=0)
self.canvas.grid(column=0, row=2, sticky='news')
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(2, weight=1)
self.content = wdg.Frame(self.canvas)
self.tabs = wdg.Tabs(self.content, self.tab_names)
self.tabs.grid(column=0, row=1, sticky='news')
self.tabs.enable_traversal()
# preferences tabbed notebook
pref_tab_names = (
('general', 1),
('date', 0),
('color scheme', 10),
('types', 1))
self.pref_tabs = wdg.Tabs(
self.tabs.store['preferences'], pref_tab_names)
self.pref_tabs.grid()
self.pref_tabs.enable_traversal()
# scrollbars
self.canvas.config(
yscrollcommand=self.parent.vscrollbar.set,
xscrollcommand=self.parent.hscrollbar.set)
self.parent.vscrollbar.config(command=self.canvas.yview)
self.parent.hscrollbar.config(command=self.canvas.xview)
# change-current-person area
current = wdg.Frame(self.content)
current.grid(column=0, row=0, pady=(24, 0))
self.curr_per = wdg.LabelH3(
current,
text='Current Person (ID):',
anchor='e')
self.curr_per.grid(column=1, row=0, padx=12, pady=12)
self.curr_per_displ = wdg.LabelH3(
current,
anchor='w')
self.curr_per_displ.grid(column=2, row=0, padx=12, pady=12)
self.change_curr_per = wdg.LabelH3(
current,
text='Change Current Person to:')
self.change_curr_per.grid(column=3, row=0, padx=(36,12), pady=12)
self.new_person_fill = wdg.AutofillEntry(current)
self.new_person_fill.grid(column=4, row=0, padx=12, pady=12)
self.new_person_fill.focus_set()
self.new_person_fill.config(textvariable=self.new_person_fill.var)
self.new_person_fill.values = names.make_values_list_for_person_select()
self.new_person_fill.fix_max_width()
# The binding to Return also works for Ctrl-Return accidentally but OK
self.new_person_fill.bind('<Key-Return>', self.select_new_current_person)
self.new_person_fill.bind(
'<Control-Key-space>', self.select_new_current_person)
self.new_person_fill.bind(
'<Control-Key-Tab>', self.select_new_current_person)
search_opener = wdg.Button(
current,
text='Find/Create a Person/Name',
command=self.open_person_search_dialog)
search_opener.grid(column=5, row=0, padx=12, pady=12)
# persons tab
id = get_current_person()[0]
current_person_birthname = names.get_name_with_id(id)
self.persons = PersonsTab(
self.tabs.store['person'],
self,
root,
self.new_person_fill,
current_person_birthname)
self.persons.grid(column=0, row=0)
# galleries in Places and Sources tabs and Persons tab gallery dialog
place_gallery = gl.Gallery(
self.tabs.store['place'], self.tabs, self.tabs.store['graphics'],
root,
self.canvas)
source_gallery = gl.Gallery(
self.tabs.store['source'], self.tabs, self.tabs.store['graphics'],
root,
self.canvas)
# names tab
self.names = NamesTab(
self.tabs.store['names'], root, self.new_person_fill)
self.names.grid(column=0, row=0)
# dates tab
date_widgets = DatePrefsWidgets(self.pref_tabs.store['date'], root)
date_widgets.grid(column=0, row=0)
# colors tab
colorizer = clz.Colorizer(
self.pref_tabs.store['color scheme'],
self.pref_tabs,
root,
self.persons.right_panel)
colorizer.grid(column=0, row=0)
# types tab
types_settings_instrux = wdg.LabelH3(
self.pref_tabs.store['types'],
justify='left',
text='For each type choose whether to show, delete or hide it\n'
'so it won\'t be available to select. Built-in types can\'t\n'
'be deleted, just hidden. User-made types can be deleted\n'
'or hidden, except for types that are being used in your tree,\n'
'in which case that type can\'t be deleted or hidden.\n\n'
'Types which can be created, hidden and deleted by the user\n'
'include event types, role types and color schemes.')
types_settings_instrux.grid()
# footer
footer = wdg.Frame(self.content)
footer.columnconfigure(0, weight=1)
footer.rowconfigure(0, weight=1)
footer.grid(column=0, row=2, sticky='news')
self.foot_label = wdg.LabelBoilerplate(
footer,
text="Treebard GPS is free, portable, open-source software "
"written in Python, Tkinter and Sqlite.\nTreebard's purpose "
"is to showcase functionalities that could inspire developers "
"and users of genealogy database software to expect a "
"better user experience.\nGPS stands for "
"'Genieware Pattern Simulation' because GPS is here to show "
"the way.\nCreated 2014 - 2021 by Scott Robertson. Email: "
"stumpednomore-at-gmail.com. Donations: "
"http://gofundme/whearly",
justify='center')
self.foot_label.grid(column=0, row=0, pady=24)
ST.config_generic(root)
# statusbar tooltips for root
visited = (
(self.new_person_fill,
"Change Current Person",
"Select someone or add a name."),
(place_gallery.thumbstrip,
"Thumbnail Views",
"Click thumbnail to display. Hover below to see "
"file name. If radio, click to make main image."),
(place_gallery.pic_canvas,
"Image",
"Arrow keys change image when it's in focus."),
(place_gallery.previous_img,
"Left Button",
"Click with mouse or when highlighted click with spacebar."),
(place_gallery.next_img,
"Right Button",
"Click with mouse or when highlighted click with spacebar."),
(source_gallery.thumbstrip,
"Thumbnail Views",
"Click thumbnail to display. Hover below to see "
"file name. If radio, click to make main image."),
(source_gallery.pic_canvas,
"Image",
"Arrow keys change image when it's in focus."),
(source_gallery.previous_img,
"Left Button",
"Click with mouse or when highlighted click with spacebar."),
(source_gallery.next_img,
"Right Button",
"Click with mouse or when highlighted click with spacebar.")
)
wdg.run_statusbar_tooltips(
visited,
self.parent.root_statusbar.status_label,
self.parent.root_statusbar.tooltip_label)
root.config(bg=formats['bg'])
self.canvas.bind_all(
'<MouseWheel>',
lambda evt, canvas=self.canvas: utes.scroll_on_mousewheel(
evt, canvas))
self.canvas.create_window(0, 0, anchor=tk.NW, window=self.content)
def show_top_pic(self, top_pic_button):
current_file_tup = files.get_current_file()
current_file = current_file_tup[0]
image_dir = current_file_tup[1]
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('''
SELECT images
FROM images_entities
JOIN person
ON images_entities.person_id = person.person_id
JOIN current
ON current.person_id = person.person_id
JOIN image
ON image.image_id = images_entities.image_id
WHERE images_entities.main_image = 1''')
top_pic = cur.fetchone()
cur.close()
conn.close()
if top_pic:
img_stg = ''.join(top_pic)
new_stg = '{}treebard_gps/data/{}/images/{}'.format(
st.root_drive, image_dir, img_stg)
top = Image.open(new_stg)
img1 = ImageTk.PhotoImage(top)
top_pic_button.config(image=img1)
top_pic_button.image = img1
self.tabbook_x = top.width
self.tabbook_y = top.height
def open_person_search_dialog(self):
person_search = Search(
self, # Main
root,
self.new_person_fill,
self.tabs.store['names'],
self.persons.findings_table.top_pic_button)
person_search.make_search_dialog()
def select_new_current_person(self, evt):
if len(self.new_person_fill.get()) != 0:
curr_person = self.new_person_fill.get()
curr_person = curr_person.split(' #')
subject_id = curr_person[1]
self.new_current_person = curr_person[0]
replace_findings_table(
self.persons,
self.persons.attributes_content,
self.new_person_fill,
self.persons.top_pic_button,
self,
self.tabs.store['person'],
subject_id)
class TopMenu(wdg.Frame):
def __init__(self, parent, *args, **kwargs):
wdg.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.make_menu()
def make_menu(self):
menubar = tk.Menu(self.parent)
menubar = menubar
menubar_items = {
'file': (
('New Tree',
lambda: self.parent.files.make_tree(root), 'Ctrl+N'),
('Open...',
lambda: self.parent.files.open_tree(root), 'Ctrl+O'),
('Save',
self.parent.main.persons.findings_table.save, 'Ctrl-S'),
('Save As...',
lambda: self.parent.files.save_as(root), 'Ctrl+Alt+S'),
('Save Copy As...',
self.parent.files.save_copy_as, 'Ctrl+Alt+C'),
('Rename...',
lambda: self.parent.files.rename_tree(root), ''),
('Close', placeholder, ''),
('Exit', root.quit, '')),
'edit': (
('Cut', placeholder, 'Ctrl+X'),
('Copy', placeholder, 'Ctrl+C'),
('Paste', placeholder, 'Ctrl+V'),
('Edit', placeholder, '')),
'entities': (
('Add Person, Place, or Source', placeholder, ''),
('Person', placeholder, ''),
('Place', placeholder, ''),
('Event', placeholder, ''),
('Attribute', placeholder, ''),
('Media', placeholder, '')),
'output': (
('Charts', placeholder, ''),
('Reports', placeholder, '')),
'research': (
('Do List', placeholder, ''),
('Research Goals', placeholder, ''),
('Contacts', placeholder, ''),
('Correspondence', placeholder, '')),
'utilities': (
('Your Profile', placeholder, ''),
('Appearance Settings', placeholder, ''),
('Relationship Calculator', placeholder, ''),
('Date Calculator', placeholder, ''),
('Duplicates & Matches Detector', placeholder, ''),
('U-R-Your-Own-Grampaw Detector', placeholder, ''),
('Sure Thing Calculator', placeholder, ''),
('Go Figure Planmaker', placeholder, '')),
'window': (
('Arrange All', placeholder, ''),
('Tile Horizontal', placeholder, ''),
('Tile Vertical', placeholder, ''),
('Close All But Home Window', placeholder, ''),
('Close Top Window', placeholder, '')),
'help': (('About Treebard', placeholder, ''),
("Users' Manual", placeholder, ''),
('Genealogy Tips & Tricks', placeholder, ''),
('Treebard Website', placeholder, ''),
('Treebard Forum', placeholder, ''),
('Help Contents, Search, & Index', placeholder, ''),
('Contact Treebard', placeholder, ''),
('Feature Requests', placeholder, ''),
('Source Code', placeholder, ''),
('Donations', placeholder, ''))}
for k,v in menubar_items.items():
menoo = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label=k.title(), menu=menoo)
for tup in v:
menoo.add_command(label=tup[0], command=tup[1], accelerator=tup[2])
# keystroke bindings for menu command accelerators
# display the menu
root.config(menu=menubar)
class PersonsTab(wdg.Frame):
def __init__(
self,
parent,
main,
root,
new_person_fill,
current_person_birthname,
*args, **kwargs):
wdg.Frame.__init__(self, parent, *args, **kwargs)
self.person_tab = parent
self.main = main
self.root = root
self.new_person_fill = new_person_fill
self.current_person_birthname = current_person_birthname
self.rc_menu = rcm.RightClickMenu(self.root)
self.make_widgets()
def make_widgets(self):
def position_attributes(evt=None):
if evt:
# recalculate notebook position if window is moved
self.right_panel.posx = self.right_panel.winfo_rootx()
self.right_panel.posy = self.right_panel.winfo_rooty()
a = self.right_panel.posx
b = self.main.tabbook_x
c = self.right_panel.notebook.winfo_reqwidth()
d = self.right_panel.posy
e = self.main.tabbook_y
f = self.right_panel.notebook.winfo_reqheight()
xx = a - b + c
yy = d - e + f
w = max(
self.right_panel.notebook.winfo_reqwidth(),
self.main.tabbook_x)
h = max(
self.right_panel.notebook.winfo_reqheight(),
self.main.tabbook_y)
x = min(xx, self.right_panel.posx)
y = min(yy, self.right_panel.posy)
self.right_panel.store['attributes'].config(
width=self.main.tabbook_x,
height=self.main.tabbook_y + 7)
self.att.geometry('{}x{}+{}+{}'.format(w, h, x, y))
def open_attributes(evt):
'''
Open mock-up of a notebook tab when attributes
tab is clicked. Can be resized to overlap the
pedigree area because it is a toplevel window
and not a real notebook tab (not a frame).
'''
position_attributes()
self.att.deiconify()
def close_attributes(evt):
self.att.withdraw()
def start_edge_sizer(evt):
self.init_geometry = self.att.geometry()
self.click_down_x = self.att.winfo_pointerx()
self.orig_pos_x = self.att.winfo_rootx()
self.orig_pos_y = self.att.winfo_rooty()
def stop_edge_sizer(evt):
self.click_up_x = self.click_down_x
new_pos_x = self.orig_pos_x
xy = self.init_geometry.split('+')
wh = xy.pop(0).split('x')
orig_wd = int(wh[0])
orig_ht = int(wh[1])
self.new_w = orig_wd
self.new_h = orig_ht
self.click_up_x = self.att.winfo_pointerx()
dx = self.click_down_x - self.click_up_x
self.new_w = orig_wd + dx
new_pos_x = self.orig_pos_x - dx
new_pos_y = self.orig_pos_y
if self.new_w < 10:
self.new_w = 10
self.att.geometry('{}x{}+{}+{}'.format(
self.new_w, self.new_h, new_pos_x, new_pos_y))
pedigree = wdg.Frame(self)
pedigree.grid(column=0, row=0, padx=12, pady=16)
# draggable canvas in frame in labelframe in notebook tab
self.pedigree_pan = wdg.Frame(pedigree)
self.pedigree_pan.grid(column=0, row=0)
self.pedigree_pan.rowconfigure(0, weight=1)
# # mockup/placeholder till the real one comes
self.make_pedigree_chart()
# right panel notebook toggles picture and attributes chart
panel_tabs = [('images', 'I'), ('attributes', 'A')]
minx=self.main.tabbook_x/self.master.winfo_screenwidth()
miny=self.main.tabbook_y/self.master.winfo_screenheight()
self.right_panel = wdg.TabBook(
self,
root=self.root,
tabwidth=11,
selected='images',
case='title',
side='se',
minx=minx,
miny=miny,
tabs=panel_tabs)
self.right_panel.grid(column=1, row=0, padx=24, pady=24)
self.right_panel.store['images'].grid_columnconfigure(0, weight=1)
self.right_panel.store['images'].grid_rowconfigure(0, weight=1)
self.top_pic_button = wdg.ButtonPlain(
self.right_panel.store['images'],
command=self.open_person_gallery)
self.main.show_top_pic(self.top_pic_button)
self.top_pic_button.grid(column=0, row=0, sticky='news', padx=3, pady=3)
# attributes tab is empty but when it opens,
# a dialog opens to cover it, allowing this
# apparent tab to overlap the pedigree chart
# and easily be resized since it's not a frame.
self.right_panel.store['attributes'].bind('<Expose>', open_attributes)
self.right_panel.store['images'].bind('<Expose>', close_attributes)
self.att = wdg.Toplevel(self.right_panel.store['attributes'])
self.att.withdraw()
self.att.wm_overrideredirect(True)
self.root.bind('<Configure>', position_attributes)
self.att.grid_columnconfigure(1, weight=1)
self.att.grid_rowconfigure(0, weight=1)
left_grip = wdg.FrameHilited1(
self.att,
width=6,
cursor='sb_h_double_arrow')
left_grip.grid(column=0, row=0, sticky='news')
left_grip.bind('<Button-1>', start_edge_sizer)
left_grip.bind('<B1-Motion>', stop_edge_sizer)
left_grip.bind('<ButtonRelease-1>', stop_edge_sizer)
vscroll = wdg.AutoScrollbar(
self.att,
orient='vertical')
hscroll = wdg.AutoScrollbar(
self.att,
orient='horizontal')
self.attributes_canvas = wdg.CanvasHilited(
self.att,
highlightthickness=0,
bd=0,
yscrollcommand=vscroll.set,
xscrollcommand=hscroll.set)
self.attributes_canvas.grid(column=1, row=0, sticky='news', pady=3, padx=3)
vscroll.config(command=self.attributes_canvas.yview)
hscroll.config(command=self.attributes_canvas.xview)
vscroll.grid(column=2, row=0, sticky='ns')
hscroll.grid(column=1, row=1, sticky='ew')
# create canvas contents
self.attributes_content = wdg.FrameHilited3(
self.attributes_canvas,
padx=3, pady=3)
self.attributes_content.grid_rowconfigure(1, weight=1)
self.attributes_content.grid_columnconfigure(1, weight=1)
# Creation of the attributes table and events table begins
# in the files module where make_findings_table()
# is called once for each table.
self.attributes_table = FindingsTable(
self.attributes_content,
self.new_person_fill,
self.top_pic_button,
self.main,
self.main.tabs.store['person'])
# opening position of scrollbars
self.attributes_canvas.create_window(
0, 0, anchor='nw', window=self.attributes_content)
self.attributes_content.update_idletasks()
self.attributes_canvas.config(
scrollregion=self.attributes_canvas.bbox("all"))
# findings table instances are gridded in make_header_row()
self.findings_table = FindingsTable(
self,
self.new_person_fill,
self.top_pic_button,
self.main,
self.person_tab)
# # - - - context-sensitive right-click menu - - - #
rcm_widgets = (self.top_pic_button,)
rcm.make_rc_menus(
rcm_widgets,
self.rc_menu,
ms.persons_tab_msg)
def make_pedigree_chart(self):
self.pedigree_chart = pc.PedigreeChart(
self.pedigree_pan,
self.current_person_birthname,
self.root,
width=950, height=300,
takefocus=1,
bd=1,
highlightthickness=1,
highlightbackground=formats['highlight_bg'],
bg=formats['bg'])
def open_person_gallery(self):
person_gallery_dlg = wdg.Toplevel()
person_gallery_dlg.grid_columnconfigure(0, weight=1)
person_gallery_dlg.grid_rowconfigure(0, weight=1)
person_gallery = gl.Gallery(
person_gallery_dlg, self.main.tabs, self.main.tabs.store['graphics'],
root, self.main.canvas)
person_gallery_statusbar = wdg.StatusbarTooltips(person_gallery_dlg)
person_gallery_statusbar.grid(column=0, row=2, sticky='ew')
wdg.run_statusbar_tooltips(
person_gallery.visited,
person_gallery_statusbar.status_label,
person_gallery_statusbar.tooltip_label)
ST.config_generic(person_gallery_dlg)
# ****************** SEARCH CLASSES ******************************
COL_HEADS = ('ID', 'Name', 'Birth', 'Death', 'Mother', 'Father')
NAMER_HEADS = ('Name Type', 'First Name', 'Middle Names', 'Surname', 'Suffix')
NAME_TYPES = (
'birth name', 'nickname', 'also known as', 'legally changed name',
'pseudonym', 'adoptive name', 'married name', 'alternate spelling',
'mis-spelling', 'call name', 'anglicized name', 'religious order name',
'stage name')
NAME_TYPE_IDS = (1, 12, 2, 10, 3, 5, 6, 8, 9, 13, 15, 17, 11)
NONPRINT_KEYS = (
'Return', 'Tab', 'Shift_L', 'Shift_R', 'Escape',
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8',
'F9', 'F10', 'F11', 'F12', 'Caps_Lock', 'Control_L',
'Control_R', 'Win_L', 'Win_R', 'Alt_R', 'Alt_L',
'App', 'space', 'Up', 'Down', 'Left', 'Right',
'Num_Lock', 'Home', 'Prior', 'End', 'Next', 'Insert',
'Pause')
new_tables = []
new_events_table = None
new_attributes_table = None
def replace_findings_table(
events_table_parent,
attributes_table_parent,
person_autofill,
top_pic_button,
main,
person_tab,
subject_id):
'''
Findings table is instantiated automatically on load.
In this function it's destroyed and replaced when changing
current person or other times such as redrawing the table
by running save(). Events table and attributes table are both
attributes of the same instance of the PersonsTab class but
gridded in different parents.
'''
global new_tables
global new_events_table
global new_attributes_table
if not subject_id:
return
if len(new_tables) > 0:
for table in new_tables:
table.destroy()
new_tables = []
events_table_parent.findings_table.destroy()
events_table_parent.attributes_table.destroy()
new_events_table = FindingsTable(
events_table_parent,
person_autofill,
top_pic_button,
main,
person_tab)
new_events_table.distinguish_evt_att(person_id=subject_id)
new_tables.append(new_events_table)
new_events_table.config(bg=formats['bg'])
new_events_table.grid(column=0, row=1, sticky='ew')
new_attributes_table = FindingsTable(
attributes_table_parent,
person_autofill,
top_pic_button,
main,
person_tab)
new_tables.append(new_attributes_table)
new_attributes_table.config(bg=formats['bg'])
new_attributes_table.grid(column=0, row=1, sticky='ew')
new_attributes_table.current_person_attributes = new_events_table.current_person_attributes
new_events_table.make_findings_table(
new_events_table.current_person_events)
new_attributes_table.make_findings_table(
new_attributes_table.current_person_attributes)
new_attributes_table.update_current_person(subject_id, new_attributes_table)
person_autofill.delete(0, 'end')
GENDER_TYPES = ('unknown (default if left blank)', 'female', 'male', 'other')
NAME_SUFFIXES = (
'jr.', 'sr.', 'jr', 'sr', 'junior', 'senior',
'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii',
'ix', 'x', 'xi', 'xii', 'xiii', 'xiv', 'xv')
def get_current_person():
current_file = files.get_current_file()[0]
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute(
'''
SELECT name.person_id, names
FROM name
JOIN person
ON person.person_id = name.person_id
JOIN current
ON name.person_id = current.person_id
''')
current_person = cur.fetchone()
if not current_person:
return
else:
current_person_id = current_person[0]
current_person = current_person[1]
return current_person_id, current_person
class NamesTab(wdg.Frame):
def __init__(self, parent, root, autofill, *args, **kwargs):
wdg.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.root = root
self.autofill = autofill
self.current_person = get_current_person()[0]
self.NAMES_TABLE_HEADS = (
'Name or ID Number', 'Name Type', 'Sort Order', 'Used By')
self.full_name = ''
self.order = []
self.new_or_current = tk.StringVar()
self.new_or_current.set('current')
self.make_widgets()
self.toggle_name_var()
def make_widgets(self):
self.announce = wdg.LabelH2(self)
self.names_table = wdg.Table(self, self.NAMES_TABLE_HEADS)
n = 0
for head in self.NAMES_TABLE_HEADS:
lab = wdg.LabelColumn(
self.names_table.mainframe, text=self.NAMES_TABLE_HEADS[n])
lab.grid(column=n, row=0, sticky='ew', ipadx=12)
n += 1
self.fill_names_table()
choose = wdg.FrameHilited(self)
self.choose1 = wdg.RadiobuttonHilited(
choose,
text='Add Name for Current Person',
command=self.toggle_name_var,
variable=self.new_or_current,
value='current')
self.choose2 = wdg.RadiobuttonHilited(
choose,
text='Create a New Person',
command=self.toggle_name_var,
variable=self.new_or_current,
value='new')
self.person_add = PersonAdd(
self, self.autofill, self.root)
self.announce.grid(
column=0, row=0, padx=6, pady=6, columnspan=5, sticky='w')
self.names_table.grid(
column=0, row=1, padx=24, pady=24, columnspan=2)
choose.grid(column=0, row=2, columnspan=2, padx=12, pady=12, sticky='w')
self.choose1.grid(column=0, row=0, padx=6, pady=6, sticky='w')
self.choose2.grid(column=1, row=0, padx=6, pady=6, sticky='w')
self.person_add.grid(column=0, row=3, columnspan=5)
def toggle_name_var(self):
'''
Radiobuttons toggle a boolean so the new name is
assigned to either a new person or the current person.
'''
if self.new_or_current.get() == 'new':
self.person_add.add_butt.config(
command=self.person_add.add_person)
elif self.new_or_current.get() == 'current':
self.person_add.add_butt.config(command=self.add_name)
def add_name(self):
self.current_person = get_current_person()[0]
self.person_add.get_entered_values()
self.person_add.make_sort_order()
self.person_add.save_new_name(self.current_person)
self.fill_names_table()
def fill_names_table(self):
'''
Add names data for current person to names table
each time a new name is made for current person
and each time current person is changed.
'''
self.current_person = get_current_person()[0]
self.clear_table()
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute(
'''
SELECT names, name_types, sort_order, used_by
FROM name
LEFT JOIN name_type
ON name.name_type_id = name_type.name_type_id
WHERE person_id = ?
''',
(self.current_person,))
current_names = cur.fetchall()
cur.close()
conn.close()
current_names = [list(i) for i in current_names]
for lst in current_names:
if not lst[3]:
lst[3] = ''
row = 2
for lst in current_names:
column=0
for stg in lst:
cell = wdg.EntryLabel(
self.names_table.mainframe,
self.use_input,
text=stg)
cell.grid(column=column, row=row, ipadx=12, ipady=6)
column += 1
row += 1
def clear_table(self):
'''
Prepare to update table.
'''
for child in self.names_table.mainframe.winfo_children():
if child.grid_info()['row'] > 1:
child.destroy()
# do not delete this seemingly unused def, unless you know exactly what you're doing:
def use_input(self):
pass
def reset(self):
self.new_or_current.set('current')
self.toggle_name_var()
self.person_add.reset()
class Search(wdg.Toplevel):
def __init__(
self,
main,
root,
person_autofill,
names_tab,
top_pic_button,
*args, **kwargs):
wdg.Toplevel.__init__(self, *args, **kwargs)
self.main = main
self.root = root
self.result_rows = []
self.hilit_row = None
self.unhilit_row = None
self.person_autofill = person_autofill
self.sent_text = ''
self.new_current_id = None
self.new_current_person = None
self.all_matches = []
self.tkvars = {}
self.sort_by = None
self.widget = None
self.nametip = None
self.x = self.y = 0
self.nametip_text = None
self.name_list = []
self.pointed_to = None
self.names_tab = names_tab
self.person_id = None
self.top_pic_button = top_pic_button
self.ma_id = None
self.pa_id = None
self.rc_menu = rcm.RightClickMenu(self.root)
def open_search(self, evt):
''' This recreates a results table when a character
is typed into or removed from the input entry at
top of search dialog.
'''
if evt.keysym in NONPRINT_KEYS:
return
elif evt.keysym.isalnum() is False:
return
for child in self.search_results.mainframe.winfo_children():
if child.grid_info()['row'] not in (0, 1):
child.destroy()
self.all_matches = get_matches(self.search_input)
self.make_search_dialog_cells()
def make_search_results_header_row(self):
for col in range(0, 6):
var = tk.StringVar()
self.tkvars[col] = var
lab = wdg.LabelColumn(
self.search_results.mainframe,
text=COL_HEADS[col],
cursor='hand2')
lab.grid(column=col, row=0, sticky='ew', ipadx=12)
lab.bind('<Button-1>', self.track_column_state)
def open_names_tab(self):
xfr = self.search_input.get()
self.destroy()
self.main.tabs.select(self.names_tab)
self.main.names.person_add.name_input.input.delete(0, 'end')
self.main.names.person_add.name_input.input.insert(0, xfr)
self.main.names.person_add.name_input.input.config(
font=formats['input_font'])
self.main.names.new_or_current.set('new')
self.main.names.toggle_name_var()
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute(
'''
SELECT gender
FROM person
JOIN name
ON person.person_id = name.person_id
WHERE names = ? AND name_type_id = 1
''',
(xfr,))
first_match = cur.fetchone()
cur.close()
conn.close()
if first_match:
first_match = first_match[0]
self.main.names.gender_input.input.config(state='normal')
self.main.names.gender_input.input.delete(0, 'end')
self.main.names.gender_input.input.insert(0, first_match)
self.main.names.gender_input.input.config(state='readonly')
self.main.names.gender = first_match
self.main.canvas.yview_moveto(0.0)
def make_search_dialog(self):
self.title('Person Search')
self.geometry('+1+1')
# if u do this u have to deal with rcm being not on top and unturnoffable:
# self.grab_set()
self.canvas = wdg.Canvas(self)
self.content = wdg.Frame(self.canvas)
self.protocol('WM_DELETE_WINDOW', self.close_search_dialog)
self.canvas.config(
bd=0,
highlightthickness=0)
self.canvas.grid(
column=0, row=0,
sticky='news')
self.content.grid_columnconfigure(0, weight=1)
self.content.grid_rowconfigure(0, weight=1)
self.vscrollbar = wdg.AutoScrollbar(self)
self.vscrollbar.grid(column=1, row=0, sticky='ns')
self.hscrollbar = wdg.AutoScrollbar(self, orient='horizontal')
self.hscrollbar.grid(column=0, row=1, sticky='ew')
self.canvas.config(
yscrollcommand=self.vscrollbar.set,
xscrollcommand=self.hscrollbar.set)
self.vscrollbar.config(command=self.canvas.yview)
self.hscrollbar.config(command=self.canvas.xview)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
header = wdg.Frame(self.content)
header.grid(column=0, row=0, sticky='ew')
self.search_dlg_heading = wdg.LabelH2(
header,
text='Person Search')
self.search_dlg_heading.grid(column=0, row=0, pady=(24,0))
instrux = wdg.Label(
header, text='Search for person by name(s) or id number:')
instrux.grid(column=0, row=1, sticky='e', padx=24, pady=12)
self.sent_text = self.person_autofill.get()
self.search_input = wdg.Entry(header)
self.search_input.grid(column=1, row=1, sticky='w', padx=12, pady=12)
self.search_input.insert(0, self.sent_text)
self.search_input.focus_set()
self.search_input.bind('<KeyRelease>', self.open_search)
self.person_adder = wdg.Button(
header,
text='Add New Person',
command=self.open_names_tab)
self.person_adder.grid(column=2, row=1, padx=12, pady=12)
self.search_results = wdg.Table(self.content, COL_HEADS)
self.search_results.grid(
column=0, row=1, sticky='news', padx=48, pady=48)
search_closer = wdg.Button(
self.content,
text='Cancel',
command=self.close_search_dialog)
search_closer.grid(column=2, row=2, padx=6, pady=6, sticky='e')
# STATUSTIPS
person_search_statusbar = wdg.StatusbarTooltips(self)
person_search_statusbar.grid(column=0, row=3, sticky='ew')
visited = (
(self.search_input.ent,
"Person Search Input",
"Type any part of any name or ID number; table will fill "
"with matches."),
(self.search_results.mainframe,
"Person Search Table",
"Select highlighted row with Enter or Space key to change "
"current person, or double-click any row."))
wdg.run_statusbar_tooltips(
visited,
person_search_statusbar.status_label,
person_search_statusbar.tooltip_label)
# right click context help doesn't work yet for person search dialog
# maybe because of the convoluted instaniation process which takes
# place in the Main class; this should be simplified so adding rcm
# to Search class works the same as adding rcm to any other class.
rcm_widgets = (self.search_input.ent, self.search_dlg_heading)
rcm.make_rc_menus(
rcm_widgets,
self.rc_menu,
ms.search_person_msg)
self.make_search_results_header_row()
self.canvas.create_window(0, 0, anchor='nw', window=self.content)
ST.config_generic(self)
self.resize_scrollbar()
self.resize_window()
def make_search_dialog_cells(self):
self.result_rows = []
c = 0
for person_row in self.all_matches:
self.make_row_list_for_search_results_table(person_row)
row_list = self.row_list
self.result_rows.append(row_list)
c += 1
for i in range(0, 6):
if i == 0:
init_sort = i
self.tkvars[init_sort].set('clicked_once')
else:
no_sort = i
self.tkvars[no_sort].set('not_clicked')
self.result_rows = sorted(
self.result_rows,
key=lambda q: q[init_sort])
row = 2
for lst in self.result_rows:
col = 0
for val in lst:
if col in (0,1,4,5):
text = lst[col]
lst[col] = val
elif col in (2,3):
text = val[1]
else:
break
lab = wdg.LabelSearch(
self.search_results.mainframe, cursor='hand2')
lab.grid(column=col, row=row, sticky='ew', ipadx=12)
lab.config(text=text)
if lab.grid_info()['row'] != 0:
if lab.grid_info()['column'] in (0, 1):
self.widget = lab
self.make_nametip()
col += 1
row += 1
for child in self.search_results.mainframe.winfo_children():
if child.grid_info()['row'] not in (0, 1):
child.bind(
'<Double-Button-1>',
self.select_searched_current_person)
child.bind('<Return>', self.select_searched_current_person)
child.bind('<Key-space>', self.select_searched_current_person)
child.bind('<FocusIn>', self.highlight_on_focus)
child.bind('<FocusOut>', self.unhighlight_on_unfocus)
child.bind('<Key-Up>', self.go_up)
child.bind('<Key-Down>', self.go_down)
if child.grid_info()['column'] == 0:
child.config(takefocus=1)
ST.config_generic(self)
self.resize_scrollbar()
self.resize_window()
self.search_input.focus_set()
def select_searched_current_person(self, evt):
if evt.widget.grid_info()['row'] == 0:
return
self.hilit_row = evt.widget.grid_info()['row']
for child in self.search_results.mainframe.winfo_children():
if child.grid_info()['row'] in (0, 1):
pass
elif (child.grid_info()['row'] == self.hilit_row and
child.grid_info()['column'] == 1):
self.new_current_person = child['text']
# handle the double-click event
if evt.type == '4':
if (evt.widget.grid_info()['column'] == 0 and
evt.widget.grid_info()['row'] == self.hilit_row):
self.new_current_id = evt.widget['text']
elif (evt.widget.grid_info()['column'] in (1,2,3,4,5) and
evt.widget.grid_info()['row'] == self.hilit_row):
for child in self.search_results.mainframe.winfo_children():
if (child.grid_info()['row'] == self.hilit_row and
child.grid_info()['column'] == 0):
self.new_current_id = child['text']
if evt.type != '4':
self.new_current_id = evt.widget['text']
self.new_current_person = names.get_name_with_id(self.new_current_id)
self.close_search_dialog()
replace_findings_table(
self.main.persons,
self.main.persons.attributes_content,
self.person_autofill,
self.top_pic_button,
self.main,
self.main.tabs.store['person'],
self.new_current_id)
self.off()
def close_search_dialog(self):
self.destroy()
def go_up(self, evt):
next = evt.widget.tk_focusPrev()
next.focus_set()
def go_down(self, evt):
prior = evt.widget.tk_focusNext()
prior.focus_set()
def highlight_on_focus(self, evt):
self.hilit_row = evt.widget.grid_info()['row']
for child in self.search_results.mainframe.winfo_children():
if child.grid_info()['row'] in (0, 1):
pass
elif child.grid_info()['row'] == self.hilit_row:
child.config(bg=formats['highlight_bg'])
def unhighlight_on_unfocus(self, evt):
self.unhilit_row = evt.widget.grid_info()['row']
for child in self.search_results.mainframe.winfo_children():
if child.grid_info()['row'] in (0, 1):
pass
elif child.grid_info()['row'] == self.unhilit_row:
child.config(bg=formats['bg'])
def resize_scrollbar(self):
self.update_idletasks()
self.canvas.config(scrollregion=self.canvas.bbox("all"))
def resize_window(self):
self.update_idletasks()
page_x = self.content.winfo_reqwidth()+24
page_y = self.content.winfo_reqheight()+48
self.geometry('{}x{}'.format(page_x, page_y))
def track_column_state(self, evt):
'''
Bound to column head labels.
Each column uses its own tkinter variable to track one of two
possible states: 1st click or no click. If on being clicked
the column was the last one clicked, its state is 'clicked_once'
so it sorts descending. Otherwise, the column's state is
'not_clicked' so it sorts ascending. On changing columns
the newly clicked column always sorts ascending. ID column
autosorts ascending on load.
'''
sortcol = evt.widget.grid_info()['column']
keycols = (0, 6, 10, 11, 7, 8)
a = 0
for value in keycols:
if a == sortcol:
self.sortkey = value
a += 1
self.sort_by = evt.widget.grid_info()['column']
ascending = sorted(self.result_rows, key=lambda f: f[self.sortkey])
descending = sorted(
self.result_rows, key=lambda f: f[self.sortkey], reverse=True)
if self.tkvars[self.sort_by].get() == 'not_clicked':
for k,v in self.tkvars.items():
if v.get() == 'clicked_once':
v.set('not_clicked')
self.tkvars[self.sort_by].set('clicked_once')
self.row_list = ascending
elif self.tkvars[self.sort_by].get() == 'clicked_once':
self.tkvars[self.sort_by].set('not_clicked')
self.row_list = descending
self.reorder_column()
def reorder_column(self):
''' Reconfigure labels in table. '''
cells = []
for child in self.search_results.mainframe.winfo_children():
if child.grid_info()['row'] > 1:
cells.append([child])
new_text = []
for row in self.row_list:
new_text.extend(
(row[0], row[1], row[2][1], row[3][1], row[4], row[5]))
a = 0
for lst in cells:
lst.append(new_text[a])
a += 1
for lst in cells:
lst[0].config(text=lst[1])
def make_row_list_for_search_results_table(self, unique_match):
'''
Gets a tuple (person_id, other_names) for one person at a time
from the Search class which has collected matches from a search
input. The unique person data is used to create a
row list which will be used to create
a sortable search results table with one person per row. The
other_names value is a list of names by results table row
which will be displayed in a nametip.
'''
self.row_list = []
self.found_person = unique_match[0]
self.row_list.append(self.found_person)
self.other_names = unique_match[1]
self.display_name = names.get_name_with_id(self.found_person)
self.row_list.append(self.display_name)
ext = [[], [], '', '', '', '', '', []]
self.row_list.extend(ext[0:])
# this has to run last
self.get_values_of_searched()
def get_values_of_searched(self):
''' '''
self.get_death()
self.get_birth()
if self.other_names:
self.get_other_names()
else:
self.row_list[9] = ''
self.make_sorters()
def make_sorters(self):
'''
self.ma_id & self.pa_id don't exist yet at this point and
if there is no birth/offspring event they won't.
'''
self.row_list[6] = self.get_sort_names(
self.row_list[0])
self.row_list[10] = self.make_sorter_for_formatted_dates(
self.row_list, 2)
self.row_list[11] = self.make_sorter_for_formatted_dates(
self.row_list, 3)
def make_sorter_for_formatted_dates(self, row, date_index):
'''
Add a sortable date to the search results row lists, for birth
column or death column, whichever was clicked. Prefix is
stripped out. The
iso date string is converted to a list of integers [y,m,d]
which extends the existing date key's value for use to sort
the table by the date column; BC year integers are made
negative. CHANGE THIS SO IT MAKES SORTERS FOR ONE ROW AT A TIME
'''
sorter = row[date_index][0]
for pfx in dates.INPUT_PFX:
if sorter.startswith(pfx) is True:
sorter = sorter.lstrip(pfx)
if sorter.startswith('?') is True:
sorter = sorter.replace('?-to-', '')
int_sort = sorter.split('-')
nums = []
for item in int_sort:
if len(item) == 4:
idx = int_sort.index(item)
nums.append(item)
break
nums.extend([int_sort[idx+1], int_sort[idx+2], int_sort[idx+3]])
ints = []
if nums[3] == 'bc':
ints.append(-int(nums[0]))
ints.extend([int(nums[1]), int(nums[2])])
else:
del nums[3]
for num in nums:
ints.append(int(num))
sorter = ints
row.append(sorter)
return sorter
def get_sort_names(self, subject_id):
''' '''
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('''
SELECT
sort_order
FROM name
JOIN person
ON person.person_id = name.person_id
WHERE name.person_id = ?
AND name_type_id = 1
''',
(subject_id,))
sort_name = cur.fetchone()
cur.close()
conn.close()
# only looking for birth names and there might not be one
if sort_name:
sort_name = sort_name[0].lower()
elif not sort_name:
sort_name = ''
return sort_name
def get_death(self):
''' '''
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('''
SELECT
date
FROM person
JOIN finding
ON finding.person_id = person.person_id
JOIN event_type
ON finding.event_type_id = event_type.event_type_id
WHERE finding.person_id = ?
AND finding.event_type_id == 4 ''',
(self.found_person,))
death_date = cur.fetchone()
cur.close()
conn.close()
self.death_date = ['-0000-00-00-', '']
if not death_date:
self.row_list[3] = self.death_date
return
iso_date = death_date[0]
self.death_date = [iso_date, dates.format_misc_date(iso_date)]
self.row_list[3] = self.death_date
def get_birth(self):
''' '''
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('''
SELECT
finding_id,
date
FROM person
JOIN finding
ON finding.person_id = person.person_id
JOIN event_type
ON finding.event_type_id = event_type.event_type_id
WHERE finding.person_id = ?
AND finding.event_type_id == 1 ''',
(self.found_person,))
birth_date = cur.fetchone()
cur.close()
conn.close()
self.birth_date = ['-0000-00-00-', '']
if not birth_date:
self.row_list[2] = self.birth_date
return
iso_date = birth_date[1]
self.birth_date = [iso_date, dates.format_misc_date(iso_date)]
self.row_list[2] = self.birth_date
self.offspring_event = birth_date[0]
self.get_ma()
self.get_pa()
self.row_list[7] = self.get_sort_names(self.ma_id)
self.row_list[8] = self.get_sort_names(self.pa_id)
def get_ma(self):
''' '''
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('''
SELECT
name.person_id,
names
FROM person
LEFT JOIN findings_persons
ON person.person_id = findings_persons.person_id
LEFT JOIN kin_type
ON findings_persons.kin_type_id = kin_type.kin_type_id
LEFT JOIN name
ON person.person_id = name.person_id
WHERE findings_persons.kin_type_id = 1
AND name_type_id = 1
AND findings_persons.finding_id = ? ''',
(self.offspring_event,))
self.mother = cur.fetchall()
cur.close()
conn.close()
if len(self.mother) != 0:
self.mother = self.mother[0]
ma = None
self.ma_id = self.mother[0]
ma = names.get_name_with_id(self.ma_id)
self.row_list[4] = ma
def get_pa(self):
''' '''
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('''
SELECT
name.person_id,
names
FROM person
LEFT JOIN findings_persons
ON person.person_id = findings_persons.person_id
LEFT JOIN kin_type
ON findings_persons.kin_type_id = kin_type.kin_type_id
LEFT JOIN name
ON person.person_id = name.person_id
WHERE findings_persons.kin_type_id = 2
AND name_type_id = 1
AND findings_persons.finding_id = ? ''',
(self.offspring_event,))
self.father = cur.fetchall()
cur.close()
conn.close()
if len(self.father) != 0:
pa = None
self.pa_id = self.father[0][0]
pa = names.get_name_with_id(self.pa_id)
self.row_list[5] = pa
def get_other_names(self):
'''
For nametips.
'''
tip_names = []
for lst in self.other_names:
name = lst[0]
name_type = lst[1]
tip_names.append([name_type, name])
name_tips = []
for lst in tip_names:
sub = ': '.join(lst)
name_tips.append(sub)
name_tips = '\n'.join(name_tips)
for lst in self.other_names:
name = lst[0]
name_type = lst[1]
name_kv = '{}: {}'.format(name_type, name)
if lst[2]:
used_by = lst[2]
else:
used_by = 'unknown'
usedby_kv = 'name used by: {}'.format(used_by)
self.row_list[9] = name_tips
# NAMETIPS
def show_nametip(self):
''' Some rows in the search results table have no name
because the displayed findings only show birth names. The
nametips will point out that there may be no birth name
stored for the person. Or the user might type "Daisy" and
get "Alice". The nametip will show that Alice's nickname is
Daisy.
'''
maxvert = self.winfo_screenheight()
if self.nametip or not self.nametip_text:
return
x, y, cx, cy = self.widget.bbox('insert')
self.nametip = d_tip = wdg.Toplevel(self.widget)
label = wdg.LabelNegative(
d_tip,
text=self.nametip_text,
justify='left',
relief='solid',
bd=1,
bg=formats['highlight_bg'])
label.pack(ipadx=6, ipady=3)
mouse_at = self.winfo_pointerxy()
tip_shift = 48
if mouse_at[1] < maxvert - tip_shift * 2:
x = mouse_at[0] + tip_shift
y = mouse_at[1] + tip_shift
else:
x = mouse_at[0] + tip_shift
y = mouse_at[1] - tip_shift
d_tip.wm_overrideredirect(1)
d_tip.wm_geometry('+{}+{}'.format(x, y))
def off(self):
d_tip = self.nametip
self.nametip = None
if d_tip:
d_tip.destroy()
def make_nametip(self):
''' Runs once on widget construction. '''
self.widget.bind('<Enter>', self.handle_enter)
self.widget.bind('<Leave>', self.on_leave)
def handle_enter(self, evt):
'''
Get person id from text in column 0 of pointed row. Find that
person id as row[id] in search results dicts. In that dict get
row[other names].
'''
self.pointed_to = evt.widget
pointed_row = self.pointed_to.grid_info()['row']
for child in self.search_results.mainframe.winfo_children():
if child.grid_info()['row'] in (0, 1):
pass
elif (child.grid_info()['column'] == 0 and
child.grid_info()['row'] == pointed_row):
self.person_id = child['text']
for row in self.result_rows:
if row[0] == self.person_id:
pointed_dict = row
self.nametip_text = pointed_dict[9]
if self.nametip_text:
self.show_nametip()
def on_leave(self, evt):
self.other_names = []
self.off()
def get_matches(search_input):
got = search_input.get()
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute(
'''
SELECT DISTINCT
person.person_id
FROM person
JOIN name
ON person.person_id = name.person_id
WHERE names LIKE ?
OR person.person_id LIKE ? ''',
('%'+got+'%', '%'+got+'%'))
all_matches = cur.fetchall()
all_matches = [list(i) for i in all_matches]
for lst in all_matches:
person_id = lst[0]
cur.execute('''
SELECT names, name_types, used_by
FROM name
LEFT JOIN name_type
ON name.name_type_id = name_type.name_type_id
WHERE person_id = ?''',
(person_id,))
other_names = cur.fetchall()
other_names = [list(tup) for tup in other_names]
if other_names:
lst.append(other_names)
elif not other_names:
lst.append('')
cur.close()
conn.close()
return all_matches
def get_values(current_person):
'''
Value-getting functions are nested in this function
in order that the whole findings table can be built with one
database connection. The list FindingsTable.findings is a class
variable instead of an instance variable because both
instances of the FindingsTable class (i.e. the events
table and the attributes table) are made from the same list.
Both tables exist at the same time and are replaced/updated
at the same time. Return values are used in a chain from
query function to query function since the variable
has to be updated several times by several separate queries
during each update process, and if the update functions
are run in the wrong order, the results will be wrong. So
using return values to call the query functions exactly once in
exactly the right place ensures that the right query function
will be run at the right time. Also the class variable can
be referenced outside the FindingsTable class which is handy
and allows this large collection of nested functions,
get_values(), to reside outside the class also.
'''
def get_finding_ids():
''' Get ids for each row in findings table and start dict rows. '''
FindingsTable.findings = []
cur.execute('''
SELECT finding_id
FROM finding
WHERE person_id = ? ''',
(current_person,))
ids1 = cur.fetchall()
cur.execute('''
SELECT finding_id
FROM findings_persons
WHERE person_id = ? ''',
(current_person,))
ids2 = cur.fetchall()
all_ids = ids1 + ids2
all_ids = [i[0] for i in all_ids]
for id in all_ids:
finding_row = [
id, [], [], [], [], [], [''], [{}], [[]], 0, [0,0,0]]
FindingsTable.findings.append(finding_row)
return FindingsTable.findings
def get_generic_events():
'''
The value 'unknown' for place fields is used instead of
nulls in the database where there is no known value for
place. This allows use of a default value of 1 for the
place_id fk in the database finding table. This is the
id for the place 'unknown'. The code that builds the
findings table in the GUI doesn't like nulls and maybe at
the time I decided it would be better to store 'unknown'
for place no. 1 instead of an empty string in case I forgot
why I did it. I tried making it number 999999 assuming that
many places would never be needed, but then the next place
I added was auto-numbered 1000000 by sqlite which was annoying
so I used place_id 1 instead.
'''
FindingsTable.findings = get_finding_ids()
cur.execute('''
SELECT
finding_id,
event_types,
date,
places,
particulars,
age
FROM finding
JOIN event_type
ON event_type.event_type_id = finding.event_type_id
JOIN place
ON place.place_id = finding.place_id
WHERE person_id = ?
AND finding.event_type_id != 1''',
(current_person,))
generic_events = cur.fetchall()
for tup in generic_events:
for row in FindingsTable.findings:
if tup[0] == row[0]:
lists = [[i] for i in tup[1:]]
row[1:6] = lists
return FindingsTable.findings
def get_birth_events():
''' Birth events in which current person is the child. '''
FindingsTable.findings = get_generic_events()
cur.execute('''
SELECT finding_id, date, places, particulars
FROM finding
JOIN event_type
ON finding.event_type_id = event_type.event_type_id
JOIN place
ON finding.place_id = place.place_id
WHERE person_id = ?
AND finding.event_type_id = 1''',
(current_person,))
birth_event = cur.fetchone()
if birth_event:
birth_event = list(birth_event)
birth_event_id = birth_event[0]
cur.execute('''
SELECT findings_persons.person_id
FROM findings_persons
JOIN finding
ON findings_persons.finding_id = finding.finding_id
WHERE findings_persons.finding_id = ?
AND findings_persons.person_id != ?''',
(birth_event_id, current_person))
parents = cur.fetchall()
if len(parents) == 0:
pa_ma = []
else:
pa_ma = []
for id in parents:
id = id[0]
name = names.get_name_with_id(id)
pa_ma.append([name, id])
for row in FindingsTable.findings:
if row[0] == birth_event[0]:
birth_event.insert(1, 'birth')
if birth_event[3] == 'unknown':
birth_event[3] = ''
birth_event.append('0') # age at birth
lists = [[i] for i in birth_event[1:6]]
fixed = []
for lst in pa_ma:
lst.append(None)
fixed.append(lst)
if len(fixed) > 0:
lists.append(fixed)
else:
lists.append([[]])
row[1:7] = lists
return FindingsTable.findings
def get_offspring_events():
''' Birth events in which current person is a parent. '''
FindingsTable.findings = get_birth_events()
cur.execute('''
SELECT
findings_persons.finding_id,
date,
places,
particulars,
findings_persons.age,
finding.person_id
FROM finding
JOIN findings_persons
ON finding.finding_id = findings_persons.finding_id
JOIN place
ON finding.place_id = place.place_id
WHERE kin_type_id in (1, 2)
AND findings_persons.person_id = ?''',
(current_person,))
children = cur.fetchall()
children = [list(i) for i in children]
if len(children) != 0:
kidvars = []
for lst in children:
id = lst[5]
name = names.get_name_with_id(id)
kidvar = [[name, id, None]]
kidvars.append(kidvar)
for row in FindingsTable.findings:
for lst in children:
if row[0] == lst[0]:
lst.insert(1, 'offspring')
if lst[3] == 'unknown':
lst[3] = ''
lists = [[i] for i in lst[1:6]]
for item in kidvars:
if item[0][1] == lst[6]:
lists.append(item)
row[1:7] = lists
return FindingsTable.findings
def get_couple_events():
''' '''
FindingsTable.findings = get_offspring_events()
cur.execute('''
SELECT
finding.finding_id,
event_types,
date,
places,
particulars,
findings_persons.age
FROM finding
LEFT JOIN findings_persons
ON finding.finding_id = findings_persons.finding_id
LEFT JOIN event_type
ON finding.event_type_id = event_type.event_type_id
LEFT JOIN person
ON findings_persons.person_id = person.person_id
LEFT JOIN place
ON finding.place_id = place.place_id
WHERE findings_persons.person_id = ?
AND findings_persons.kin_type_id = 9''',
(current_person,))
couple_events = cur.fetchall()
couple_events = [list(i) for i in couple_events]
for lst in couple_events:
name = ''
finding_id = lst[0]
cur.execute('''
SELECT person_id
FROM findings_persons
WHERE finding_id = ?
AND person_id != ? ''',
(finding_id, current_person))
spouse_id = cur.fetchone()
if spouse_id:
id = spouse_id[0]
name = names.get_name_with_id(id)
lst.append([name, id])
else:
lst.append([''])
for row in FindingsTable.findings:
for lst in couple_events:
if lst[0] == row[0]:
fix_me = lst[6]
fix_me.append(None)
if lst[3] == 'unknown':
lst[3] = ''
lists = [[i] for i in lst[1:6]]
lists.append([fix_me])
row[1:7] = lists
return FindingsTable.findings
def get_generic_event_roles():
'''
Example: employer in an Occupation event is generic role.
Flower girl in a Wedding event is couple-event role because
it will be linked to both spouses.
'''
cur.execute('''
SELECT
finding.finding_id,
findings_roles.role_type_id,
findings_roles.person_id
FROM
finding
JOIN findings_roles
ON findings_roles.finding_id = finding.finding_id
JOIN person
ON person.person_id = findings_roles.person_id
JOIN role_type
ON role_type.role_type_id = findings_roles.role_type_id
WHERE finding.person_id = ?''',
(current_person,))
generic_event_roles = cur.fetchall()
generic_event_roles = [list(i) for i in generic_event_roles]
return generic_event_roles
def get_couple_event_ids():
'''
For use in getting couple event roles and
couple event notes.
'''
cur.execute('''
SELECT finding_id
FROM findings_persons
WHERE person_id = ?''',
(current_person,))
couple_event_ids = cur.fetchall()
couple_event_ids = [i[0] for i in couple_event_ids]
qmarks = len(couple_event_ids)
return couple_event_ids, qmarks
def get_couple_event_roles():
couple_event_ids = get_couple_event_ids()
cur.execute('''
SELECT
findings_roles.finding_id,
findings_roles.role_type_id,
findings_roles.person_id
FROM
finding
JOIN findings_roles
ON findings_roles.finding_id = finding.finding_id
JOIN person
ON person.person_id = findings_roles.person_id
JOIN role_type
ON role_type.role_type_id = findings_roles.role_type_id
WHERE findings_roles.finding_id IN ({})
'''.format(','.join('?' * couple_event_ids[1])),
couple_event_ids[0])
couple_event_roles = cur.fetchall()
couple_event_roles = [list(i) for i in couple_event_roles]
return couple_event_roles
def make_roles_from_array():
'''
Gets all roles for current person's finding rows. When
dots are clicked in findings table roles column, the dialog
opens showing the right roles for that person and that role.
'''
FindingsTable.findings = get_couple_events()
generic_event_roles = get_generic_event_roles()
couple_event_roles = get_couple_event_roles()
all_roles = generic_event_roles + couple_event_roles
unique_ids = []
for lst in all_roles:
if lst[0] not in unique_ids:
unique_ids.append(lst[0])
all_roles_by_finding = []
for lst in all_roles:
for id in unique_ids:
roles_per_finding = []
if lst[0] == id:
roles_per_finding.append(lst)
for item in roles_per_finding:
if len(item) > 0:
all_roles_by_finding.append(item)
final = []
for i in range(len(unique_ids)):
final.append([])
t = 0
for id in unique_ids:
for lst in all_roles_by_finding:
if lst[0] == id:
final[t].append(lst)
t += 1
for row in FindingsTable.findings:
for lst in final:
if row[0] == lst[0][0]:
row[7] = [lst]
return FindingsTable.findings
def get_notes():
FindingsTable.findings = make_roles_from_array()
couple_event_ids = get_couple_event_ids()
cur.execute('''
SELECT
finding.finding_id,
notes
FROM
finding
JOIN findings_notes
ON finding.finding_id = findings_notes.finding_id
JOIN note
ON findings_notes.note_id = note.note_id
JOIN person
ON finding.person_id = person.person_id
WHERE finding.person_id = ?''',
(current_person,))
generic_event_notes = cur.fetchall()
generic_event_notes = [list(i) for i in generic_event_notes]
cur.execute('''
SELECT
findings_notes.finding_id,
notes
FROM
finding
JOIN findings_notes
ON finding.finding_id = findings_notes.finding_id
JOIN note
ON findings_notes.note_id = note.note_id
WHERE findings_notes.finding_id IN ({})
'''.format(','.join('?' * couple_event_ids[1])),
couple_event_ids[0])
couple_event_notes = cur.fetchall()
couple_event_notes = [list(i) for i in couple_event_notes]
all_notes = generic_event_notes + couple_event_notes
for row in FindingsTable.findings:
notes = [[]]
for lst in all_notes:
if lst[0] == row[0]:
notes[0].append(lst[1])
row[8] = notes
return FindingsTable.findings
def get_source_count():
FindingsTable.findings = get_notes()
source_count = []
for tup in FindingsTable.findings:
id = tup[0]
cur.execute(
''' SELECT COUNT(finding_id)
FROM claims_findings
WHERE finding_id = ?''',
(id,))
src_count = cur.fetchone()
source_count.append((id, src_count[0]))
for tup in source_count:
for row in FindingsTable.findings:
if tup[0] == row[0]:
row[9] = tup[1]
return FindingsTable.findings
def make_formatted_dates_sortable():
''' Add a sortable date key to the findings row dicts:
prefix is stripped out; date string is converted to list of integers
which is stored as the "sortable_date" key's value; BC year
integers are made negative. '''
FindingsTable.findings = get_source_count()
for row in FindingsTable.findings:
sorter = row[2][0]
for pfx in dates.INPUT_PFX:
if sorter.startswith(pfx) is True:
sorter = sorter.lstrip(pfx)
if sorter.startswith('?') is True:
sorter = sorter.replace('?-to-', '')
int_sort = sorter.split('-')
nums = []
for item in int_sort:
if len(item) == 4:
idx = int_sort.index(item)
nums.append(item)
break
nums.extend([int_sort[idx+1], int_sort[idx+2], int_sort[idx+3]])
ints = []
if nums[3] == 'bc':
ints.append(-int(nums[0]))
ints.extend([int(nums[1]), int(nums[2])])
else:
del nums[3]
for num in nums:
ints.append(int(num))
sorter = ints
row[10] = sorter
return FindingsTable.findings
conn = sqlite3.connect(current_file)
cur = conn.cursor()
FindingsTable.findings = make_formatted_dates_sortable()
cur.close()
conn.close()
return FindingsTable.findings
FINDING_TABLE_HEADS = (
'Event', 'Date', 'Place', 'Particulars',
'Age', 'Kin', 'Roles', 'Notes', 'Sources')
listx = list(FINDING_TABLE_HEADS[0:5])
FINDING_TABLE_TEXT_COLS = tuple(listx)
FINDING_TABLE_DOT_COLS = FINDING_TABLE_HEADS[6:8]
# replace with query from db so user can add his own couple events:
couple_event_types = ['marriage', 'divorce', 'honeymoon', 'annulment', 'marriage banns', 'wedding', 'separation', 'wedding anniversary celebration']
class FindingsTable(wdg.Table):
'''
Make events and attributes tables on load or on
changing current person or executing Save command.
'''
findings = []
def __init__(
self,
master,
person_autofill,
top_pic_button,
main,
person_tab,
heads=FINDING_TABLE_HEADS,
**options):
wdg.Table.__init__(self, master, heads, **options)
self.master = master # PersonsTab instance
self.person_autofill = person_autofill
self.top_pic_button = top_pic_button
self.main = main
self.person_tab = person_tab # person tab of main notebook
self.master.grid_rowconfigure(1, weight=0)
self.finding_id = None
self.string_input = ''
self.new_row = 0
self.dlg_type = ''
self.role_ids = []
self.note_string = []
self.col_header = ''
self.cell_valuer = ''
self.kin_id = None
self.subject_birthname = ''
self.source_count = 0
self.from_db = True
self.generic_event_roles = []
self.couple_event_roles = []
self.table_cells_parent = self.mainframe
self.current_person_events = []
self.current_person_attributes = []
self.valdor = dates.ValidDate()
self.bind_all('<Control-S>', self.save)
self.bind_all('<Control-s>', self.save)
def distinguish_evt_att(self, person_id=None):
'''
Runs many queries to get all findings
for the current person. Separates them into two lists
depending on whether or not the finding is dated.
Also called in methods of the Files class.
'''
if person_id:
current_person = person_id
else:
current_person = get_current_person()[0]
FindingsTable.findings = get_values(current_person)
self.subject_birthname = names.get_name_with_id(current_person)
for row in FindingsTable.findings:
if (row[1][0] in ('birth', 'death', 'burial') or
row[2][0] != '-0000-00-00-'):
self.current_person_events.append(row)
else:
self.current_person_attributes.append(row)
def make_findings_table(self, evt_or_att):
'''
This is called in methods of the Files class.
'''
def make_header_row():
self.grid(column=0, row=2, sticky='ew', columnspan=2)
g = 0
for name in FINDING_TABLE_HEADS:
lab = wdg.LabelColumn(
self.table_cells_parent,
text=name)
lab.grid(column=g, row=0, padx=0, sticky='w')
g += 1
if evt_or_att is self.current_person_events:
self.order_event_rows()
make_header_row()
self.make_table_cells(parent=self.table_cells_parent)
self.make_event_selector()
ST.config_generic(self.person_tab)
elif evt_or_att is self.current_person_attributes:
self.order_attribute_rows()
make_header_row()
self.make_table_cells(parent=self.table_cells_parent)
ST.config_generic(self.main.persons.att)
self.resize_scrollbar()
self.resize_window()
self.main.persons.right_panel.make_active() # below config_generic()
self.valdor.value_from_db = None
self.fix_tab_traversal()
self.display_curr_per()
def resize_scrollbar(self):
self.update_idletasks()
# self.main.persons is PersonsTab instance
self.main.persons.attributes_canvas.config(
scrollregion=self.main.persons.attributes_canvas.bbox("all"))
def resize_window(self):
self.update_idletasks()
page_x = self.main.persons.attributes_content.winfo_reqwidth()
page_y = self.main.persons.attributes_content.winfo_reqheight()
self.main.persons.att.geometry('{}x{}'.format(page_x, page_y))
def update_current_person(self, new_current_id, findings_table):
findings_table.person_autofill.delete(0, 'end')
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(
'UPDATE current SET person_id = ? WHERE current_id = 1',
(new_current_id,))
conn.commit()
cur.close()
conn.close()
self.main.show_top_pic(self.top_pic_button)
findings_table.display_curr_per()
self.main.canvas.yview_moveto(0.0)
def display_curr_per(self):
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute(
'''
SELECT current.person_id
FROM person JOIN current
ON person.person_id = current.person_id
WHERE current_id = 1
''')
curr_id = cur.fetchone()
if curr_id:
curr_id = curr_id[0]
else:
return
cur.execute(
'''
SELECT gender FROM person WHERE person_id = ?
''',
(curr_id,))
curr_gen = cur.fetchone()[0]
cur.close()
conn.close()
self.subject_birthname = names.get_name_with_id(curr_id)
self.main.curr_per_displ.config(
text='{} ({})'.format(self.subject_birthname, curr_id))
self.main.names.announce.config(
text='Names for {}'.format(self.subject_birthname))
self.main.names.person_add.gender_input.input.delete(0, 'end')
self.main.names.person_add.gender_input.input.insert(0, curr_gen)
self.main.names.fill_names_table()
self.main.master.resize_scrollbar()
self.main.master.resize_window()
def order_event_rows(self):
FIXED_ORDER_EVENTS = {'birth' : 0, 'death' : 1, 'burial' : 2}
fixed_rows = []
sortable_rows = []
for row in self.current_person_events:
if row[1][0] == 'birth':
fixed_rows.append(row)
elif row[1][0] == 'death':
fixed_rows.append(row)
elif row[1][0] == 'burial':
fixed_rows.append(row)
else:
sortable_rows.append(row)
fixed_rows.sort(key=lambda j: FIXED_ORDER_EVENTS[j[1][0]])
sortable_rows.sort(key = lambda i: i[10])
p = 1
for dikt in sortable_rows:
fixed_rows.insert(p, dikt)
p += 1
self.ordered_rows = fixed_rows
def order_attribute_rows(self):
sortable_rows = []
for row in self.current_person_attributes:
if row[1][0] in ('birth', 'death', 'burial'):
pass
elif row[2][0] != '-0000-00-00-':
pass
else:
sortable_rows.append(row)
sortable_rows.sort(key = lambda i: i[1])
self.ordered_rows = sortable_rows
def make_role_dots(self, parent, dlg_header, holder=[[]]):
dots = wdg.LabelDots(
parent,
RolesDialog,
self.subject_birthname,
dlg_header,
self.finding_id,
self.role_ids,
root=self.main.root,
holder=[[]])
if len(self.role_ids) != 0:
dots.config(text=' ... ')
else:
dots.config(text=' ')
dots.grid(
column=6, row=self.new_row,
sticky='w', pady=3, padx=6)
def make_note_dots(self, parent, dlg_header, holder=[[]]):
dots = wdg.LabelDots(
parent,
notes.NotesDialog,
self.subject_birthname,
dlg_header,
self.finding_id,
self.note_string,
holder=[[]])
if len(self.note_string) != 0:
dots.config(text=' ... ')
else:
dots.config(text=' ')
dots.grid(
column=7, row=self.new_row,
sticky='w', pady=3, padx=6)
def make_kin_label(self):
population = len(self.coupler.winfo_children())
cell = wdg.LabelGoTo(
self.coupler,
table=self,
change_person=replace_findings_table,
subject_id=self.kin_id)
if self.from_db is True:
self.insert_values_from_db(cell)
g = 0
for col_head in FINDING_TABLE_TEXT_COLS:
if self.col_header == col_head:
cell.grid(
column=g,
row=population,
sticky='ew', pady=(3,0), padx=6)
if population == 1:
cell.grid_configure(pady=0)
g += 1
def make_table_cells(self, parent):
'''
Each row list has a [cell value] to which a widget
reference will be appended.
'''
col_num = 1
for heading in FINDING_TABLE_TEXT_COLS:
self.new_row = 2
for table_row in self.ordered_rows:
self.col_header = heading
evt_type = table_row[1][0]
self.cell_valuer = table_row[col_num][0]
self.make_label(parent, holder=table_row[col_num])
self.from_db = True
self.new_row += 1
col_num += 1
self.new_row = 2
for table_row in self.ordered_rows:
kinvars = table_row[6]
if len(kinvars[0]) == 0:
self.new_row += 1
continue
self.coupler = wdg.Frame(self.mainframe)
self.coupler.grid(
column=5, row=self.new_row, sticky='ew')
self.col_header = 'Kin'
p = 0
for lst in kinvars:
self.cell_valuer = lst[0]
self.kin_id = lst[1]
self.make_kin_label()
self.from_db = True
p += 1
self.new_row += 1
for dlg_type in FINDING_TABLE_DOT_COLS:
self.new_row = 2
for table_row in self.ordered_rows:
self.finding_id = table_row[0]
date = dates.format_misc_date(table_row[2][0])
self.dlg_header = (
table_row[1][0], date, table_row[3][0], table_row[4][0])
self.dlg_type = dlg_type
if dlg_type == 'Roles':
self.role_ids = table_row[7][0]
self.make_role_dots(
parent, self.dlg_header, holder=table_row[7])
elif dlg_type == 'Notes':
self.note_string = table_row[8][0]
self.make_note_dots(
parent, self.dlg_header, holder=table_row[8])
self.new_row += 1
self.new_row = 2
for table_row in self.ordered_rows:
self.source_count = table_row[9]
self.make_source_button(parent)
self.new_row += 1
def make_source_button(self, parent):
butt = wdg.LabelButtonText(parent)
butt.config(text=self.source_count)
butt.grid(column=8, row=self.new_row, padx=6, pady=3, sticky='w')
def make_label(self, parent, holder=[]):
'''
holder is [cell's display value]
'''
cell = wdg.EntryLabel(
parent,
self.use_input,
table=self,
list_list=FindingsTable.findings,
change_person=replace_findings_table,
subject_id=self.kin_id)
holder.append(cell)
if self.from_db is True:
self.insert_values_from_db(cell)
g = 0
for col_head in FINDING_TABLE_TEXT_COLS:
if self.col_header == col_head:
if (g != 5 or (g == 5 and len(holder[0]) == 0) or
parent.winfo_name() != 'coupler'):
cell.grid(
column=g,
row=self.new_row,
sticky='ew', pady=3, padx=6)
elif g == 5:
cell.grid(
column=g,
row=row,
sticky='ew', pady=3, padx=6)
g += 1
self.kin_id = None
def insert_values_from_db(self, cell):
if self.col_header == 'Date':
if (
'-and-' not in self.cell_valuer and
'-to-' not in self.cell_valuer):
formatted_date = self.valdor.format_date_for_table(
self.cell_valuer, widg=cell)
cell.config(text=formatted_date)
elif '-and-' in self.cell_valuer:
self.valdor.and_to = ' and '
elif '-to-' in self.cell_valuer:
self.valdor.and_to = ' to '
if (self.valdor.and_to is not None and
len(self.valdor.and_to) != 0):
formatted_date = self.valdor.select_function(
self.cell_valuer, widg=cell)
cell.config(text=formatted_date)
else:
cell.config(text=self.cell_valuer)
# validation methods e.g. "get_user_input..." are temporary,
# to be deleted or replaced later
def get_user_input_string(self, input, widg, finding_id=None):
''' If user focuses into EntryLabel in transit or to edit then
focuses out w/out making changes, no validation will be done.
As for general string validation rules, I don't know what to
disallow so will add validation rules later as needed; for now
anything that can be typed into a general string Entry is valid.
'''
if widg['state'] == 'readonly':
return
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute('''
UPDATE finding
SET particulars = ?
WHERE finding_id = ?''',
(input, self.finding_id))
conn.commit()
cur.close()
conn.close()
def get_user_input_place(self, evt):
''' '''
pass
def get_user_input_event(self, evt=None, finding_id=None):
''' '''
pass
def get_user_input_age(self, input, widg, finding_id=None):
'''
Allow positive whole number only for now. These are strings,
not integers. One purpose of using strings is for possible
application of age expressed as year-month-day, e.g.
'99y 8m 14d'.
'''
if widg['state'] == 'readonly':
return
if input.isnumeric() is False:
widg.clear_bad_data(
"non-numeric value",
"Only positive whole numbers and zero are used.")
return
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
# only works for generic events
cur.execute(
'''
UPDATE finding
SET age = ?
WHERE finding_id = ?
''',
(input, self.finding_id))
conn.commit()
# only works for couple events
current_person = get_current_person()[0]
cur.execute(
'''
UPDATE findings_persons
SET age = ?
WHERE finding_id = ?
AND person_id = ?
''',
(input, self.finding_id, current_person))
conn.commit()
cur.close()
conn.close()
def get_user_input_kin(self, evt=None, finding_id=None):
''' '''
pass
def get_event_types(self):
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('SELECT event_type_id, event_types FROM event_type')
evt_types_ids = cur.fetchall()
cur.close()
conn.close()
return evt_types_ids
def create_new_event(self, evt):
''' '''
self.make_blank_row(parent=self.table_cells_parent)
new_evt = self.new_event_input.get()
# # cover the combobox with an event save button:
row = self.new_event_input.grid_info()['row']
self.new_event_input.grid_forget()
self.new_event_input.delete(0, 'end')
self.new_event_input.grid(column=0, row=row+1)
save_event = wdg.Button(
self.mainframe, text='Save New Event', command=self.save)
save_event.grid(column=0, row=row+1, sticky='ew')
new_evt_lab = wdg.EntryLabel(
self.mainframe,
self.use_input,
table=self,
list_list=FindingsTable.findings)
new_evt_lab.grid(
column=0, row=row, sticky='ew',
pady=3, ipadx=12, padx=(12,0))
new_evt_lab.bind(
'<FocusIn>', new_evt_lab.set_current_finding, add='+')
new_evt_lab.bind(
'<Button-1>', new_evt_lab.set_current_finding, add='+')
new_evt_lab.config(text=new_evt)
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(
'''
SELECT event_type_id
FROM event_type
WHERE event_types = ?
''',
(new_evt,))
new_event_type = cur.fetchone()[0]
current_person = get_current_person()[0]
cur.execute(
'''
INSERT INTO finding (finding_id, person_id, event_type_id)
VALUES (null, ?, ?)
''',
(current_person, new_event_type,))
conn.commit()
cur.execute(
'''
SELECT seq FROM SQLITE_SEQUENCE WHERE name = 'finding'
''')
new_finding_id = cur.fetchone()[0]
new_row = [
new_finding_id,
[new_evt, new_evt_lab],
['', ''], ['', ''], ['', ''], ['', ''], ['', ''],
[{}], [[]], 0, [0,0,0]]
FindingsTable.findings.append(new_row)
self.finding_id = new_finding_id
cur.close()
conn.close()
# format the new event label
ST.config_generic(self.person_tab)
def make_event_selector(self):
''' '''
evt_types_ids = self.get_event_types()
evt_types = []
for tup in evt_types_ids:
evt_types.append(tup[1])
evt_types = sorted(evt_types)
self.new_event_input = wdg.Grombo(
self.mainframe,
'add event',
values=evt_types)
self.new_event_input.grid(
column=0,
row=self.new_row,
sticky='ew', pady=3, padx=6)
self.new_event_input.bind(
'<<ComboboxSelected>>', self.create_new_event)
def make_blank_row(self, parent):
'''
self.new_row is where the new row grids and
+1 accounts for the Separator.
'''
for heading in FINDING_TABLE_TEXT_COLS[1:7]:
self.col_header = heading
self.from_db = False
self.make_label(parent)
for heading in FINDING_TABLE_DOT_COLS:
if heading == 'Roles':
self.dlg_type = 'Roles'
self.role_ids = []
self.make_role_dots(parent, 'Enter new role for event.')
elif heading == 'Notes':
self.dlg_type = 'Notes'
self.make_note_dots(parent, 'Enter new note for event.')
self.make_source_button(parent)
# format the new event row
ST.config_generic(self.person_tab)
def fix_tab_traversal(self):
def third_and_second_items(pos):
return pos[2], pos[1]
row_fixer = []
for child in self.mainframe.winfo_children():
if child.grid_info()['row'] != 0:
row_fixer.append((
child,
child.grid_info()['column'],
child.grid_info()['row']))
row_fixer_2 = sorted(row_fixer, key=third_and_second_items)
widgets = []
for tup in row_fixer_2:
widgets.append(tup[0])
for widg in widgets:
widg.lift()
def use_input(self, input, widg):
''' See notes in EntryLabel class definition. '''
col = widg.grid_info()['column']
funx_per_col = {
1 : self.valdor.validate_date_or_not,
3 : self.get_user_input_string,
4 : self.get_user_input_age}
for k,v in funx_per_col.items():
if col == k:
v(input, widg, finding_id=self.finding_id)
# # *************************
def save(self, evt=None):
'''
Redraws the findings table, for example after a
new event is added, it won't appear interfiled
in the table by date until save() is run or the
app is reloaded or anything that causes the current
person to change or reload (such as resetting the
current person to the already current person).
Even if only the current person's main image is
changed, this always redraws the whole table so
that if the user wants to redraw the person tab
with the newly chosen image, all he has to do is
press Ctrl-S.
'''
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('SELECT person_id FROM current')
current_person = cur.fetchone()[0]
cur.close()
conn.close()
if self.person_tab.focus_get() is self.person_autofill:
self.person_tab.focus_set()
replace_findings_table(
self.main.persons,
self.main.persons.attributes_content,
self.person_autofill,
self.top_pic_button,
self.main,
self.main.tabs.store['person'],
current_person)
class DatePrefsWidgets(wdg.Frame):
def __init__(self, parent, root, *args, **kwargs):
wdg.Frame.__init__(self, parent, *args, **kwargs)
self.root = root
self.rc_menu = rcm.RightClickMenu(self.root)
self.make_widgets()
def make_widgets(self):
self.test_frm = wdg.Frame(self)
self.test_frm.grid(column=0, row=0, pady=24)
self.test1 = wdg.LabelH3(
self.test_frm,
text="Date Entry Demo (doesn't affect your tree)")
self.test1.grid(column=0, row=0)
self.tester = wdg.Frame(self.test_frm)
self.tester.grid(column=0, row=1, sticky='w')
self.tester.grid_columnconfigure(0, weight=1)
self.tester.grid_columnconfigure(1, weight=1)
self.tester.grid_columnconfigure(2, weight=1)
date_entries = ['Date Input I', 'Date Input II', 'Date Input III']
self.date_test = dates.ValidDate()
self.tester_widgets = {}
g = 0
for lab in date_entries:
lbl = wdg.Label(self.tester, text=date_entries[g])
lbl.grid(column=0, row= g+1, padx=24, sticky='e')
dent = wdg.EntryLabel(
self.tester,
self.use_input)
dent.grid(column=1, row=g+1, sticky='ew')
dent.config(width=56)
self.tester_widgets[lab] = dent
g += 1
self.date_prefs = wdg.Frame(self)
self.date_prefs.grid(column=0, row=2, pady=24)
self.general = wdg.FrameHilited(self.date_prefs)
self.general.grid(
column=0, row=3, columnspan=3, rowspan=2,
padx=(12,0), pady=(0,18))
self.pref_lab = wdg.LabelH2(
self.date_prefs, text='Set Date Display Preferences')
self.pref_lab2 = wdg.Label(
self.date_prefs, text='first value in each dropdown list is default')
self.section1 = wdg.LabelHilited(
self.general,
text='Select where all the following changes will apply:')
self.section2 = wdg.LabelH3(self.date_prefs, text='Prefixes')
self.section3 = wdg.LabelH3(self.date_prefs, text='Suffixes')
self.section4 = wdg.LabelH3(self.date_prefs, text='Compound Dates')
options = (
"General", "Estimated", "Approximate", "Calculated",
"Before/After", "Epoch", "Julian/Gregorian",
"From...To...", "Between...And...")
self.date_pref_heads = {}
p = 0
for heading in options:
lab = wdg.LabelH3(self.date_prefs, text=options[p])
self.date_pref_heads[heading] = lab
combo = wdg.ClearableReadonlyCombobox(
self.date_prefs,
values=dates.DATE_PREF_COMBOS[p],
font=(formats['input_font']))
dates.date_pref_combos[heading] = combo
p += 1
self.this_tree_or_all = tk.StringVar()
self.date_radios = {}
d = 0
radios = ('This Tree', 'All Trees')
for choice in radios:
rad = wdg.RadiobuttonHilited(
self.general, text=choice, variable=self.this_tree_or_all,
command=dates.get_rad_val)
rad.grid(column=d, row=1)
self.date_radios[choice] = rad
d += 1
self.date_radios['This Tree'].config(value='local')
self.date_radios['All Trees'].config(value='global')
self.this_tree_or_all.set('local')
self.pref_lab.grid(column=0, row=0, columnspan=4, sticky='we')
self.pref_lab2.grid(column=0, row=1, columnspan=4, sticky='we', pady=(0, 24))
self.section1.grid(column=0, row=0, sticky='w',
pady=12, padx=24, columnspan=3)
self.date_pref_heads['General'].grid(column=3, row=3, padx=12)
dates.date_pref_combos['General'].grid(column=3, row=4, padx=12, pady=(0,18))
self.section2.grid(column=0, row=5, sticky='w', pady=12, padx=24)
self.date_pref_heads['Estimated'].grid(column=0, row=6, padx=12)
dates.date_pref_combos['Estimated'].grid(column=0, row=7, padx=12, pady=(0,18))
self.date_pref_heads['Approximate'].grid(column=1, row=6, padx=12)
dates.date_pref_combos['Approximate'].grid(column=1, row=7, padx=12, pady=(0,18))
self.date_pref_heads['Calculated'].grid(column=2, row=6, padx=12)
dates.date_pref_combos['Calculated'].grid(column=2, row=7, padx=12, pady=(0,18))
self.date_pref_heads['Before/After'].grid(column=3, row=6, padx=12)
dates.date_pref_combos['Before/After'].grid(column=3, row=7, padx=12, pady=(0,18))
self.section3.grid(column=0, row=8, sticky='w', pady=12, padx=24)
self.date_pref_heads['Epoch'].grid(column=0, row=9, padx=12)
dates.date_pref_combos['Epoch'].grid(column=0, row=10, padx=12, pady=(0,12))
self.date_pref_heads['Julian/Gregorian'].grid(column=1, row=9, padx=12)
dates.date_pref_combos['Julian/Gregorian'].grid(
column=1, row=10, padx=12, pady=(0,12))
self.section4.grid(column=2, row=8, sticky='w', pady=12, padx=24)
self.date_pref_heads['From...To...'].grid(column=2, row=9, padx=12)
dates.date_pref_combos['From...To...'].grid(column=2, row=10, padx=12, pady=(0,12))
self.date_pref_heads['Between...And...'].grid(column=3, row=9, padx=12)
dates.date_pref_combos['Between...And...'].grid(
column=3, row=10, padx=12, pady=(0,12))
self.bbox = wdg.Frame(self)
self.bbox.grid(column=0, row=3, pady=24)
self.submit = wdg.Button(
self.bbox,
text='SUBMIT PREFERENCES',
command=lambda tk_var=self.this_tree_or_all: submit_date_prefs(tk_var),
width=30)
self.submit.grid(column=0, row=0, padx=(0,24))
self.revert = wdg.Button(
self.bbox,
text='REVERT TO DEFAULT VALUES',
command=lambda tk_var=self.this_tree_or_all: revert_to_default(tk_var),
width=30)
self.revert.grid(column=1, row=0, padx=(24,0))
self.dates_visited = (
(self.test_frm,
"",
"Use top area to test input; bottom area for display settings."),
(self.tester, "Demo Date Entry Inputs",
"Enter date parts in any order. "
"Right-click any area for more info."),
(self.general, "",
"Select whether a format should affect "
"all your trees or just the current one."),
(dates.date_pref_combos['General'],
"General Date Format",
"Select a general type of date display."),
(dates.date_pref_combos['Estimated'],
"Estimated Date Prefix",
"Use estimated dates for unsourced guessed dates."),
(dates.date_pref_combos['Approximate'],
"Approximate Date Prefix",
"Use approximated dates for sourced imprecise dates."),
(dates.date_pref_combos['Calculated'],
"Calculated Date Prefix",
"Calculated dates derived from other data such as age."),
(dates.date_pref_combos['Before/After'],
"Before or After Date Prefix",
"When you know something happened before or after some date."),
(dates.date_pref_combos['Epoch'],
"Epoch Date Suffix",
"'BC' and 'AD' now have more politically correct variations."),
(dates.date_pref_combos['Julian/Gregorian'],
"Calendar Era Date Suffix",
"Mark dates 'old style' or 'new style' for events during "
"calendar transition times."),
(dates.date_pref_combos['From...To...'],
"Format Two Dates in a Span",
"Something started at one time and lasted till another time."),
(dates.date_pref_combos['Between...And...'],
"Format Two Dates in a Range",
"Something happened somewhere within a range between two dates."),
(self.submit,
"Submit Changes",
"The changes you selected will be saved."),
(self.revert,
"Revert to Defaults",
"Date formats will revert to Treebard's out-of-the-box defaults."))
rcm_widgets = (
self.test1,
self.tester_widgets['Date Input I'],
self.tester_widgets['Date Input II'],
self.tester_widgets['Date Input III'],
self.pref_lab, self.general,
dates.date_pref_combos['General'],
dates.date_pref_combos['Estimated'],
dates.date_pref_combos['Approximate'],
dates.date_pref_combos['Calculated'],
dates.date_pref_combos['Before/After'],
dates.date_pref_combos['Epoch'],
dates.date_pref_combos['Julian/Gregorian'],
dates.date_pref_combos['From...To...'],
dates.date_pref_combos['Between...And...'],
self.submit,
self.revert)
rcm.make_rc_menus(
rcm_widgets,
self.rc_menu,
ms.dates_prefs_msg)
def use_input(self, input, widg):
'''
This is what makes EntryLabel work. No finding_id
needed for date tester widgets in date preferences tab.
'''
self.date_test.validate_date_or_not(input, widg)
class PersonAdd(wdg.Frame):
def __init__(self, parent, autofill, root, *args, **kwargs):
wdg.Frame.__init__(self, parent, *args, **kwargs)
self.root = root
self.autofill = autofill
self.gender = 'unknown'
self.new_person_id = None
self.role_person_edited = False
self.findings_roles_id = None
self.rc_menu = rcm.RightClickMenu(self.root)
self.make_widgets()
def make_widgets(self):
self.gender_input = wdg.LabelEntryPair(self, input_type='readonly')
self.gender_input.label.config(text='Gender:')
self.gender_input.input.config(values=GENDER_TYPES)
all_pics = self.get_all_pics()
self.image_input = wdg.LabelEntryPair(self, input_type='clickany')
self.image_input.input.config(values=all_pics)
self.image_input.label.config(text='Main Image:')
self.name_type_input = wdg.LabelEntryPair(
self, input_type='readonly')
self.name_type_input.input.config(values=self.get_name_types())
self.name_type_input.label.config(text='Name Type:')
self.name_input = wdg.LabelEntryPair(self)
self.name_input.input.config(width=65)
self.name_input.label.config(text='Full Name')
self.how = wdg.LabelH3(
self,
text='Alphabetize name: after clicking auto-sort, tab into '
'auto-filled name fields to modify\nsort order with '
'arrow keys or if sort order is correct, just click ADD.')
autosort = wdg.Button(
self, text='AUTOSORT', command=self.show_sort_order)
self.order_frm = wdg.Frame(self)
s = 0
for stg in range(20):
mov = wdg.LabelMovable(self.order_frm)
mov.grid(column=s, row=0, padx=3)
s += 1
self.buttonbox = wdg.Frame(self)
self.add_butt = wdg.Button(self.buttonbox, text='ADD', width=8)
self.gender_input.grid(
column=0, row=3, padx=12, pady=12, sticky='e')
self.image_input.grid(column=1, row=3, padx=12, pady=12)
self.name_type_input.grid(
column=0, row=4, padx=12, pady=12, sticky='e')
self.name_input.grid(column=1, row=4, padx=12, pady=12)
self.how.grid(column=0, row=5, padx=6, pady=6, columnspan=4)
autosort.grid(column=0, row=6, padx=6, pady=6)
self.order_frm.grid(column=1, row=6, columnspan=4, pady=24)
self.buttonbox.grid(column=1, row=7, sticky='e')
self.add_butt.grid(column=0, row=0, padx=6, pady=6, sticky='e')
self.preset()
rcm_widgets = (self.name_input.input, self.name_type_input.input)
rcm.make_rc_menus(
rcm_widgets,
self.rc_menu,
ms.person_add_msg)
def preset(self):
self.gender_input.input.config(state='normal')
self.gender_input.input.delete(0, 'end')
self.gender_input.input.config(state='readonly')
self.image_input.input.delete(0, 'end')
self.image_input.input.insert(0, 'no_photo_001.gif')
self.name_type_input.input.config(state='normal')
self.name_type_input.input.delete(0, 'end')
self.name_type_input.input.insert(0, 'birth name')
self.name_type_input.input.config(state='readonly')
def reset(self):
self.preset()
self.name_input.input.delete(0, 'end')
for child in self.order_frm.winfo_children():
child['text'] = ''
self.dupe_check = True
def add_person(self, findings_roles_id=None):
self.get_entered_values()
self.findings_roles_id = findings_roles_id
self.check_for_dupes()
def get_entered_values(self):
'''
'''
if len(self.gender_input.input.get()) != 0:
self.gender = self.gender_input.input.get()
self.full_name = self.name_input.input.get()
self.selected_image = self.image_input.input.get()
self.name_type = self.name_type_input.input.get()
def check_for_dupes(self):
'''
If birth name already exists in database, open dialog.
'''
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('SELECT person_id FROM person')
all_people = cur.fetchall()
cur.close()
conn.close()
all_people = [[i[0]] for i in all_people]
names_only = []
for id in all_people:
display_name = names.get_name_with_id(id[0])
names_only.append(display_name)
id.insert(0, display_name)
people_vals = []
for lst in all_people:
if not lst[0]:
lst[0] = ''
people_vals.append(' #'.join([lst[0], str(lst[1])]))
if self.full_name not in names_only:
self.make_new_person()
self.make_sort_order()
self.save_new_name(self.new_person_id)
self.reset()
else:
self.dupe_check = msg.YesNoMessage(
self,
title='Duplicate Check',
message="This birth name already exists. To create a "
"new person by the same name, click SUBMIT. The "
"two persons can be merged later if desired.",
input=self.full_name).show()
if self.dupe_check is True:
self.make_new_person()
self.make_sort_order()
self.save_new_name(self.new_person_id)
self.reset()
else:
self.reset()
def make_new_person(self):
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(
'''
INSERT INTO person VALUES (null, ?)
''',
(self.gender,))
conn.commit()
cur.execute(
'''
SELECT seq FROM SQLITE_SEQUENCE WHERE name = 'person'
''')
new_person_id = cur.fetchone()
if new_person_id:
self.new_person_id = new_person_id[0]
if self.role_person_edited is True:
cur.execute(
'''
UPDATE findings_roles
SET person_id = ?
WHERE findings_roles_id = ?
''',
(self.new_person_id, self.findings_roles_id))
conn.commit()
self.role_person_edited = False
cur.close()
conn.close()
def make_sort_order(self):
self.order = []
for child in self.order_frm.winfo_children():
text = child['text']
self.order.append(text)
self.order = ' '.join(self.order)
self.order = self.order.replace(' , ', ', ')
self.order = self.order.strip(', ')
def save_new_name(self, subject_id):
conn = sqlite3.connect(current_file)
cur = conn.cursor()
conn.execute('PRAGMA foreign_keys = 1')
cur.execute(
'SELECT image_id FROM image WHERE images = ?',
(self.selected_image,))
img_id = cur.fetchone()[0]
cur.execute('''
INSERT INTO images_entities (image_id, main_image, person_id)
VALUES (?, 1, ?) ''',
(img_id, subject_id))
conn.commit()
cur.execute(
'''
SELECT name_type_id
FROM name_type
WHERE name_types = ?
''',
(self.name_type,))
name_type_id = cur.fetchone()
name_type_id = name_type_id[0]
cur.execute('''
INSERT INTO name
VALUES (null, ?, ?, ?, ?, null)''',
(subject_id, self.full_name, name_type_id, self.order))
conn.commit()
new_list = names.make_values_list_for_person_select()
self.autofill.values = new_list
cur.close()
conn.close()
self.image_input.input.delete(0, 'end')
self.image_input.input.insert(0, 'no_photo_001.gif')
for widg in (self.name_type_input, self.name_input):
widg.input.config(state='normal')
widg.input.delete(0, 'end')
self.name_type_input.input.config(state='readonly')
for child in self.order_frm.winfo_children():
child.config(text='')
self.gender_input.input.config(state='normal')
self.gender_input.input.delete(0, 'end')
self.gender_input.input.config(state='readonly')
self.gender_input.input.focus_set()
def show_sort_order(self):
self.got = self.name_input.input.get()
self.new_name = self.got
self.got = self.got.split()
if len(self.got) == 0:
return
else:
length = len(self.got)-1
word = self.got[length].lower()
self.got.insert(0, ',')
length += 1
if word not in NAME_SUFFIXES:
self.got.insert(0, self.got.pop())
elif word in NAME_SUFFIXES and self.got[length].lower() == word:
self.got.insert(0, self.got.pop())
self.got.insert(0, self.got.pop())
for child in self.order_frm.winfo_children():
child.config(text='')
v = 0
for name in self.got:
self.order_frm.winfo_children()[v].config(text=name)
v += 1
def get_name_types(self):
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('SELECT name_types FROM name_type ORDER BY name_types')
name_types = cur.fetchall()
cur.close()
conn.close()
name_types = [i[0] for i in name_types]
return name_types
def get_all_pics(self):
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('SELECT images FROM image')
picvals = cur.fetchall()
cur.close()
conn.close()
return picvals
class RolesDialog(wdg.Toplevel):
def __init__(
self,
parent,
subject,
role_ids,
dlg_header,
finding_id=None,
root=None,
*args,
**kwargs):
wdg.Toplevel.__init__(self, *args, **kwargs)
self.parent = parent
self.title('{} ({})'.format('Roles for an Event', subject))
self.role_ids = role_ids
self.dlg_header = dlg_header
self.finding_id = finding_id
self.root = root
self.autofill = parent.master.person_autofill
self.original_role_type = ''
self.original_role_person = ''
self.user_input_person = ''
self.role_types = []
self.findings_roles_id = 0
self.edited_role_id = 0
self.got_row = 0
self.role_types = []
self.persons = get_all_persons()
self.roles_per_finding = []
self.rc_menu = rcm.RightClickMenu(self.root)
self.make_widgets()
self.protocol("WM_DELETE_WINDOW", self.close_roles_dialog)
def make_widgets(self):
# auto-scrollbar
self.role_canvas = wdg.Canvas(
self,
bd=0,
highlightthickness=0)
self.role_canvas.grid(column=0, row=0, sticky='news')
self.role_content = wdg.Frame(self.role_canvas)
self.role_content.grid_columnconfigure(0, weight=1)
self.role_content.grid_columnconfigure(1, weight=0)
self.role_content.grid_columnconfigure(2, weight=1)
self.role_content.grid_rowconfigure(1, weight=1)
vscroll = wdg.AutoScrollbar(self, width=16)
vscroll.grid(column=1, row=0, sticky='ns')
hscroll = wdg.AutoScrollbar(self, orient='horizontal', width=16)
hscroll.grid(column=0, row=2, sticky='ew')
self.role_canvas.config(
yscrollcommand=vscroll.set,
xscrollcommand=hscroll.set)
vscroll.config(command=self.role_canvas.yview)
hscroll.config(command=self.role_canvas.xview)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=1)
# widgets
roles_table = wdg.Frame(self.role_content)
new_roles_area = wdg.Frame(self.role_content)
self.roles_dialog_header = wdg.Frame(self.role_content)
self.rolfrm = wdg.Frame(self.role_content)
self.make_roles_table()
new_roles_header = wdg.LabelH3(new_roles_area, text='Create New Role')
self.role_type_input = wdg.ClickAnywhereCombo(
new_roles_area, values=self.role_types)
self.person_input = wdg.AutofillEntry(new_roles_area)
self.person_input.values = self.persons
self.person_input.config(textvariable=self.person_input.var)
self.person_input.fix_max_width()
self.add_butt = wdg.Button(
new_roles_area,
text='Add',
command=self.get_add_state)
self.done_butt = wdg.Button(
new_roles_area,
text='Done',
command=self.add_and_close)
self.close_butt = wdg.Button(
new_roles_area,
text='Close',
command=self.close_roles_dialog)
roles_statusbar = wdg.StatusbarTooltips(self)
roles_statusbar.grid(column=0, row=2, sticky='ew')
roles_table.grid(column=0, row=0)
new_roles_area.grid(column=0, row=2)
self.roles_dialog_header.grid(column=0, row=0, ipadx=12, padx=24, pady=(36,12))
self.rolfrm.grid(column=0, row=1, columnspan=2)
self.rolfrm.grid_columnconfigure(4, weight=1)
new_roles_header.grid(column=0, row=0, padx=6, pady=6)
self.role_type_input.grid(column=0, row=1, padx=(24,6), pady=6)
self.person_input.grid(column=1, row=1, padx=6, pady=6)
self.add_butt.grid(column=2, row=1, padx=6, pady=6)
self.done_butt.grid(column=3, row=1, padx=6, pady=6)
self.close_butt.grid(column=4, row=2, padx=(6,24), pady=(6,24))
self.make_edit_row()
self.get_role_types()
visited = (
(self.roles_dialog_header,
'',
'Type, date, place & particulars of this event if known.'),
(self.edit_role_type,
'Edit Role Type Input',
'Existing roletype can be changed to a different type.'),
(self.edit_role_person,
'Edit Role Person Input',
'Existing role person can be changed or new person made.'),
(self.ok_butt,
'Edit Role OK Button',
'Submit changes to this role.'),
(self.cancel_butt,
'Edit Role Cancel Button',
'Close role edit row, make no changes.'),
(self.delete_butt,
'Edit Role Delete Button',
'Delete this role but not the role type or role person.'),
(self.role_type_input,
'New Role: Type Input',
'Select role type or create a new one.'),
(self.person_input,
'New Role: Person Input',
'Select role person, add new one, or leave blank.'),
(self.add_butt,
'Add New Role Button',
'Make a new role and leave dialog open.'),
(self.done_butt,
'Add New Role & Close Dialog Button',
'Make a new role and close dialog.'),
(self.close_butt,
'Close Dialog Button',
'Close dialog without making another role.'))
wdg.run_statusbar_tooltips(
visited,
roles_statusbar.status_label,
roles_statusbar.tooltip_label)
rcm_widgets = (
self.role_type_input, self.person_input, self.add_butt,
self.done_butt, self.close_butt)
rcm.make_rc_menus(
rcm_widgets,
self.rc_menu,
ms.role_dlg_msg,
header_parent=self.roles_dialog_header,
dlg_header=self.dlg_header,
which_dlg='roles')
self.role_canvas.create_window(
0, 0, anchor=tk.NW, window=self.role_content)
ST.config_generic(self)
self.resize_window()
self.resize_scrollbar()
def make_roles_list(self):
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute(
'''
SELECT
findings_roles_id,
role_types,
person_id,
findings_roles.role_type_id
FROM role_type
JOIN findings_roles
ON role_type.role_type_id = findings_roles.role_type_id
WHERE finding_id = ?
''',
(self.finding_id,))
roles_per_finding = cur.fetchall()
if roles_per_finding:
self.roles_per_finding = [list(i) for i in roles_per_finding]
for lst in self.roles_per_finding:
name = names.get_name_with_id(lst[2])
lst.append(lst[2])
lst[2] = name
cur.close()
conn.close()
def make_roles_table(self):
def get_clicked_row(evt):
self.got_row = evt.widget.grid_info()['row']
def on_hover(evt):
evt.widget.config(text='Edit')
def on_unhover(evt):
evt.widget.config(text='')
self.make_roles_list()
first_butt = None
n = 0
for lst in self.roles_per_finding:
labelr = wdg.Label(self.rolfrm, text=lst[1], anchor='e')
sep = wdg.Label(self.rolfrm, text='|', anchor='w')
labelp = wdg.Label(self.rolfrm, text=lst[2], anchor='w')
editx = wdg.ButtonQuiet(
self.rolfrm,
width=2,
command=self.grid_edit_row)
labelr.grid(column=0, row=n, padx=6, pady=3, sticky='ew')
sep.grid(column=1, row=n)
labelp.grid(column=2, row=n, padx=6, pady=3, sticky='ew')
editx.grid(column=3, row=n, padx=6, pady=3)
if n == 0:
first_butt = editx
editx.bind('<Enter>', on_hover)
editx.bind('<Leave>', on_unhover)
editx.bind('<Button-1>', get_clicked_row)
editx.bind('<space>', get_clicked_row)
self.rc_menu.loop_made[editx] = ms.gen_edit_role_rows
n += 1
if first_butt:
first_butt.focus_set()
def grid_edit_row(self):
self.edited_role_id = self.roles_per_finding[self.got_row][0]
for child in self.rolfrm.winfo_children():
if child.winfo_class() != 'Label':
pass
elif child.grid_info()['row'] == self.got_row:
if child.grid_info()['column'] == 0:
self.original_role_type = self.roles_per_finding[self.got_row][1]
elif child.grid_info()['column'] == 2:
self.original_role_person = self.roles_per_finding[self.got_row][2]
self.edit_row.grid(row=self.got_row)
self.edit_role_type.delete(0, 'end')
self.edit_role_person.delete(0, 'end')
self.edit_role_type.insert(0, self.roles_per_finding[self.got_row][1])
self.edit_role_type.focus_set()
chosen_person_id = self.roles_per_finding[self.got_row][4]
if chosen_person_id is None:
self.edit_role_person.insert(0, '')
else:
self.edit_role_person.insert(
0, '{} #{}'.format(
self.roles_per_finding[self.got_row][2],
str(chosen_person_id)))
self.edit_row.lift()
self.resize_scrollbar()
self.resize_window()
def make_edit_row(self):
def cancel_edit_row():
self.edit_row.grid_remove()
self.resize_scrollbar()
self.resize_window()
self.edit_row = wdg.Frame(self.rolfrm)
self.edit_role_type = wdg.ClickAnywhereCombo(
self.edit_row, values=self.role_types)
self.edit_role_person = wdg.AutofillEntry(self.edit_row)
self.edit_role_person.values = self.persons
self.edit_role_person.config(textvariable=self.edit_role_person.var)
self.edit_role_person.fix_max_width()
self.ok_butt = wdg.Button(
self.edit_row,
text='OK',
command=self.get_edit_state)
self.cancel_butt = wdg.Button(
self.edit_row,
text='Cancel',
command=cancel_edit_row)
self.delete_butt = wdg.Button(
self.edit_row,
text='Delete',
command=self.delete_role)
self.edit_row.grid(column=0, row=0, columnspan=5, sticky='ew')
self.edit_role_type.grid(column=0, row=0, padx=6, pady=6)
self.edit_role_person.grid(column=1, row=0, padx=6, pady=6)
self.ok_butt.grid(column=2, row=0, padx=6, pady=6)
self.cancel_butt.grid(column=3, row=0, padx=6, pady=6)
self.delete_butt.grid(column=4, row=0, padx=6, pady=6)
self.edit_row.grid_remove()
def get_edit_state(self):
'''
Detect and respond to changes in existing roles on OK button.
'''
edited_role_type = self.edit_role_type.get()
edited_role_person = self.edit_role_person.get()
if edited_role_type in self.role_types:
if edited_role_type != self.original_role_type:
self.update_role_type(edited_role_type)
else:
self.make_new_role_type(edited_role_type)
self.update_role_type(edited_role_type)
if edited_role_person in self.persons:
if edited_role_person != self.original_role_person:
self.change_role_person(edited_role_person)
elif len(edited_role_person) == 0:
findings_roles_id = self.roles_per_finding[self.got_row][0]
self.set_role_person_unknown(findings_roles_id)
else:
self.make_new_person(from_edit=True, edited_role_person=edited_role_person)
edited_person_id = self.person_add.new_person_id
self.update_role_person(edited_person_id)
self.original_role_type = edited_role_type
self.original_role_person = self.edit_role_person.get()
self.edit_row.grid_remove()
def get_add_state(self):
if len(self.role_type_input.get()) == 0:
self.role_type_input.focus_set()
return
chosen_role_type = self.role_type_input.get()
self.user_input_person = self.person_input.get()
if chosen_role_type not in self.role_types:
self.make_new_role_type(chosen_role_type)
self.make_new_role(chosen_role_type)
def make_new_role(self, role_type):
if len(self.user_input_person) == 0:
role_person_id = None
elif self.user_input_person not in self.persons:
self.make_new_person()
role_person_id = self.person_add.new_person_id
else:
role_person_id = self.person_input.get().split(' #')[1]
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(
'''
SELECT role_type_id
FROM role_type
WHERE role_types = ?
''',
(role_type,))
role_type_id = cur.fetchone()[0]
cur.execute(
'''
INSERT INTO findings_roles
VALUES (null, ?, ?, ?)
''',
(self.finding_id, role_type_id, role_person_id))
conn.commit()
cur.close()
conn.close()
dont_destroy = []
for child in self.rolfrm.winfo_children():
if child.winfo_class() == 'Frame':
edit_row_frame = child
dont_destroy.append(edit_row_frame)
break
for child in self.rolfrm.winfo_children():
if child.master == edit_row_frame:
dont_destroy.append(child)
for child in self.rolfrm.winfo_children():
if child not in dont_destroy:
child.destroy()
self.make_roles_table()
self.role_type_input.delete(0, 'end')
self.person_input.delete(0, 'end')
self.resize_scrollbar()
self.resize_window()
self.make_roles_list()
def make_new_person(self, from_edit=False, edited_role_person=None):
'''
Open PersonAdd dialog.
'''
def close_dialog():
self.new_role_person_dialog.destroy()
self.new_role_person_dialog = wdg.Toplevel(self)
self.person_add = PersonAdd(
self.new_role_person_dialog,
self.person_input,
self.root)
self.person_add.grid()
self.person_add.name_input.input.delete(0, 'end')
if from_edit is False:
self.person_add.name_input.input.insert(0, self.user_input_person)
else:
self.person_add.name_input.input.insert(0, edited_role_person)
self.person_add.add_butt.config(command=self.person_add.add_person)
self.close_butt = wdg.Button(
self.person_add.buttonbox,
text='Close',
width=8,
command=close_dialog)
self.close_butt.grid(column=1, row=0, padx=6, pady=6)
self.new_role_person_dialog.protocol("WM_DELETE_WINDOW", close_dialog)
self.person_add.gender_input.input.focus_set()
ST.config_generic(self.new_role_person_dialog)
self.new_role_person_dialog.wait_window()
self.persons = get_all_persons()
def change_role_person(self, edited_role_person):
'''
Change role person to a person that already exists.
'''
new_person_data = edited_role_person.split(' #')
new_person_id = new_person_data[1]
new_person_name = new_person_data[0]
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(
'''
UPDATE findings_roles
SET person_id = ?
WHERE findings_roles_id = ?
''',
(new_person_id, self.edited_role_id))
conn.commit()
cur.close()
conn.close()
for child in self.rolfrm.winfo_children():
if child.winfo_class() == 'Frame':
pass
elif child.grid_info()['row'] == self.got_row:
if child.grid_info()['column'] == 2:
child.config(text=new_person_name)
self.resize_scrollbar()
self.resize_window()
self.make_roles_list()
def make_new_role_type(self, edited_role_type):
'''
Get user pref w/ dialog. Connect to db, insert new role_type.
'''
if len(edited_role_type) == 0:
return
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(
'''
INSERT INTO role_type VALUES(null, ?)
''',
(edited_role_type,))
conn.commit()
cur.close()
conn.close()
self.get_role_types()
def update_role_type(self, edited_role_type):
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(
'''
SELECT role_type_id
FROM role_type
WHERE role_types = ?
''',
(edited_role_type,))
chosen_role_type_id = cur.fetchone()
if chosen_role_type_id:
chosen_role_type_id = chosen_role_type_id[0]
cur.execute(
'''
UPDATE findings_roles
SET role_type_id = ?
WHERE findings_roles_id = ?
''',
(chosen_role_type_id, self.edited_role_id))
conn.commit()
cur.close()
conn.close()
for child in self.rolfrm.winfo_children():
if child.winfo_class() == 'Frame':
pass
elif child.grid_info()['row'] == self.got_row:
if child.grid_info()['column'] == 0:
child.config(text=edited_role_type)
self.resize_scrollbar()
self.resize_window()
self.get_role_types()
self.make_roles_list()
def update_role_person(self, edited_person_id):
'''
Change person in existing role to newly created person.
'''
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(
'''
UPDATE findings_roles
SET person_id = ?
WHERE findings_roles_id = ?
''',
(edited_person_id, self.edited_role_id))
conn.commit()
cur.close()
conn.close()
edited_role_person_name = names.get_name_with_id(edited_person_id)
for child in self.rolfrm.winfo_children():
if child.winfo_class() == 'Frame':
pass
elif child.grid_info()['row'] == self.got_row:
if child.grid_info()['column'] == 2:
child.config(text=edited_role_person_name)
self.resize_scrollbar()
self.resize_window()
self.make_roles_list()
def set_role_person_unknown(self, findings_roles_id):
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(
'''
UPDATE findings_roles
SET person_id = null
WHERE findings_roles_id = ?
''',
(findings_roles_id,))
conn.commit()
cur.close()
conn.close()
for child in self.rolfrm.winfo_children():
if child.winfo_class() == 'Frame':
pass
elif child.grid_info()['row'] == self.got_row:
if child.grid_info()['column'] == 2:
child.config(text='')
self.resize_scrollbar()
self.resize_window()
self.make_roles_list()
def delete_role(self):
def delete_role_from_db():
conn = sqlite3.connect(current_file)
conn.execute('PRAGMA foreign_keys = 1')
cur = conn.cursor()
cur.execute(
'''
DELETE FROM findings_roles
WHERE findings_roles_id = ?
''',
(self.edited_role_id,))
conn.commit()
cur.close()
conn.close()
self.make_roles_list()
delete_role_from_db()
self.edit_row.grid_remove()
for child in self.rolfrm.winfo_children():
if child.winfo_class() == 'Frame':
pass
elif child.grid_info()['row'] == self.got_row:
child.destroy()
elif child.grid_info()['row'] > self.got_row:
row = child.grid_info()['row']
child.grid(row=row-1)
self.resize_scrollbar()
self.resize_window()
def add_and_close(self):
self.get_add_state()
self.close_roles_dialog()
def close_roles_dialog(self, evt=None):
self.destroy()
def get_role_types(self):
self.role_types = []
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute(
'''
SELECT role_types
FROM role_type
''')
role_types = cur.fetchall()
cur.close()
conn.close()
role_types = [i[0] for i in role_types]
role_types.sort()
self.role_types = role_types
for widg in (self.edit_role_type, self.role_type_input):
widg.config(values=self.role_types)
def resize_scrollbar(self):
self.update_idletasks()
self.role_canvas.config(scrollregion=self.role_canvas.bbox("all"))
def resize_window(self):
self.update_idletasks()
page_x = self.role_content.winfo_reqwidth() + 24
page_y = self.role_content.winfo_reqheight() + 48
self.geometry('{}x{}'.format(page_x, page_y))
def get_all_persons():
conn = sqlite3.connect(current_file)
cur = conn.cursor()
cur.execute('SELECT person_id FROM person')
person_ids = cur.fetchall()
person_ids = [i[0] for i in person_ids]
cur.close()
conn.close()
persons = []
for id in person_ids:
name = names.get_name_with_id(id)
if len(name) != 0:
name = '{} #{}'.format(name, id)
persons.append(name)
return persons
def placeholder():
print('menu test')
if __name__ == "__main__":
root = tk.Tk()
ST = st.ThemeStyles(app=root)
Treebard(root).pack(side='top', fill='both', expand=True)
root.iconbitmap(default='favicon.ico')
root.title('Treebard Genieware Pattern Simulation')
root.withdraw()
splash = SplashScreen(root)
root.geometry('+0+0')
root.mainloop()