docs.txt
Dec 25, 2023 22:16:56 GMT -8
Post by Uncle Buddy on Dec 25, 2023 22:16:56 GMT -8
D:\treebard\etc\docs.txt Last Changed: 2024-07-18
# user_docs.txt
CONTENTS:
REPRESENTING RELATIONSHIPS AMONG ELEMENTS OF GENEALOGY IN A SQL DATABASE
PLACES
PLACE AUTOFILLS
PLACES IN THE DATABASE TABLES
GENDER
TKINTER SCROLLBARS
COLORIZER SCROLLING
PERSON AUTOFILL WIDGETS
COMMON PROBLEMS THAT ARE HARD TO FIND BUT EASY TO FIX
REPRESENTING RELATIONSHIPS AMONG ELEMENTS OF GENEALOGY IN A SQL DATABASE
The elements of genealogy are those things which get their own database table, for the most part, and are endowed with unique ID numbers. The primary elements of genealogy are those things that pre-exist genealogy, like persons, places, and sources. Some elements of genealogy, such as couples and other relationships, are compound elements because they are comprised of two or more elements.
A database table exists for each of the elements so that each unique member
of each element category will have its own primary key or unique ID number
within that category. To link two elements together, they are in the same row of a database table. Two data items are in the same row literally, in the case of a one-to-one relationship such as person-to-gender. "Mary" and "female" go in the same row of the person table.
But in the case of person-to-name, that's a one-to-many relationship, so person and name each need their own table in the database. A foreign key from the person table will be used in a person_id column of the name table. But how do you know that it shouldn't be the other way around? Why can't you put the name_id in the person_table? The key to it is that one person can have many names, but each name refers to only one person. So the primary key from the person table is used as a foreign key in the name table as many times as necessary. It won't work the other way around.
The definition of a relationship between two data types is its "cardinality". The cardinality types are one-to-one, one-to-many, and many-to-many.
Assertions are basic genealogy elements which exist separately from the genealogist's conclusions. The genealogist's conclusions are OF genealogy, they are secondary since they don't pre-exist genealogy. They can exist without evidence but can optionally be influenced by assertions. Assertions--what a source says--exist because a citation--a place within a source--exists. Citations can't exist without sources. Since we're talking about what pre-exists what, here is the progression, according to the real world:
--source must exist in order for
--citation to exist, and citation must exist in order for
--assertion to exist
A source can have many citations, but each citation refers to only one source. A citation can yield many assertions, but each assertion is related to only one citation. So source-to-citation and citation-to-assertion are both one-to-many relationships. To represent a one-to-many relationship, a SQL database has a table for each element, and the foreign key for the one side of the relationship is used in a column in the many side of the relationship. So the citation table has a column for foreign keys from the source table. The assertion table has a column for foreign keys from the citation table. This is the right way to represent the data relationships in a database table schema, in order to mirror the structure of the real world.
Since the assertion is what the source says at the citation, we have to ask what is the topic of what's being said by the assertion. An assertion is a claim, a statement of fact or supposed fact, about something. What is this something? Assertions often make statements about the elements event, attribute, and name. We can lump events and attributes together in the database table called "event". In the user interface we can separate them but that's another discussion. As data they can be treated exactly the same, so they should be.
Once again we have to determine cardinality before we know where to put the foreign key. Does event_id go in the assertions table, or the other way around?
An event has several parts, including date, place, age, particulars, and role, about which an assertion can be made. Since a conclusion is about one of these items, an assertion has to be about one of these items. We treat these assertion types the same in some ways, because they're just text from the source--literal or paraphrased--but in other ways they need to be stored separately, because the user will want to be reminded of how he came to a conclusion about a place OR a date, not both. A source might be wrong about the place but right about the date, so assertions are evaluated separately and can't be lumped together.
To answer the question about cardinality, an event can be linked to any number of date assertions, any number of place assertions, etc., but each assertion refers to only one event. So the cardinality of event-to-assertion is one-to-many. Therefore the event_id has to be used as a foreign key in the assertion table. While a name is a sort of special attribute of a person, it's not like other attributes. Person names are their own element in genealogy, so they get their own table. Each name gets its own primary key. A person who has a birth name and two aliases will have one person ID and three name IDs. The person ID is repeated in three rows of the name table. Primary keys can exist only once in their own table. So getting this backwards doesn't work.
The cardinality of name-to-assertion is the same as event-to-assertion, and this will be the same for other elements that assertions can make statements about. So the assertion table has a column for name_id and a column for event_id, and either of these foreign key columns--but not both--requires a reference for each assertion.
I don't know if you can tell just by reading this, but once you've tried to do it in a real database, you find that there is no way to accurately mirror the real world in a database about genealogy sources without an assertion table. The assertion text itself can be left blank when there's no confusion for vying assertions to try and clear up, and who wants to say a bunch of obvious things anyway? But the assertion table and its columns and rows must exist in order to correctly link events and names to sources and citations. Anything else is pretending based on assumptions, which are just glossed over information, cherry-picked for convenience. That is the state of the genieware industry, and that's why Treebard and UNIGEDS exist. As a non-commercial entity, the Treebard project has taken the time to get UNIGEDS right.
For an example of many-to-many relationships, we can look at notes. When information doesn't fit in another category, a note can take up the slack. In other genieare, I used to have to copy and paste notes from element to element, so in UNIGEDS I created a proper schema for notes so any note could be linked to any number of elements and any element could be linked to any number of notes. That's an example of a many-to-many relationship. Most columns in many-to-many tables (or "junction tables") are foreign key columns. In UNIGEDS there's a notes_links table with an obligatory column for note_id's foreign key and several other columns such as source_id and name_id so that any element can be linked to any note by putting that element in the same row of notes_links as the foreign key for the note_id.
PLACES
Unlike other genieware providers, your friendly neighborhood Treebard developer doesn't dumb down places to make his job easier. Since the first day of Treebard development, we have obsessed over detail upon detail to make our place data structure, input, and usage relevant to the most finicky historian while still being easy to use for beginning genealogists.
In Treebard, there are three different primary ID numbers which refer to places. Every single place has a unique ID number so that two different places can have the same name, like "Paris, France" and "Paris, Texas". This requires us to have separate IDs for every place name too: each "Paris" name has a unique ID number since they can't be told apart by their spelling. But for the most part, we don't just want to say "Paris" in genealogy at all; we want to say "Paris, Ile de France, France" or "Paris, Lamar County, Texas, USA". On top of that, any of the many Parises on earth is likely to have had different enclosing parents during its life span, so for example we need unique IDs for the nested places "Dallas, Texas, USA" and "Dallas, Republic of Texas". Then, if we want to include the county, the same single place "Dallas" will be included in separate unique nestings for the various counties it's in, now and in the past.
As your humble self-taught designer of genealogy software which intends to do complex work behind the scenes while providing a user interface that's easy to learn and easy to master, I've decided that the best approach is generally to prevent the user from seeing ID numbers for places. Juggling the notions of single place IDs, place name IDs, and nested place IDs is for the software developer, not for the software user. I propose to avoid the confusion that would ensue by not mentioning place IDs in the user interface. To the user, "123 Static Boulevard, Country Village Township, Bailing Wire County, Indiana, United States of America" is one place. For the genieware developer, treating this as one place would just be an excuse.
There still has to be a way for the user to tell two same-named single places apart. One way is to look at other nestings that the single place is already a part of. These are given as examples on the New & Duplicate Places Dialog, which opens up more often that some users will like since Treebard tries to prevent mistakes of this kind before they happen. Another feature of this dialog is (should be--the feature isn't currently finished) that you can assign a hint to a place or edit an existing hint. Here's a simple hypothetical example, not meant to be perfectly accurate. The place that westerners call "Israel" has had dozens of names over the millenia of its existence, with many of these places defined by different boundaries and governments. As an over-simplified example, you could input two separate places named "Israel", providing one with a hint, "modern Israel" and the other with a hint "ancient Israel". As long as the HINTS are unique and relevant, you'll have no trouble figuring out which Paris the hint "Eiffel Tower" refers to when it displays instead of a confusing ID number.
Treebard has made the decision to use nested places as much as possible for everything, allowing as little interaction as possible of the user with the single place i.e. place_id. Our places routine is as complex as it needs to be in order that serious researchers can get satisfaction from trying to track places as they existed historically in the real world. It's not that complicated, but it is complicated enough to be a can of worms when you try to let the user in on it. We need to shield the user from knowing anything about place IDs as much as possible, and let the user not care what's going on behind the scenes. So we give the user nested places, and that's what the user interacts with. Eg: when the user creates a new place outside of the events table, it's in the places tab. The user creates a nested place here, same as he would in the events table, and it works exactly the same except that the place being created is not linked to an event.
Don't auto-create nestings in the nested_place table. Based on experience, for example, there should be no single nestings added except for countries. That's how they'd be entered anyway, as singles. There should be no auto-made nestings, they're not needed. If there's a single nest for Lima, then it doesn't fill in "Lima, Peru", it stops at Lima and you have to go to the end of the autofilled "Lima", and then type ', P' after Lima to get Lima, Peru, to fill in, even if it's the only Lima. This defeats the purpose of using autofills, so the general idea is that the more stuff is in the collection of nested strings, the more characters the user will have to type before his intended target fills in. So only the user should add places.
All names of countries and other largest-enclosing-nests--as well as all nested place strings in general--have to be unique. Treebard has no way of telling the difference between "Congo" and "Congo" if they are two different countries with the same name. Research will turn up unique names for the two countries. On the other hand, "Timbuktu, Africa" and "Timbuktu, California" are not a problem. (I know, they're not really spelled the same, this is just for illustration.)
Don't fall for the temptation to add all countries to the database to save the user the trouble. We need consistent policies applied consistently. Most users will research regarding only a handful of countries and we are anti-bloat. The autofill feature is important and the user should be the only one who inputs a place, since the more places exist in the database, the more characters will have to be typed to get the desired place to fill in. And the user gets to make the decisions, for example how to spell a place, whether to abbreviate long place names, how to name a place, whether to enter Israel in Hebrew characters, whether to call ancient Israel and modern Israel by the same name, etc.
PLACE AUTOFILLS
As for whether you can input names with their historically accurate alphabets, I haven't had time to try it yet, and you can inform me whether or not it's possible to do that. I think Python and Tkinter have the ability to use pretty much any alphabet on earth and possibly can be trained to use ancient hieroglyphics if you are descended from Cleopatra, but I have yet to delve into this topic in my research. Characters are one of my weak areas but I believe Python is up to snuff for anything reasonable that needs to be done, if someone has time to do it.
There are other autofill fields in Treebard, but the nested place autofills have a special feature. When you input a place to an autofill field, that place is moved to the front of the hit list so it will be the first suggestion next time you start typing a word that starts with the same letters. For example, if there is a place called Zamora and a place called Zimbabwe in your tree, and you type a "z", then "Zamora" will fill in. If you then type an "i", then "Zimbabwe" will fill in. If you accept Zimbabwe, then next time you type a "z" in an autofill field, "Zimbabwe" will fill in first, not Zamora. This feature will save the user even more typing than the plain autofill.
PLACES IN THE DATABASE TABLES
When creating, deleting, or updating a place, up to eight database tables need to be taken into account and place lists needs to be updated if the app is not being redrawn. For a model of how some of this can be handled with code, see `places.make_new_place` which handles the tables `place`, `place_name`, `nested_place`, `main_tbd`, and `traits_tbd`. Additionally, the tables `event`, `notes_links`, and `current_tbd` have to be considered. Table names with '_tbd' appended are unique to Treebard, the others are part of UNIGEDS. Immutable keys #1 exist for unknown place_id, place_name_id, and nested_place_id. By "immutable", I mean the developer is not supposed to let the user delete or edit these. They're currently used by the code somewhere.
--`place`: rows are keyed to `place_id` and characteristics unique to a single place such as Paris are stored, but place names are not unique; latitude and longitude are unique.
--`place_name` stores a foreign key for `place_id` and a name string.
--`nested_place` stores up to 9 levels of nested `place_id` foreign keys in `nest0` thru `nest8`. Main use is to create autofill nested place strings such as "Paris, France" and "Paris, Texas" in which nest0 will be unique for the two Parises. Unused nests can't be null, must be 1.
--`main_tbd.place_name_id` stores a foreign key for the main name used by a single place. If `place_id` = 5 for both "USA" and "United States", then the place_name_id for one of those two names goes in `main_tbd.place_name_id` to tell Treebard which name to display. If a place has no place_name_id stored in main_tbd, the code won't work.
--`traits_tbd` stores a `hint` for each single place. The hint helps the user decide which "Israel" he wants to use if one hint is "ancient" and the other hint is "since 1948". Hints are not optional so are auto-generated as the timestamp for when the place was created. A feature needs to be added for users to change the auto-created hint if desired.
--`event` stores a foreign key for `nested_place_id` which is 1 by default, never null.
--`current_tbd` has only one row and includes a field for `nested_place_id`. This foreign key will tell the places tab which place_id to display description and images for, by consulting `nest0` of that `nested_place_id`. Completing the code for the places tab involved making the realization that a delineation between nested places and single places should not be presented to the user, it will just confuse him. Almost all user interaction is with nested places, so all place inputs will work the same. Behind the scenes, depending on what's being done we might be working with either or both a single place and a nested place.
--`notes_links` rows always have a foreign key for a note and can contain a foreign key for a `place_id` or a `place_name_id`. If a single place is added, nothing needs to be done automatically, but if a single place is deleted, its `place_id` and all that place ID's related place name IDs have to be deleted from `notes_links`.
In regards to GUI design, the goal is for the user to never see all this complexity.
GENDER
Nobody's gender ideology is built into Treebard including mine, if I have any.
Treebard records four possible genders: male, female, other and unknown. We recognize biologically-imposed categories but don't use them to enforce anything except the roles of biological mother and father in regards to offspring. For example, on the persons tab, the father is designed to display on the left and the mother on the right, since that's where people will look for them. This doesn't affect adoptive parents, foster parents, or guardians since they're not biological parents. Treebard won't even notice if there are two female foster parents or if an adoptive male is input on the right. Even the biological parent will display on the side where he/she was input by the user, despite the gender. The user could literally input a female as father and a male as mother if he/she/it/they chose to do so for whatever reason.
As for the current (2024) gender hoopla, Treebard (the app) has no opinion and is not involved with gender issues other than to remain as flexible as possible under the circumstances. We (the developer) were youthful and omniscient once, and we try to flex to the demands of those who still are. Flex but not break. The evangelists of today's trends will get old someday too. Some of them. Disillusionment is enlightenment, but try telling that to those who have yet to earn their Stupidity Merit Badge through gruelling decades of hard knocks and unfulfilled delusions. Treebard's overarching policy on fashions and trends--both political and computerish--is, "Which part of genealogy is supposed to be modern and politically enlightened, anyway?"
Treebard is about accuracy and ease of use for genealogists of all skill levels, all cultures, all political and religious persuasions, etc. And if we were to cater to the whims of passing fancy (even if those whims are considered gospel by some), we would have no time left to do our real work, which is to serve the interest of genealogists in general. If we were to cater to the whims of political opinion, we would be torn to pieces within minutes no matter which side we chose. In spite of our advanced age and encroaching senility, we make Treebard as liberal as possible without burning it to the ground. In case anyone has forgotten, the dictionary meaning of "liberal" is "generous", and the dictionary meaning of "conservative" is "careful". So Treebard is both of these things, according to the dictionary, but neither of these things, politically.
TKINTER SCROLLBARS
The purpose of this section is to tell how to make a canvas and scrollbar do different things under a variety of circumstances.
I wrote this because I needed a cheat sheet, not because I'm an expert.
I. MAKE SCROLLBARS:
sbv = Scrollbar(
toplevel,
command=canvas.yview,
hideable=True)
canvas.config(yscrollcommand=sbv.set)
sbh = Scrollbar(
toplevel,
orient='horizontal',
command=canvas.xview,
hideable=True)
canvas.config(xscrollcommand=sbh.set)
A scrollbar and its canvas are always siblings, e.g. in the above example,
the parent of the canvas would also be `toplevel`.
The class is a custom "Toykinter" widget based on the Tkinter API so using
it is almost identical to using the Tkinter scrollbar except that it can be
easily configured like any Tkinter widget instead of using Windows system
colors. Also it is optionally hideable; default for that option is False.
The complication with the hideable scrollbar is that it needs a place to be
when it appears, so an offset--a blank space the same size as the hidden
scrollbar--can be added to the required size of the window. Then a spacer
can be added to the north and west edges of the window to balance this out.
These procedures increase the size of the window to prevent the scrollbar
from appearing before it's needed. The offset spacer or scrollbar width is
what I call "scridth".
II. CANVAS, SCROLLBAR AND WINDOW SIZING:
There are several things that can have dimensions so I think of them as a stack
with the toplevel (toplevel root or dialog) on bottom:
toplevel
scrollregion
canvas
window (content frame)
The canvas is a widget, gridded, packed or placed like any other widget.
What I'm calling a "content frame" is a single frame covering the whole
canvas, so that
when the canvas is scrolled, the effect is that the content and all its
widgets are being scrolled. Since this frame is not gridded but created
by canvas.create_window(), in my code where it says 'window' this should be
a reference to a content frame in a canvas. If there will be objects drawn
on the canvas instead of widgets in a content frame, give the canvas a size
with its width and height options. But if there will be a content frame,
ignore the canvas width and height options.
The scrollregion can be visualized as an area behind the canvas, at least
as large as the canvas, which can be slid around with only part of it
visible at one time. The scrollregion can be panned by dragging with the
mouse or arrow keys, or scrolled with scrollbars or the mousewheel.
A. RESIZABLE CANVAS
The root window and some toplevel windows might have dynamically varying
contents. The scrollregion is set to autosize to all the canvas' contents
(bounding box or bbox), which is just the content frame in this case.
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
canvas = Canvas(root)
content = Frame(canvas) # don't grid
canvas.grid(column=0, row=0, sticky='news')
content_window = canvas.create_window(0, 0, anchor='nw', window=content)
canvas.config(scrollregion=canvas.bbox('all'))
def resize_canvas(event):
root.update_idletasks()
if event.width > content.winfo_reqwidth():
canvas.itemconfigure(content_window, width=event.width)
canvas.bind('<Configure>', resize_canvas)
The toplevel could also be resized this way when its contents change. This way
instead of resizing automatically when contents change, you have to know when
contents will change and call the function at that time:
def resize_scrolled_content(toplevel, canvas):
toplevel.update_idletasks()
canvas.config(scrollregion=canvas.bbox('all'))
page_x = canvas.content.winfo_reqwidth()
page_y = canvas.content.winfo_reqheight()
toplevel.geometry('{}x{}'.format(page_x, page_y))
B. NESTED CANVAS WITH A FIXED SIZE
Within a toplevel whether it's got its own full-size scrolled area or not,
a smaller scrolled area of a fixed size could be contained. In this case,
the scrollregion doesn't get set to bbox('all') but to a fized size at
least the size of the canvas. The resizing methods are not needed here.
canvas = Canvas(root, width=500, height=750)
content = Frame(canvas) # don't grid
canvas.create_window(0, 0, anchor='nw', window=content)
canvas.config(scrollregion=(0, 0, 900, 1000))
Instead of hard-coding the width and height of the scrollregion, the
required width and height of the canvas contents can be detected so
the size of the scrollregion is the exact size it needs to be. The
canvas width and height should be set to any size smaller than the
scrollregion but won't go below a minimum size that's built into Tkinter.
C. DROPDOWN WINDOW WITH A FIXED WIDTH (check current combobox code for best example.)
A toplevel without a border can be used as a dropdown window, for example
in a custom-made combobox, and provided with a vertical scrollbar. Its
width will be fixed to that of a host, for example the entry widget in a
combobox. The scrollregion height will be calculated to fit the vertical
contents. The window height is left to resize to its contents.
In this example, self is the combobox, i.e. a frame that holds the combobox
entry and arrow.
host = self.Entry(self)
host.grid()
host_width = self.winfo_reqwidth()
self.window = self.canvas.create_window(
0, 0, anchor='nw', window=self.content, width=host_width)
self.canvas.config(scrollregion=(0, 0, host_width, self.fit_height))
D. MOUSEWHEEL SCROLLING
Dialogs which inherit from the ScrolledDialog class add themselves to
`ScrolledDialog.main_canvases` so they will scroll with the mousewheel.
The root app (root) and any other scrolled dialog that doesn't
inherit from ScrolledDialog have to be added, e.g....
`ScrolledDialog.main_canvases.append(root.canvas)`
...and scrolled areas that are not dialogs have to be added, e.g....
`ScrolledDialog.nested_canvases.append(canvas)`
...and this has to be called once for content created on load...
`ScrolledDialog.bind_canvases_to_mousewheel()`
...and this has to be called for each new scrolled dialog that did not
exist when the app loaded...
`ScrolledDialog.bind_canvas_to_mousewheel(self.canvas)`
COLORIZER SCROLLING
Re: `yview_moveto` using only two settings (0.0 or 1.0) for
"auto-scrolled all the way up" or all the way down. The user
can scroll manually to any increment, but when traversing
the schemes with the arrow keys, the scrolling is automatic
so the user doesn't have to grab the mouse. This feature is
currently limited to either all up or all down.
To accomodate a lot more color schemes than the 60-plus that
I've already tested with the auto-scrolling feature, it
would be necessary to use positions between the 0.0 and 1.0
currently being used. As it is now, the user is limited to
two pages of swatches, three rows each. Unless the size of the
canvas is increased to four rows of swatches per page; then it
would be two pages of swatches, four rows each.
See the scrollbar in the custom_combobox_widget.py for an
example of proportional autoscrolling being done
with the Toykinter scrollbar, but who needs that many color
schemes anyway? Especially since any and all of the built-in
color schemes could be deleted, then the user could make his
own dozens of schemes. And actually, there's no real limit to
how many schemes there can be, if the user doesn't mind
manually scrolling when there are more than six rows of
swatches.
PERSON AUTOFILL WIDGETS
There's a quirk in the system for autofilling names that will rarely if ever show up, and seems to be harmless so far when it shows up in the Change Current Person field at the top of the app. If you type a "z" and there are only two names--say "Zachary Dupree"--that start with a "z" and both names are exactly the same, the duplicate name chooser will open as soon as you type the "z", asking you to select which Zachary Dupree you wanted. This is fine if you wanted to use Zachary Dupree but not if you wanted to enter Zoe Blankenship for the first time to create a person by that name. The glitch is harmless and the workaround is simple. Cancel out of the duplicates dialog, delete EVERTHING BUT THE Z that autofills, and finish typing Zoe's name. Everything will work as expected. This could be "fixed" with several lines of code but it will happen so rarely that Treebard prefers the workaround, which possibly anyone could figure out if they weren't reading this. As soon as there are two differently spelled names in the database--i.e. one other name that starts with a "z" besides the two "Zachary Duprees"--the problem will never occur again when typing a "z". To test this with the sample_tree.tbd file, delete any name starting with a "z" except for the two instances of Zachary somebody, and follow the instructions above.
COMMON PROBLEMS THAT ARE HARD TO FIND BUT EASY TO FIX:
A. HOW TO CLEAN UP AFTER MANUALLY DELETING A BUNCH OF EXPERIMENTAL TREES. Sometimes they can't be deleted one at a time within Treebard because of errors, or because you don't want to delete them one at a time. Treebard is a portable app and the DELETE CURRENT TREE command in the menu doesn't use the Windows Recycle Bin. The files go away permanently, so this is also a way to have the files go into the Recycle Bin where they could be recovered if necessary. Cleaning up is a simple 3-step process:
1. Delete the folders named the same as the trees. e.g. in <current drive>/treebard/data/testing_001/testing_001.tbd you'd want to delete the directory testing_001.
2. In the SQLite command line interface, open <current drive>/treebard/data/settings/tbard.db and run a query to delete the unwanted file references. EG if you want to delete all but the sample_tree (which should never be deleted), and one other tree, run a query like this:
delete from family_tree where family_tree_id not in ('sample_tree', 'sample_tree_expimported');
3. Open <current drive>/treebard/data/settings/config.txt and delete unwanted trees from the line that starts `recent_files,` so e.g. this is what's left: `recent_files, Sample Tree_+_Sample Tree ExpImported`. ALTERNATELY you can just delete the config.txt file and it will be recreated in its default form next time Treebard starts.
B. THE PROGRAM REFUSES TO START BECAUSE SOME DATA IS MISSING
This often is caused by a mistake that leaves a null value in *.tbd's current_tbd.person_id field. Manually update the column with any valid person_id for the tree.
C. Tkinter errors like...
...File "C:\Users\<user>\AppData\Local\Programs\Pyth...\Lib\tkinter\__init__.py", line 1711, in _configure
self.tk.call(_flatten((self._w, cmd)) + self._options(cnf))
_tkinter.TclError: unknown option "-fg"
...usually mean that a widget class has not been listed in one of the
`styles` dictionary keys in widgets.py. This is an idiosynchracy of Treebard's colorizer for
which Python's error message is not very meaningful.
D. CHANGES TO UNIGEDS
I do all my work in sample_tree.tbd. When I change the database schema, I do it in sample_tree.tbd because 1) it has the tables ending in _tbd, without which Treebard won't work, and 2) unigeds.db has no values in its tables and has to stay that way. Any change in a table schema (except in tables ending in _tbd) then needs to be made in unigeds.db when tested and approved. Here is the easy way, so the queries don't have to be run over again.
The tables in `unigeds.db` are completely blank so we don't have to
remember which tables have default rows in them. Default values are added,
edited, and remembered in this module. These functions run every time the
user creates a new tree, since the default new tree is now completely
blank. This code is run in `make_tree()` which is in `opening.py`.
When a table schema is altered while developing in `sample_tree.db`,
do this to make a whole new `unigeds.db` in a few easy steps:
************FIRST DELETE `unigeds.db` and `unigeds.sql`***************
C:\Users\Lutherman>sqlite3 d:/treebard/data/sample_tree/sample_tree.tbd
SQLite version 3.34.0 2020-12-01 16:14:00
sqlite> .output d:/treebard/data/settings/unigeds.sql
sqlite> .schema
sqlite> .exit
C:\Users\Lutherman>sqlite3 d:/treebard/data/settings/unigeds.db < d:/treebard/data/settings/unigeds.sql
Error: near line 1: object name reserved for internal use: sqlite_sequence
(I DON'T KNOW what the error is but the import of the schema seems to work fine.)
IMPORTANT FINAL STEP: Since this new version of unigeds.db is being created by copying
sample_tree.tbd, it will include main_tbd, current_tbd, and traits_tbd. These tables
should be dropped since they should be created by new_tree.py and should not exist
in unigeds.db:
sqlite> drop table main_tbd;
sqlite> drop table traits_tbd;
sqlite> drop table current_tbd;
E. PREVENT FLASHING
1. When opening a dialog, if it shows as a bright white flash till the widgets are colorized, this can be mostly prevented by keeping the dialog small so its bright white background is not seen when it's first created: `dialog.geometry("0x0+0+0")`.
2. When redrawing an area such as a table, the widgets are created white and then colorized. This slims the code down considerably vs. formatting widgets in their class definitions, and they're going to be reformatted anyway by `configall`. To hide the flashing of widgets as they're being created, put up a screen to cover the app or a portion of it until the widgets are all made and formatted, then destroy the screen:
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()
self.make_widgets()
self.make_inputs()
configall()
resize_scrolled_content()
screen.destroy()
This is a much bigger issue than what it sounds like, because drawing all the widgets on the computer screen and then redrawing them the right size and color takes literally 3 to 4 times longer than drawing them behind a `screen`. This is because if there's a `screen`, they aren't actually drawn at all until the computer has stopped thinking about it and the `screen` is destroyed. Tkinter draws only what's on top, there's no way to draw two layers of widgets on a computer screen.
3. When the app first opens, a Toplevel widget is used in full screen mode to cover everything till the app is ready to show. An error while this screen is up will force you to close the app with Windows Task Manager a.k.a. the three finger salute.
F. COLORIZING TOOLTIPS
The Michael Foord tooltips used mainly on the icon ribbon menu are not configurable with Treebard's Colorizer class, because the Tooltips class is not a widget.
Tooltips that are in a dialog (e.g. `name_tip` in the PersonSearch class) are easier to colorize than tooltips in a permanent window (e.g. `kin_tip` in the EventsTable class and `id_tip` in the NuclearFamiliesTable class). This is because dialogs are created using current formats dictionary values while tooltips in a permanent window are created using the formats dictionary that was current when the permanent window was created.
This problem has been worked around by using a neutral color for all tooltips except for the statusbar tooltips which are always black.
Pretty or bright colors aren't desirable for tooltips since the user doesn't always need them and the flashing when the mouse moves across the screen is distracting. If the user prefers pretty or bright colors for his app, the tooltips will still be NEUTRAL_COLOR or black.
G. GEDCOM IMPORT
If the user doesn't place images with the right names in the tree/image directory, the image galleries won't open and there will be a series of Python/Tkinter errors if the user clicks on a gallery to open it. So the user has a choice of 1) putting an image with the right title in the images directory for the tree (see the GEDCOM for the image file names), or 2) manually editing the GEDCOM to remove references to images.
I started writing code to deal with the problem but this is not Treebard's problem. It's a GEDCOM problem, and I won't bloat Treebard to work around GEDCOM. The right way to share a tree is to share the whole database. Every genieware vendor should have the same back end schema. That's good enough for me.