search.py
Nov 25, 2022 18:08:48 GMT -8
Post by Uncle Buddy on Nov 25, 2022 18:08:48 GMT -8
<drive>:\treebard\search.py Last Changed 2024-07-25
# search.py
import tkinter as tk
import sqlite3
from base import tree_path
from redraw import Redraw, get_name_from_id
from widgets import (
configall, make_formats_dict, get_color_scheme_id, ScrolledDialog,
run_statusbar_tooltips, RightClickMenu, make_rc_menus, TabBook,
NEUTRAL_COLOR)
from persons import open_new_person_dialog
from dates import format_stored_date, get_date_formats
from messages_context_help import search_person_help_msg
from unigeds_queries import (
select_person_distinct_like, select_name_details,
select_event_sorter, select_name_sort_order, select_person_death_date,
select_person_birth_date, select_event_mother, select_event_father)
import dev_tools as dt
from dev_tools import look, seeline
COL_HEADS = ('ID', 'Name', 'Birth', 'Death', 'Mother', 'Father')
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')
class PersonSearch(ScrolledDialog):
formats = {}
def __init__(
self, master, treebard, main, entry, events_table, formats,
*args, **kwargs):
ScrolledDialog.__init__(self, master, *args, **kwargs)
self.tree = master
self.treebard = treebard
self.main = main
self.entry = entry
self.events_table = events_table
self.formats = formats
self.protocol("WM_DELETE_WINDOW", self.cancel)
self.date_prefs = get_date_formats()
self.tree.title('Person Search')
self.geometry('+100+20')
self.result_rows = []
self.hilit_row = None
self.unhilit_row = None
self.sent_text = ''
self.all_matches = []
self.tkvars = {}
self.sort_by = None
self.widget = None
self.name_tip = None
self.nametip_text = None
self.pointed_to = None
self.person_id = None
self.ma_id = None
self.pa_id = None
self.offspring_event = None
self.rc_menu = RightClickMenu(self, self.treebard)
self.buttonbox = tk.Frame(self.window)
self.b1 = tk.Button(self.buttonbox, text="OK", width=7)
self.b2 = tk.Button(
self.buttonbox, text="CANCEL", width=7, command=self.cancel)
self.make_inputs()
self.maxsize(
int(self.winfo_screenwidth() * 0.90),
int(self.winfo_screenheight() * 0.90))
ScrolledDialog.bind_canvas_to_mousewheel(self.canvas)
configall(self, self.formats)
self.resize_scrolled_content(self, self.canvas, add_x=24, add_y=24)
def make_inputs(self):
self.columnconfigure(1, weight=1)
header = tk.Frame(self.window)
self.search_dlg_heading = tk.Label(header, text='Person Search')
self.search_dlg_heading.grid(column=0, row=0, pady=(24,0))
instrux = tk.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.entry.get()
self.search_input = tk.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.handle_key_press)
self.person_adder = tk.Button(
header,
text='ADD NEW PERSON',
command=lambda inwidg=self.entry,
treebard=self.treebard,
inwidg2=self.search_input: self.make_new_person(
inwidg, treebard, inwidg2))
self.search_table = tk.Frame(self.window)
# children of self
self.columnconfigure(1, weight=1)
self.rowconfigure(1, weight=1)
self.statusbar.grid(column=1, row=3, sticky='ew')
# children of self.window
self.window.columnconfigure(2, weight=1)
self.window.rowconfigure(1, weight=1)
header.grid(column=0, row=0, sticky='ew')
self.search_table.grid(column=0, row=1, sticky='news', padx=48, pady=48)
self.buttonbox.grid(column=0, row=2, sticky='e', pady=6)
# children of header
self.person_adder.grid(column=2, row=1, padx=12, pady=12)
# children of self.buttonbox
self.b1.grid(column=0, row=0)
self.b2.grid(column=1, row=0, padx=(12,0))
visited = (
(self.search_input,
"Person Search Input",
"Type any part of any name or ID number; table will fill "
"with matches."),
(self.search_table,
"Person Search Table",
"Select highlighted row with Enter or Space key to change "
"current person, or click any row."))
run_statusbar_tooltips(
visited,
self.statusbar.status_label,
self.statusbar.tooltip_label, self)
rcm_widgets = (
self.search_input, self.search_dlg_heading, self.search_table)
make_rc_menus(
rcm_widgets,
self.rc_menu,
search_person_help_msg)
self.make_header_row()
def make_new_person(self, inwidg, treebard, inwidg2):
open_new_person_dialog(
self.tree, self.treebard, inwidg=inwidg, inwidg2=inwidg2)
self.tree.person_autofill_values = self.tree.update_person_autofill_values()
inwidg.delete(0, 'end')
def cancel(self):
self.grab_release()
self.entry.focus_set()
self.treebard.lift()
self.destroy()
def handle_key_press(self, evt):
""" Recreate 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_table.winfo_children():
if child.grid_info()['row'] not in (0, 1):
child.destroy()
conn = sqlite3.connect(self.tree.file)
cur = conn.cursor()
self.all_matches = self.get_matches(cur)
self.make_search_dialog_cells(cur)
self.resize_scrolled_content(self, self.canvas, add_y=36, add_x=24)
cur.close()
conn.close()
def get_matches(self, cur):
got = self.search_input.get()
if len(got) < 3:
return
cur.execute(select_person_distinct_like, (f"%{got}%", f"%{got}%"))
all_matches = [list(i) for i in cur.fetchall()]
for lst in all_matches:
person_id = lst[0]
cur.execute(select_name_details, (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('')
return all_matches
def make_header_row(self):
for col in range(0, 6):
var = tk.StringVar()
self.tkvars[col] = var
lab = tk.Label(
self.search_table,
text=COL_HEADS[col],
cursor='hand2',
anchor='w')
lab.grid(column=col, row=0, sticky='ew', ipadx=12)
lab.bind('<Button-1>', self.track_column_state)
def make_search_dialog_cells(self, cur):
if not self.all_matches:
return
self.result_rows = []
c = 0
for person_row in self.all_matches:
self.make_row_list_for_search_results_table(person_row, cur)
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 = tk.Label(self.search_table, cursor='hand2', anchor="w")
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_table.winfo_children():
if child.grid_info()['row'] not in (0, 1):
for evt_stg in ("<Button-1>", "<Return>", "<Key-space>"):
child.bind(evt_stg, self.change_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)
self.maxsize(
int(self.winfo_screenwidth() * 0.90),
int(self.winfo_screenheight() * 0.90))
self.search_input.focus_set()
configall(self.search_table, self.formats)
def change_current_person(self, evt):
width, height = self.tree.geometry().split("+", 1)[0].split("x")
screen = tk.Frame(
self.tree, bg=self.formats["bg"], width=width, height=height)
screen.grid()
conn = sqlite3.connect(self.tree.file)
conn.execute("PRAGMA foreign_keys = 1")
cur = conn.cursor()
current_name = None
persid = None
if evt.widget.grid_info()['row'] == 0:
return
self.hilit_row = evt.widget.grid_info()['row']
for child in self.search_table.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):
current_name = child['text']
# Append old value of persid to stack before updating
# persid to its new value.
self.tree.forward_stack = []
self.tree.backward_stack.append(self.tree.persid)
# click name or ID in table to change current person
if evt.type == '4':
if (evt.widget.grid_info()['column'] == 0 and
evt.widget.grid_info()['row'] == self.hilit_row):
persid = int(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_table.winfo_children():
if (child.grid_info()['row'] == self.hilit_row and
child.grid_info()['column'] == 0):
persid = int(child['text'])
if evt.type != '4':
persid = int(evt.widget['text'])
self.close_search_dialog()
self.tree.persid = persid
rdw = Redraw(main=self.main, tree=self.tree, formats=self.formats)
rdw.redraw_person_tab()
rdw.redraw_names_tab()
TabBook.resize_scrolled_dialog_with_tabbook(
self.tree, self.main.canvas, self.main)
cur.close()
conn.close()
screen.destroy()
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_table.winfo_children():
if child.grid_info()['row'] in (0, 1):
pass
elif child.grid_info()['row'] == self.hilit_row:
child.config(bg=self.formats['highlight_bg'])
def unhighlight_on_unfocus(self, evt):
self.unhilit_row = evt.widget.grid_info()['row']
for child in self.search_table.winfo_children():
if child.grid_info()['row'] in (0, 1):
pass
elif child.grid_info()['row'] == self.unhilit_row:
child.config(bg=self.formats['bg'])
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_table.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, cur):
""" 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 name_tip.
"""
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 = get_name_from_id(self.found_person, cur)
self.row_list.append(self.display_name)
ext = [[], [], '', '', '', '', '', [], [], []]
self.row_list.extend(ext[0:])
# this has to run last
self.get_values(cur)
def get_values(self, cur):
self.get_death(cur)
self.get_birth(cur)
if self.other_names:
self.get_other_names()
else:
self.row_list[9] = ''
self.make_sorters(cur)
def make_sorters(self, cur):
self.row_list[6] = self.get_sort_names(self.row_list[0], cur)
if self.offspring_event:
self.row_list[10] = self.make_sorter_for_formatted_dates(
self.row_list, cur)
self.row_list[11] = self.make_sorter_for_formatted_dates(
self.row_list, cur)
def make_sorter_for_formatted_dates(self, row, cur):
cur.execute(select_event_sorter, (self.offspring_event,))
sorter = cur.fetchone()
if sorter:
sorter = sorter[0].split(",")
sorter = [int(i) for i in sorter]
else:
sorter = [0,0,0]
return sorter
def get_sort_names(self, subject, cur):
cur.execute(select_name_sort_order, (subject,))
sort_name = cur.fetchone()
if sort_name:
sort_name = sort_name[0].lower()
elif not sort_name:
sort_name = ''
return sort_name
def get_death(self, cur):
cur.execute(select_person_death_date, (self.found_person,))
death_date = cur.fetchone()
self.death_date = ['-0000-00-00-------', '']
if death_date is None:
self.row_list[3] = self.death_date
return
storable_date = death_date[0]
self.death_date = [
storable_date,
format_stored_date(storable_date, date_prefs=self.date_prefs)]
self.row_list[3] = self.death_date
def get_birth(self, cur):
cur.execute(select_person_birth_date, (self.found_person,))
birth_date = cur.fetchone()
self.birth_date = ['-0000-00-00-------', '']
if birth_date is None:
self.row_list[2] = self.birth_date
return
storable_date = birth_date[1]
self.birth_date = [
storable_date,
format_stored_date(storable_date, date_prefs=self.date_prefs)]
self.row_list[2] = self.birth_date
self.offspring_event = birth_date[0]
self.get_ma(cur)
self.get_pa(cur)
self.row_list[7] = self.get_sort_names(self.ma_id, cur)
self.row_list[8] = self.get_sort_names(self.pa_id, cur)
def get_ma(self, cur):
name = ""
cur.execute(select_event_mother, (self.offspring_event,))
mom = cur.fetchone()
if mom:
self.ma_id = mom[0]
else:
self.ma_id = None
if self.ma_id is not None:
name = self.tree.person_autofill_values[self.ma_id][0]["name"]
self.row_list[4] = name
def get_pa(self, cur):
name = ""
cur.execute(select_event_father, (self.offspring_event,))
pop = cur.fetchone()
if pop:
self.pa_id = pop[0]
else:
self.pa_id = None
if self.pa_id is not None:
name = self.tree.person_autofill_values[self.pa_id][0]["name"]
self.row_list[5] = name
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
def show_nametip(self):
""" 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 name_tip will show that Alice's nickname is
Daisy.
"""
maxvert = self.winfo_screenheight()
if self.name_tip or not self.nametip_text:
return
x, y, cx, cy = self.widget.bbox('insert')
self.name_tip = d_tip = tk.Toplevel(self.widget, bd=1)
label = tk.Label(d_tip, text=self.nametip_text, justify='left', bd=0,
bg=NEUTRAL_COLOR, fg="black")
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.name_tip
self.name_tip = None
if d_tip:
d_tip.destroy()
def make_nametip(self):
self.widget.bind('<Enter>', self.handle_enter)
self.widget.bind('<Leave>', self.on_leave)
def handle_enter(self, evt):
self.pointed_to = evt.widget
pointed_row = self.pointed_to.grid_info()['row']
for child in self.search_table.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()