base.py
Jul 24, 2024 21:57:31 GMT -8
Post by Uncle Buddy on Jul 24, 2024 21:57:31 GMT -8
<drive>:\treebard\base.py Last Changed 2024-07-25
# base.py
from sys import argv
from os import (
getlogin, makedirs, path, rename, mkdir, listdir, remove, rmdir, getcwd)
from shutil import copy2
import dev_tools as dt
from dev_tools import look, seeline
CONFIG_DEFAULT = (
# miscellaneous user preferences
("placeholder_name", "_____ _____"),
# closing state
("prior_family_tree", "sample_tree"),
("openpic", "tree_scape_pexels.jpg"),
("recent_files", "Sample Tree"),
# directories
("look_first_dir_for_images", ""),
# images
("use_default_images", "1"),
("image_male", ""),
("default_image_male", "0_default_image_male.gif"),
("image_female", ""),
("default_image_female", "0_default_image_female.jpg"),
("image_unisex", ""),
("default_image_unisex", "0_default_image_unisex.jpg"),
("image_nested_place", ""),
("default_image_nested_place", "0_default_image_place.jpg"),
("image_source", ""),
("default_image_source", "0_default_image_source.jpg"),
# date formats
("date_formats", "alpha_dmy"),
("abt", "abt"),
("est", "est"),
("cal", "calc"),
("bef_aft", "bef/aft"),
("bc_ad", "BCE/CE"),
("os_ns", "OS/NS"),
("span", "from_to"),
("range", "btwn_&"),
# fonts--user preference
("font", "verdana"),
("font_size", "12"),
# widget formats
("default_bg", "#414141"),
("default_highlight_bg", "#646464"),
("default_head_bg", "#878787"),
("default_fg", "#c8c8c8"),
("default_font", "verdana"),
("default_font_size", "12"))
def get_path_parts():
""" Detect the full path of the running script and save its parts.
line 24 argv ['D:\\treebard\\treebard_root_037.py']
line 24 argv ['D:\\treebard\\build\\exe.win32-3.11\\treebard_root_037.exe']
line 24 argv
['C:\\Users\\Lutherman\\Desktop\\build\\exe.win32-3.11\\treebard_root_037.exe']
"""
argv_fix = argv[0].split("\\")
current_drive = argv_fix[0]
app_path = "/".join(argv_fix[0:-1])
filename = argv_fix[-1]
extension = filename.split(".", -1)[-1]
if extension == "exe":
app_path = f"{app_path}/lib"
image_path = f"{app_path}/images"
tree_path = f"{app_path}/data"
unigeds_path = f"{app_path}/data/settings/unigeds.db"
tbard_path = f"{app_path}/data/settings/tbard.db"
windows_user = getlogin()
trash_path = f"{current_drive}/treebard_backups/{windows_user}/trash"
if not path.exists(trash_path):
makedirs(trash_path)
backups_path = f"{current_drive}/users/{windows_user}/documents/treebard/backups"
return (
current_drive, extension, unigeds_path, app_path, backups_path, trash_path,
tree_path, image_path, tbard_path)
(current_drive, extension, unigeds_path, app_path, backups_path, trash_path,
tree_path, image_path, tbard_path) = get_path_parts()
class Database:
def __init__(self):
self.data = {}
self.persist_path = f"{tree_path}/settings/config.txt"
if not path.isfile(self.persist_path):
self.write_record()
self.make_db()
def write_record(self):
with open(self.persist_path, mode="w", encoding="utf-8-sig") as config:
config.write(f"config\n")
for tup in CONFIG_DEFAULT:
key, val = tup
config.write(f"{key}, {val}\n")
config.write(f"end_config")
def make_db(self):
""" Read data saved in text file. Make in-memory Python database for
select queries. This is also used in the `Query` class. If
config.txt becomes corrupted (for example there's a power outage
while the file is being written to), manually delete it and a
new one will be made with default values.
"""
with open(self.persist_path, mode="r", encoding="utf-8-sig") as config:
for line in config:
ln = line.replace("\n", "")
if ln == "config":
tail = f"end_config"
continue
elif ln == tail:
break
else:
k,v = ln.split(", ", 1)
self.update_dict(key=k, value=v)
def update_dict(self, key=None, value=None):
if self.data.get(key) is None:
self.data[key] = {}
self.data[key] = value
class Query:
""" Update and select rows from non-SQL 'tables' i.e. Python dicts. """
def __init__(self):
self.db = Database()
def select(self, key):
return self.db.data.get(key)
def update(self, key=None, value=None):
""" Change a value in the existing record of a .txt database.
Run code to recreate the in-memory Python dict database.
"""
with open(self.db.persist_path, mode="r", encoding="utf-8-sig") as edit:
lines = edit.readlines()
lines_list = [lines[0]]
for ln in lines[1:]:
lst = ln.split(", ", 1)
if lst[0] == key:
lst[1] = f"{value}\n"
fix = ', '.join(lst)
lines_list.append(fix)
else:
lines_list.append(ln)
with open(self.db.persist_path, mode="w", encoding="utf-8-sig") as go:
for ln in lines_list:
go.write(ln)
self.db.make_db()
def show(self, key):
print(f"self.db.data[{key}]:", self.db.data[key])
def show_all(self):
print("self.db.data:", self.db.data)
""" Slash convention:
Joining slashes--i.e. leading and trailing slashes--should always be
added by `join()` or in the f-string. Only included slashes e.g.
`dir_a/dir_b` can be hard-coded. So there's no guessing what was done.
"""
def get_image_dir():
query = Query()
return query.select("look_first_dir_for_images")
def resize_scrollbar(dlg, canvas):
dlg.update_idletasks()
canvas.config(scrollregion=canvas.bbox('all'))
""" DON'T USE THIS `resize_scrolled_content` FOR INSTANCES OF ScrolledDialog
which has its own similar method.
"""
def resize_scrolled_content(toplevel, canvas, window, add_x=0, add_y=0):
""" Besides configuring the scrollbar when the content changes, this
gives a hideable scrollbar a place to grid (scridth) so the
scrollbar doesn't appear before it's needed due to its own
width. Extra space or `scridth` is added where the hidden scrollbars
will appear. Extra spacer frames (such as `scridth_n` and `scridth_w`
in main.py) are added to balance this out (don't do any of this with
padding). The end result is a hideable scrollbar without a lop-sided
border around the canvas.
"""
def resize_window():
""" Don't try to DETECT scrollbar width (`scridth`) in this function.
For some reason it causes certain combinations of values
below to freeze the app. Hard-coded is good enough since there
are only a few sizes of scrollbar. Add 10 to the scrollbar width
and that gives it wiggle room (makes it work right--not sure why).
EDIT THIS, I GOT RID OF 2 SIZES OF SCROLLBAR...................
"""
toplevel.update_idletasks()
if toplevel.winfo_name() == 'tk':
bar_height = 27 # statusbar
scridth = 26
else:
bar_height = 96 # menubar + ribbon + statusbar
scridth = 30
page_x = window.winfo_reqwidth() + scridth + add_x
page_y = window.winfo_reqheight() + scridth + bar_height + add_y
toplevel.geometry(f"{page_x}x{page_y}")
resize_scrollbar(toplevel, canvas)
resize_window()
def resize_window(toplevel, frame):
""" Generic for unscrolled toplevel windows. """
toplevel.update_idletasks()
page_x = frame.winfo_reqwidth()
page_y = frame.winfo_reqheight()
toplevel.geometry(f"{page_x}x{page_y}")
def fix_tab_traversal(families_table, events_table):
""" Create a tab traversal since the nukefam_table
can't be made first, but should be traversed first.
"""
for table in (families_table, events_table):
table.lift()
def stop_flashing(dlg, scrollbar_width=None):
""" Call when configuring scrollregion if flashing occurs for tiny
images. Doesn't always work if called after configuring scrollregion,
seems more reliable when called before. This was done because the
other way to stop the flashing is to drag or resize the dialog.
"""
dlg.update_idletasks()
size, position = dlg.geometry().split("+", 1)
w, h = size.split("x")
x, y = position.split("+")
if scrollbar_width:
dlg.geometry(
f"{str(int(w)+scrollbar_width*2)}x{h}+{x}+{str(int(y)+1)}")
else:
dlg.geometry(
f"{str(int(w)+scrollbar_width*2)}x{h}+{x}+{str(int(y)+1)}")
def split_sorter(date):
sorter = date.split(",")
date = [int(i) for i in sorter]
return date
# capitalizE righT
def titlize(stg):
''' Function by Yugal Jindle. Python's `title()` method doesn't work right
if there are apostrophes etc. in the word, since it breaks words at
punctuation. According to https://bugs.python.org/issue7008, the
`string.capwords()` method is also buggy and should be deprecated. This
is to make "Linda's Tree" not be "Linda'S Tree".
'''
lst = []
for temp in stg.split(" "): lst.append(temp.capitalize())
return ' '.join(lst)
# CENTERING
def center_dialog(dlg, frame=None):
""" I think the `frame` parameter refers to the `create_window` method of
Canvas.
"""
if frame:
dlg.update_idletasks()
win_width = frame.winfo_reqwidth()
win_height = frame.winfo_reqheight()
right_pos = int(dlg.winfo_screenwidth()/2 - win_width/2)
down_pos = int(dlg.winfo_screenheight()/2 - win_height/2) - 50
else:
dlg.update_idletasks()
win_width = dlg.winfo_reqwidth()
win_height = dlg.winfo_reqheight()
right_pos = int(dlg.winfo_screenwidth()/2 - win_width/2)
down_pos = int(dlg.winfo_screenheight()/2 - win_height/2)
dlg.geometry("+{}+{}".format(right_pos, down_pos))
# # - - - - - - - - - - - - - - - #
# Tkinter keysyms for keys that actually type a character with len(keysym) > 1:
OK_PRINT_KEYS = (
"space", "parenleft", "parenright", "underscore", "minus", "asterisk",
"slash", "period", "comma", "equal", "plus", "ampersand", "asciicircum",
"percent", "dollar", "numbersign", "at", "exclam", "asciitilde",
"quoteleft", "bar", "backslash", "less", "greater", "colon", "semicolon",
"quotedbl", "quoteright", "bracketleft", "braceleft", "bracketright",
"braceright")
# # - - - - - - - - - - - - - - - #
# color strings recognized by Tkinter
COLOR_STRINGS = [
'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige',
'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown',
'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral',
'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan',
'darkgoldenrod', 'darkgray', 'darkgrey', 'darkgreen', 'darkkhaki',
'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred',
'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray',
'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue',
'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite',
'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod',
'gray', 'grey', 'green', 'greenyellow', 'honeydew', 'hotpink', 'indianred',
'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen',
'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan',
'lightgoldenrodyellow', 'lightgray', 'lightgrey', 'lightgreen',
'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue',
'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow',
'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine',
'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen',
'mediumslateblue', 'mediumspringgreen', 'mediumturquoise',
'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin',
'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange',
'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise',
'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum',
'powderblue', 'purple', 'red', 'rosybrown', 'royalblue', 'saddlebrown',
'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver',
'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen',
'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet',
'wheat', 'white', 'whitesmoke', 'yellow']
if __name__ == "__main__":
query = Query()
print("line", look(seeline()).lineno, "query.db.data", query.db.data)