"""
# **pysimplesql** User's Manual

## DISCLAIMER:
While **pysimplesql** works with and was inspired by the excellent PySimpleGUI™ project, it has no affiliation.

## Rapidly build and deploy database applications in Python
**pysimplesql** binds PySimpleGUI to sqlite3 databases for rapid, effortless database application development. Makes a great
replacement for MS Access or Libre Office Base! Have the full power and language features of Python while having the
power and control of managing your own codebase. **pysimplesql** not only allows for super simple automatic control (not one single
line of SQL needs written to use **pysimplesql**), but also allows for very low level control for situations that warrant it.
"""
#!/usr/bin/python3

# TODO: Make a list of controls to enable/disable along with edit_protect.  This would be a pretty cool feature

# The first two imports are for docstrings
from __future__ import annotations
from typing import List, Union, Optional, Tuple, Callable
import PySimpleGUI as sg
import sqlite3
import functools
import os.path
import logging
from types import SimpleNamespace ## for iconpacks
import pysimplesql ## Needed for quick_edit pop-ups

logger = logging.getLogger(__name__)

# ---------------------------
# Types for automatic mapping
#----------------------------
TYPE_RECORD=1
TYPE_SELECTOR=2
TYPE_EVENT=3

# -----------
# Event types
# -----------
# Custom events (requires 'function' dictionary key)
EVENT_FUNCTION=0
# Query-level events (requires 'table' dictionary key)
EVENT_FIRST=1
EVENT_PREVIOUS=2
EVENT_NEXT=3
EVENT_LAST=4
EVENT_SEARCH=5
EVENT_INSERT=6
EVENT_DELETE=7
EVENT_DUPLICATE=13
EVENT_SAVE=8
EVENT_QUICK_EDIT=9
# Form-level events
EVENT_SEARCH_DB=10
EVENT_SAVE_DB=11
EVENT_EDIT_PROTECT_DB=12

# --------------------
# PROMPT_SAVE Returns
# --------------------
PROMPT_DISCARDED = 0
PROMPT_PROCEED = 1
PROMPT_NONE = 2

# ------------------------
# RECORD SAVE RETURN TYPES
# ------------------------
SAVE_FAIL=0     # Save failed due to callback
SAVE_SUCCESS=1  # Save was successful
SAVE_NONE=2     # There was nothing to save

def strip(string:str) -> str:
    """
    Strips :x from string
    """
    return string.split(':')[0]

def eat_events(win:sg.Window) -> None:
    """
    Eat extra events emitted by PySimpleGUI.Query.update().

    Call this function directly after update() is run on a Query element. The reason is that updating the selection or values
    will in turn fire more changed events, adding up to an endless loop of events.  This function eliminates this problem

    :param win: A PySimpleGUI Window instance
    :type win: PySimpleGUI.Window
    :returns: None
    :rtype: None
    """
    while True:
        event,values=win.read(timeout=0)
        if event=='__TIMEOUT__':
            break
    return

def escape(query_string:str) -> str:
    """
    Safely escape characters in strings needed for queries

     .. note:: This is not yet implemented and is here in the case that it is needed in the future.

     :param query_string: The query to escape
     :type query_string: str
     :returns: An escaped string
     :rtype: str
    """
    query_string = str(query_string)
    return query_string

class Row:
    """
    This is a convenience class used by listboxes and comboboxes to display values while keeping them linked to a primary key.

    You may have to cast this to a str() to get the value.  Of course, there are methods to get the value or primary key either way.
    .. note:: This class is not typically used by the end user.
    """
    def __init__(self, pk, val):
        self.pk = pk
        self.val = val

    def __repr__(self):
        return str(self.val)

    def __str__(self):
        # This override is so that comboboxes can display the value
        return str(self.val)

    def get_pk(self):
        """Return the primary key portion of the row"""
        return self.pk

    def get_val(self):
        """Return the value portion of the row"""
        return self.val

    def get_instance(self):
        """Return this instance of @Row"""
        return self


class Relationship:
    """
    This class is used to track primary/foreign key relationships in the database.

    See the following for more information: @Form.add_relationship and @Form.auto_add_relationships
    .. note:: This class is not typically used the end user,
    """

    def __init__(self, join:str, child:str, fk:Union[str,int], parent:str, pk:Union[str,int], requery_table:bool) -> Relationship:
        """
        Initialize a new Relationship instance

        :param join: The join type. I.e. "LEFT JOIN", "INNER JOIN", etc.
        :type: str
        :param child: The table name of the child table
        :type child: str
        :param fk: The child table's foreign key column
        :type fk: Union[str,int]
        :param parent: The table name of the parent table
        :type parent: str
        :param pk: The parent table's primary key column
        :type pk: Union[str,int]
        :returns: A Relationship instance
        :rtype: Relationship
        """
        self.join = join
        self.child = child
        self.fk = fk
        self.parent = parent
        self.pk = pk
        self.requery_table = requery_table

    def __str__(self):
        """
        Return a join clause when cast to a string
        """
        return f'{self.join} {self.parent} ON {self.child}.{self.fk}={self.parent}.{self.pk}'


class Query:
    """
    This class is used for an internal representation of database queries/tables. These are added by the following:
    Form.add_table Form.auto_add_tables
    """
    instances=[] # Track our instances

    def __init__(self, name:str, frm_reference:Form, table:str, pk_column:str, description_column:str, query:Optional[str]= '', order:Optional[str]= '', prompt_save=True) -> Query:
        """
        Initialize a new Table instance

        :param name: The name you are assigning to this query (I.e. 'qry_people')
        :type name: str
        :param frm_reference: This is a reference to the @ Form object, for convenience
        :type frm_reference: Form
        :param table: Name of the table
        :type table: str
        :param pk_column: The name of the column containing the primary key for this table
        :type pk_column: str
        :param description_column: The name of the column used for display to users (normally in a combobox or listbox)
        :type description_column: str
        :param query: You can optionally set an inital query here. If none is provided, it will default to "SELECT * FROM {query}"
        :type query: str
        :param order: The sort order of the returned query. If none is provided it will default to "ORDER BY {description_column} COLLATE NOCASE ASC"
        :type order: str
        :param prompt_save: Prompt to save changes when dirty records are present
        :type prompt_save: bool
        :returns: A Table instance
        :rtype: Query
        """
        # todo finish the order processing!
        Query.instances.append(self)

        # No query was passed in, so we will generate a generic one
        if query == '':
            query = f'SELECT * FROM {table}'
        # No order was passed in, so we will generate generic one
        if order == '':
            order = f' ORDER BY {description_column} COLLATE NOCASE ASC'

        self.name=name
        self.frm = frm_reference  # type: Form
        self._current_index = 0
        self.table = table  # type: str
        self.pk_column = pk_column
        self.description_column = description_column
        self.query = query
        self.order = order
        self.join = ''
        self.where = ''  # In addition to generated where!
        self.con = frm_reference.con
        self.dependents = []
        self.column_names = []
        self.rows = []
        self.search_order = []
        self.selector = []
        self.callbacks = {}
        self._prompt_save=prompt_save
        # self.requery(True)

    # Override the [] operator to retrieve columns by key
    def __getitem__(self, key):
        return self.get_current(key)

    # Make current_index a property so that bounds can be respected
    @property
    def current_index(self):
        return self._current_index

    @current_index.setter
    def current_index(self, val):
        if val > len(self.rows) - 1:
            self._current_index = len(self.rows) - 1
        elif val < 0:
            self._current_index = 0
        else:
            self._current_index = val

    @classmethod
    def purge_form(cls,frm:Form,reset_keygen) -> None:
        """
        Purge the tracked instances related to frm

        :param frm: the form to purge query instances from
        :return: None
        """
        new_instances=[]
        selector_keys=[]

        for i in Query.instances:
            if i.frm!=frm:
                new_instances.append(i)
            else:
                logger.debug(f'Removing Query {i.name} related to {frm.db_path}')
                # we need to get a list of elements to purge from the keygen
                for s in i.selector:
                    selector_keys.append(s['element'].key)


        # Reset the keygen for selectors and elements from this Form
        # This is probably a little hack-ish, perhaps I should relocate the keygen?
        if reset_keygen:
            for k in selector_keys:
                keygen_reset(k)
            keygen_reset_from_form(frm)
        # Update the internally tracked instances
        Query.instances=new_instances

    def set_prompt_save(self,value:bool) -> None:
        """
        Set the prompt to save action when navigating records

        :param value: a boolean value, True to prompt to save, False for no prompt to save
        :type value: bool
        :return: None
        """
        self._prompt_save=value

    def set_search_order(self, order:list) -> None:
        """
        Set the search order when using the search box.

        This is a list of columns to be searched, in order

        :param order: A list of column names to search
        :type order: list
        :returns: None
        :rtype: None
        """
        self.search_order = order

    def set_callback(self, callback:str, fctn:Callable[[Form, sg.Window], bool]) -> None:
        """
        Set Query callbacks. A runtime error will be thrown if the callback is not supported.

        The following callbacks are supported:
            before_save   called before a record is saved. The save will continue if the callback returns true, or the record will rollback if the callback returns false.
            after_save    called after a record is saved. The save will commit to the database if the callback returns true, else it will rollback the transaction
            before_update Alias for before_save
            after_update  Alias for after_save
            before_delete called before a record is deleted.  The delete will move forward if the callback returns true, else the transaction will rollback
            after_delete  called after a record is deleted. The delete will commit to the database if the callback returns true, else it will rollback the transaction
            before_duplicate called before a record is duplicate.  The duplicate will move forward if the callback returns true, else the transaction will rollback
            after_duplicate  called after a record is duplicate. The duplicate will commit to the database if the callback returns true, else it will rollback the transaction
            before_search called before searching.  The search will continue if the callback returns True
            after_search  called after a search has been performed.  The record change will undo if the callback returns False
            record_changed called after a record has changed (previous,next, etc) TODO: What about selectors?

        :param callback: The name of the callback, from the list above
        :type callback: str
        :param fctn: The function to call.  Note, the function must take in two parameters, a @Form instance, and a @PySimpleGUI.Window instance, and return True or False
        :type fctn: Callable[[Form, sg.Window], bool]
        :returns: None
        :rtype: None
        """
        logger.info(f'Callback {callback} being set on table {self.table}')
        supported = [
            'before_save', 'after_save', 'before_delete', 'after_delete', 'before_duplicate', 'after_duplicate',
            'before_update', 'after_update',  # Aliases for before/after_save
            'before_search', 'after_search', 'record_changed'
        ]
        if callback in supported:
            # handle our convenience aliases
            callback = 'before_save' if callback == 'before_update' else callback
            callback = 'after_save' if callback == 'after_update' else callback
            self.callbacks[callback] = fctn
        else:
            raise RuntimeError(f'Callback "{callback}" not supported.')

    def set_query(self, query:str) -> None:
        """
        Set the queries query string.

        This is more for advanced users.  It defaults to "SELECT * FROM {Query}; You can override the default with this method

        :param query: The query string you would like to associate with the table
        :type query: str
        :returns: None
        :rtype: None
        """
        logger.debug(f'Setting {self.table} query to {query}')
        self.query = query



    def set_join_clause(self, clause:str) -> None:
        """
        Set the table's join string.

        This is more for advanced users, as it will automatically generate from the Relationships that have been set otherwise.

        :param clause: The join clause, such as "LEFT JOIN That on This.pk=That.fk"
        :type clause: str
        :returns: None
        :rtype: None
        """
        logger.debug(f'Setting {self.table} join clause to {clause}')
        self.join = clause

    def set_where_clause(self, clause:str) -> None:
        """
        Set the table's where clause.

        This is ADDED TO the auto-generated where clause from Relationship data

        :param clause: The where clause, such as "WHERE pkThis=100"
        :type clause: str
        :returns: None
        :rtype: None
        """
        logger.debug(f'Setting {self.table} where clause to {clause}')
        self.where = clause

    def set_order_clause(self, clause:str) -> None:
        """
        Set the table's order clause.

        This is more for advanced users, as it will automatically generate from the Relationships that have been set otherwise.

        :param clause: The order clause, such as "Order by name ASC"
        :type clause: str
        :returns: None
        :rtype: None
        """
        logger.debug(f'Setting {self.table} order clause to {clause}')
        self.order = clause

    def update_column_names(self,names=None) -> None:
        """
        Generate column names for the query.  This may need done, for eample, when a manual query using joins
        is used.

        This is more for advanced users.
        :param names: a list of names (optional)
        """
        # Now we need to set  new column names, as the query could have changed
        if names!=None:
            self.column_names=names
            return

        cur = self.con.execute(self.generate_query())
        records = cur.fetchall()  # TODO: new version of this w/o cur
        for t in records:
            # Now lets get the pk
            # TODO: should we capture on_update, on_delete and match from PRAGMA?
            q2 = f'PRAGMA table_info({t["name"]})'
            cur2 = self.con.execute(q2)
            records2 = cur2.fetchall()
            names = []  # column names

            # auto generate description column.  Default it to the 2nd column,
            # but can be overwritten below
            description_column = records2[1]['name']

            pk_column = None
            for t2 in records2:
                names.append(t2['name'])
                if t2['pk']:
                    pk_column = t2['name']
                if t2['name'] == 'name':
                    description_column = t2['name']

            query_name = t['name']
            logger.debug(
                f'Adding query "{query_name}" on table {t["name"]} to Form with primary key {pk_column} and description of {description_column}')
            self.frm.add_query(query_name, t['name'], pk_column, description_column)
            self.column_names = names

    def set_description_column(self, column:str) -> None:
        """
        Set the table's description column.

        This is the column that will display in Listboxes, Comboboxes, etc.
        By default,this is initialized to either the 'name' column, or the 2nd column of the table.  This method allows you to specify
        something different

        :param column: The the column to use
        :type column: str
        :returns: None
        :rtype: None
        """
        self.description_column=column

    def records_changed(self, recursive=True) -> bool:
        """
        Checks if records have been changed by comparing PySimpleGUI control values with the stored Query values.
        :param recursive: True to check related Queries
        :type recursive: bool
        :returns: True or False on whether changed records were found
        :rtype: bool
        """
        logger.debug(f'Checking if records have changed in table "{self.table}"...')
        dirty = False  # we will start by assuming that there are no changes


        # First check the current record to see if it's dirty
        for c in self.frm.element_map:
            # Compare the DB version to the GUI version
            if c['query'].table == self.table:
                element_val = c['element'].get()
                table_val = self[c['column']]

                # For elements where the value is a Row type, we need to compare primary keys
                if type(element_val) is Row:
                    element_val = element_val.get_pk()

                # Sanitize things a bit due to empty values being slightly different in the two cases
                if table_val is None: table_val = ''

                # Cast to similar types
                if type(element_val) != type(table_val):
                    element_val = str(element_val)
                    table_val = str(table_val)

                # Fix 'False' != '0' 'True' != '1'
                if element_val == 'False' and table_val == '0':
                    element_val = '0'
                if element_val == 'True' and table_val == '1':
                    element_val = '1'

                # Strip trailing whitespace from strings
                if type(table_val) is str: table_val = table_val.rstrip()
                if type(element_val) is str: element_val = element_val.rstrip()

                if element_val != table_val:
                    dirty = True
                    logger.debug(f'CHANGED RECORD FOUND!')
                    logger.debug(f'\telement type: {type(element_val)} column_type: {type(table_val)}')
                    logger.debug(f'\t{c["element"].Key}:{element_val} != {c["column"]}:{table_val}')

        # handle recursive checking next
        if recursive:
            for rel in self.frm.relationships:
                if rel.parent == self.table and rel.requery_table:
                    if self.frm[rel.child].records_changed():
                        dirty = True
        return dirty



    def prompt_save(self, autosave=False) -> Union[PROMPT_PROCEED, PROMPT_DISCARDED, PROMPT_NONE]:
        """
        Prompts the user if they want to save when changes are detected and the current record is about to change.
        :param autosave: True to autosave when changes are found without prompting the user
        :type autosave: bool
        :returns: Prompt return value
        :rtype: Union[PROMPT_PROCEED, PROMPT_DISCARDED, PROMPT_NONE]
        """
        # Return False if there is nothing to check or _prompt_save is False
        # TODO: children too?
        if self.current_index is None or self.rows == [] or self._prompt_save is False:
            return PROMPT_NONE

        # Check if any records have changed
        changed = self.records_changed()
        if changed:
            if autosave:
                save_changes = 'Yes'
            else:
                save_changes = sg.popup_yes_no('You have unsaved changes! Would you like to save them first?')

            if save_changes == 'Yes':
                # save relationships
                for rel in self.frm.relationships:
                    if rel.parent == self.table and rel.requery_table:
                        self.frm[rel.child].save_record(True,False)
                # save this record
                self.save_record(True,False)
                return PROMPT_PROCEED
            else:
                return PROMPT_DISCARDED
        else:
            return PROMPT_NONE


    def generate_join_clause(self) -> str:
        """
        Automatically generates a join clause from the Relationships that have been set

        This typically isn't used by end users

        :returns: A join string to be used in a sqlite3 query
        :rtype: str
        """
        join = ''
        for r in self.frm.relationships:
            if self.table == r.child:
                join += f' {r.join} {r.parent} ON {r.child}.{r.fk} = {r.parent}.{r.pk}'
        return join if self.join == '' else self.join

    def generate_where_clause(self) -> str:
        """
        Generates a where clause from the Relationships that have been set, as well as the Query's where clause

        This is not typically used by end users

        :returns: A where clause string to be used in a sqlite3 query
        :rtype: str
        """
        where = ''
        for r in self.frm.relationships:
            if self.table == r.child:
                if r.requery_table:
                    clause=f' WHERE {self.table}.{r.fk}={str(self.frm[r.parent].get_current(r.pk, 0))}'
                    if where!='': clause=clause.replace('WHERE','AND')
                    where += clause

        if where == '':
            # There was no where clause from Relationships..
            where = self.where
        else:
            # There was an auto-generated portion of the where clause.  We will add the table's where clause to it
            where = where + ' ' + self.where.replace('WHERE', 'AND')

        return where

    def generate_query(self, join:bool=True, where:bool=True, order:bool=True) -> str:
        """
        Generate a query string using the relationships that have been set

        :param join: True if you want the join clause auto-generated, False if not
        :type join: bool
        :param where: True if you want the where clause auto-generated, False if not
        :type where: bool
        :param order: True if you want the order by clause auto-generated, False if not
        :type order: bool
        :returns: a query string for use with sqlite3
        :rtype: str
        """
        q = self.query
        q += f' {self.join if join else ""}'
        q += f' {self.where if where else ""}'
        q += f' {self.order if order else ""}'
        return q

    def requery(self, select_first=True, filtered=True, update=True):
        """
        Requeries the table
        The @Query object maintains an internal representation of the actual database table.
        The requery method will requery the actual database  and sync the @Query objects to it
        :param select_first: If true, the first record will be selected after the requery
        :param filtered: If true, the relationships will be considered and an appropriate WHERE clause will be generated
        :param update: passed to Query.first() to update_elements. Note that the select_first parameter must = True to use this parameter.
        :return: None
        """
        if filtered:
            join = self.generate_join_clause()
            where = self.generate_where_clause()

        query = self.query + ' ' + join + ' ' + where + ' ' + self.order
        logger.debug('Running query: ' + query)

        cur = self.con.execute(query)
        self.rows = cur.fetchall()
        if select_first:
            self.first(update,skip_prompt_save=True) # We don't want to prompt save in this situation, since there was a requery of the data

    def requery_dependents(self,update=True):
        """
        Requery parent queries as defined by the relationships of the table

        :return: None
        """
        for rel in self.frm.relationships:
            if rel.parent == self.table and rel.requery_table:
                logger.debug(f"Requerying dependent table {self.frm[rel.child].table}")
                self.frm[rel.child].requery(update=update)

    def first(self,update=True, dependents=True, skip_prompt_save=False):
        """
        Move to the first record of the table
        Only one entry in the table is ever considered "Selected"  This is one of several functions that influences
        which record is currently selected. See @Query.first, @Query.previous, @Query.next, @Query.last, @Query.search,
        @Query.set_by_pk
        :return: None
        """
        logger.debug(f'Moving to the first record of table {self.table}')
        if skip_prompt_save is False: self.prompt_save()
        self.current_index = 0
        if dependents: self.requery_dependents()
        if update: self.frm.update_elements()
        # callback
        if 'record_changed' in self.callbacks.keys():
            self.callbacks['record_changed'](self.frm, self.frm.window)

    def last(self, update=True, dependents=True, skip_prompt_save=False):
        """
        Move to the last record of the table
        Only one entry in the table is ever considered "Selected"  This is one of several functions that influences
        which record is currently selected. See @Query.first, @Query.previous, @Query.next, @Query.last, @Query.search,
        @Query.set_by_pk
        :return: None
        """
        logger.debug(f'Moving to the last record of table {self.table}')
        if skip_prompt_save is False: self.prompt_save()
        self.current_index = len(self.rows) - 1
        if dependents: self.requery_dependents()
        if update: self.frm.update_elements()
        # callback
        if 'record_changed' in self.callbacks.keys():
            self.callbacks['record_changed'](self.frm, self.frm.window)

    def next(self, update=True, dependents=True, skip_prompt_save=False):
        """
        Move to the next record of the table
        Only one entry in the table is ever considered "Selected"  This is one of several functions that influences
        which record is currently selected. See @Query.first, @Query.previous, @Query.next, @Query.last, @Query.search,
        @Query.set_by_pk
        :return: None
        """
        logger.debug(f'Moving to the next record of table {self.table}')
        if skip_prompt_save is False: self.prompt_save()
        if self.current_index < len(self.rows) - 1:
            self.current_index += 1
            if dependents: self.requery_dependents()
            if update: self.frm.update_elements()
            # callback
            if 'record_changed' in self.callbacks.keys():
                self.callbacks['record_changed'](self.frm, self.frm.window)

    def previous(self, update=True,dependents=True, skip_prompt_save=False):
        """
        Move to the previous record of the table
        Only one entry in the table is ever considered "Selected"  This is one of several functions that influences
        which record is currently selected. See @Query.first, @Query.previous, @Query.next, @Query.last, @Query.search,
        @Query.set_by_pk

        :return: None
        """
        logger.debug(f'Moving to the previous record of table {self.table}')
        if skip_prompt_save is False: self.prompt_save()
        if self.current_index > 0:
            self.current_index -= 1
            if dependents: self.requery_dependents()
            if update: self.frm.update_elements()
            # callback
            if 'record_changed' in self.callbacks.keys():
                self.callbacks['record_changed'](self.frm, self.frm.window)

    def search(self, string, update=True, dependents=True, skip_prompt_save=False):
        """
        Move to the next record in the search table that contains @string.
        Successive calls will search from the current position, and wrap around back to the beginning.
        The search order from @Query.set_search_order() will be used.  If the search order is not set by the user,
        it will default to the 'name' column, or the 2nd column of the table.
        Only one entry in the table is ever considered "Selected"  This is one of several functions that influences
        which record is currently selected. See @Query.first, @Query.previous, @Query.next, @Query.last, @Query.search,
        @Query.set_by_pk

        :param string: The search string
        :return: None
        """
        logger.debug(f'Searching for a record of table {self.table} with search term "{string}"')
        # callback
        if 'before_search' in self.callbacks.keys():
            if not self.callbacks['before_search'](self.frm, self.frm.window):
                return

        # See if the string is an element name # TODO this is a bit of an ugly hack, but it works
        if string in self.frm.window.key_dict.keys():
            string = self.frm.window[string].get()
        if string == '':
            return

        if skip_prompt_save is False: self.prompt_save() # TODO: Should this be before the before_search callback?
        # First lets make a search order.. TODO: remove this hard coded garbage

        for o in self.search_order:
            # Perform a search for str, from the current position to the end and back
            for i in list(range(self.current_index + 1, len(self.rows))) + list(range(0, self.current_index)):
                if o in self.rows[i].keys():
                    if self.rows[i][o]:
                        if string.lower() in str(self.rows[i][o]).lower():
                            print(string.lower())
                            old_index = self.current_index
                            self.current_index = i
                            if dependents: self.requery_dependents()
                            if update: self.frm.update_elements()

                            # callback
                            if 'after_search' in self.callbacks.keys():
                                if not self.callbacks['after_search'](self.frm, self.frm.window):
                                    self.current_index = old_index
                                    self.requery_dependents()
                                    self.frm.update_elements(self.table)
                            # callback
                            if 'record_changed' in self.callbacks.keys():
                                self.callbacks['record_changed'](self.frm, self.frm.window)
                            return
        return False
        # If we have made it here, then it was not found!
        # sg.Popup('Search term "'+str+'" not found!')
        # TODO: Play sound?

    def set_by_index(self, index, update=True, dependents=True, skip_prompt_save=False):
        logger.debug(f'Moving to the record at index {index} on {self.table}')
        if skip_prompt_save is False: self.prompt_save()

        self.current_index = index
        if dependents: self.requery_dependents()
        if update: self.frm.update_elements()

    def set_by_pk(self, pk, update=True, dependents=True, skip_prompt_save=False):
        """
        Move to the record with this primary key
        This is useful when modifying a record (such as renaming).  The primary key can be stored, the record re-named,
        and then the current record selection updated regardless of the new sort order.
        Only one entry in the table is ever considered "Selected"  This is one of several functions that influences
        which record is currently selected. See @Query.first, @Query.previous, @Query.next, @Query.last, @Query.search,
        @Query.set_by_pk
        :param pk: The primary key to move to
        :return: None
        """
        logger.debug(f'Setting table {self.table} record by primary key {pk}')
        if skip_prompt_save is False: self.prompt_save()

        i = 0
        for r in self.rows:
            if r[self.pk_column] == pk:
                self.current_index = i
                break
            else:
                i += 1

        if dependents: self.requery_dependents(update=update)
        if update: self.frm.update_elements(self.table)

    def get_current(self, column, default=""):
        """
        Get the current value pointed to for @column
        You can also use indexing of the @Form object to get the current value of a column
        I.e. frm["{Query}].[{column'}]

        :param column: The column you want the value of
        :param default: A value to return if the record is blank
        :return: The value of the column requested
        """
        if self.rows:
            if self.get_current_row()[column] != '':
                return self.get_current_row()[column]
            else:
                return default
        else:
            return default

    def get_keyed_value(self,value_column, key_column, key_value):
        """
        Return value_column where key_column=key_value.  Useful for datastores with key/value pairs
        """
        for r in self.rows:
            if r[key_column] == key_value:
                return r[value_column]

    def get_current_pk(self):
        """
        Get the primary key of the currently selected record
        :return: the primary key
        """
        return self.get_current(self.pk_column)

    def get_max_pk(self):
        """
        Returns the highest primary key for this table.
        This can give some insight on what the next inserted primary key may be
        :return: The maximum primary key value currently in the table
        """
        # TODO: Maybe get this right from the table object instead of running a query?
        q = f'SELECT MAX({self.pk_column}) AS highest FROM {self.table};'
        cur = self.con.execute(q)
        records = cur.fetchone()
        return records['highest']

    def get_current_row(self):
        """
        Get the sqlite3 row for the currently selected record of this table
        :return: @sqlite3.row
        """
        if self.rows:
            return self.rows[self.current_index]

    def add_selector(self, element, query:str=None, where_column:str=None, where_value:str=None):  # _listBox,_pk,_column):
        """
        Use a element such as a listbox as a selector item for this table.
        This can be done via this method, or via auto_map_elements by naming the element key "selector.{Query}"

        :param element: the @PySinpleGUI element used as a selector element
        :return: None
        """
        if type(element) not in [sg.PySimpleGUI.Listbox, sg.PySimpleGUI.Slider, sg.Combo, sg.Table]:
            raise RuntimeError(f'add_selector() error: {element} is not a supported element.')

        logger.debug(f'Adding {element.Key} as a selector for the {self.table} table.')
        d={'element': element, 'query': query, 'where_column': where_column, 'where_value': where_value}
        self.selector.append(d)

    def insert_record(self, column:str='', value:str='') -> None:
        """
        Insert a new record. If column and value are passed, it will initially set that column to the value
        (I.e. {Query}.name='New Record). If none are provided, the default values for the column are used, as set in the
        database.
        :param column: The column to set
        :param value: The value to set (I.e "New record")
        :return:
        """
        # todo: you don't add a record if there isn't a parent!!!
        # todo: this is currently filtered out by enabling of the element, but it should be filtered here too!
        # todo: bring back the values parameter

        columns = []
        values = []
        if column != '' and value != '':
            columns.append(column)
            values.append(value)

        # Make sure we take into account the foreign key relationships...
        for r in self.frm.relationships:
            if self.table == r.child:
                if r.requery_table:
                    columns.append(r.fk)
                    values.append(self.frm[r.parent].get_current_pk())

        columns = ",".join([str(x) for x in columns])
        values = ",".join([str(x) for x in values])
        # We will make a blank record and insert it
        q = f'INSERT INTO {self.table} '
        if columns != '':
            q += f'({columns}) VALUES ({values});'
        else:
            q += 'DEFAULT VALUES'
        logger.debug(q)
        cur = self.con.execute(q)
        self.con.commit()

        # Now we save the new pk
        pk = cur.lastrowid

        # and move to it
        self.requery()  # Don't move to the first record
        self.set_by_pk(pk)
        self.requery_dependents()

        self.frm.update_elements()
        self.frm.window.refresh()

    def save_record(self, display_message=True, update_elements=True):
        """
        Save the currently selected record
        Saves any changes made via the GUI back to the database.  The before_save and after_save @callbacks will call
        your own functions for error checking if needed!
        :param display_message: Displays a message "Updates saved successfully", otherwise is silent on success
        :return: None
        """
        saved=False

        # Ensure that there is actually something to save
        if not len(self.rows):
            if display_message: sg.popup_quick_message('There were no updates to save.',keep_on_top=True)
            return SAVE_NONE

        # callback
        if 'before_save' in self.callbacks.keys():
            if self.callbacks['before_save']()==False:
                logger.debug("We are not saving!")
                if update_elements: self.frm.update_elements(self.table)
                if display_message: sg.popup('Updates not saved.', keep_on_top=True)
                return SAVE_FAIL

        values = []
        # We are updating a record
        q = f'UPDATE {self.table} SET'
        for v in self.frm.element_map:
            if v['query'] == self:
                if '?' in v['element'].Key and '=' in v['element'].Key:
                    val=v['element'].get()
                    table_info, where_info = v['element'].Key.split('?')
                    q_kv = f'UPDATE {self.table} SET {v["column"]} = ? WHERE {v["where_column"]} = "{v["where_value"]}";'
                    self.con.execute(q_kv, tuple([val]))
                    saved=True
                else:
                    # TODO: what to do if there isn't a key split to do?
                    if '.' not in v['element'].Key:
                        continue
                    q += f' {v["element"].Key.split(".", 1)[1]}=?,'

                    if type(v['element'])==sg.Combo:
                        if type(v['element'].get())==str:
                            val = v['element'].get()
                        else:
                            val=v['element'].get().get_pk()
                    else:
                        val=v['element'].get()

                    values.append(val)
        if values:
            # there was something to update
            # Remove the trailing comma
            q = q[:-1]

            # Add the where clause
            q += f' WHERE {self.pk_column}={self.get_current(self.pk_column)};'
            logger.debug(f'Performing query: {q} {str(values)}')
            self.con.execute(q, tuple(values))
            saved=True

        # callback
        if saved:
            if 'after_save' in self.callbacks.keys():
                if not self.callbacks['after_save'](self.frm, self.frm.window):
                    self.con.rollback()
                    return SAVE_FAIL

            # If we ,ade it here, we can commit the changes
            self.con.commit()

            # Lets refresh our data
            pk = self.get_current_pk()
            self.requery(update_elements)
            self.set_by_pk(pk,update_elements,False,skip_prompt_save=True)
            #self.requery_dependents()
            if update_elements:self.frm.update_elements(self.table)
            logger.debug(f'Record Saved!')
            if display_message:  sg.popup_quick_message('Updates saved successfully!',keep_on_top=True)
            return SAVE_SUCCESS
        else:
            logger.debug('Nothing to save.')
            if display_message: sg.popup_quick_message('There were no updates to save!', keep_on_top=True)
            return SAVE_NONE

    def delete_record(self, cascade=True):
        """
        Delete the currently selected record
        The before_delete and after_delete callbacks are run during this process to give some control over the process

        :param cascade: Delete child records (as defined by @Relationship that were set up) before deleting this record
        :return: None
        """
        # Ensure that there is actually something to delete
        if not len(self.rows):
            return

        # callback
        if 'before_delete' in self.callbacks.keys():
            if not self.callbacks['before_delete'](self.frm, self.frm.window):
                return

        children = []
        if cascade:
            for qry in self.frm.queries:
                for r in self.frm.relationships:
                    if r.parent == self.table and r.requery_table:
                        children.append(r.child)
        
        children = list(set(children))
        if len(children):
            msg = f'Are you sure you want to delete this record? Keep in mind that children records in - {children} - will be deleted as well!'
        else:
            msg = 'Are you sure you want to delete this record?'
        answer = sg.popup_yes_no(msg, title='Confirm Delete',  keep_on_top=True)
        if answer == 'No':
            return True

        # Delete child records first!
        if cascade:
            for qry in self.frm.queries:
                for r in self.frm.relationships:
                    if r.parent == self.table:
                        q = f'DELETE FROM {r.child} WHERE {r.fk}={self.get_current(self.pk_column)}'
                        self.con.execute(q)
                        logger.debug(f'Delete query executed: {q}')
                        self.frm[r.child].requery(False)


        q = f'DELETE FROM {self.table} WHERE {self.pk_column}={self.get_current(self.pk_column)};'
        self.con.execute(q)

        # callback
        if 'after_delete' in self.callbacks.keys():
            if not self.callbacks['after_delete'](self.frm, self.frm.window):
                self.con.rollback()
            else:
                self.con.commit()
        else:
            self.con.commit()

        self.requery(False)  # Don't move to the first record
        self.current_index = self.current_index  # force the current_index to be in bounds! todo should this be done in requery?
        self.requery_dependents()

        self.requery(select_first=False)
        self.frm.update_elements()
        
    def duplicate_record(self, cascade=True):
        """
        Duplicate the currently selected record
        The before_duplicate and after_duplicate callbacks are run during this process to give some control over the process

        :param cascade: Duplicate child records (as defined by @Relationship that were set up) before duplicating this record
        :return: None
        """
        # Ensure that there is actually something to duplicate
        if not len(self.rows):
            return

        # callback
        if 'before_duplicate' in self.callbacks.keys():
            if not self.callbacks['before_duplicate'](self.frm, self.frm.window):
                return
            
        children = []
        if cascade:
            for qry in self.frm.queries:
                for r in self.frm.relationships:
                    if r.parent == self.table and r.requery_table:
                        children.append(r.child)
        
        children = list(set(children))
        if len(children):
            msg = f'Are you sure you want to duplicate this record? Keep in mind that children records in - {children} - will be duplicated as well!'
        else:
            msg = 'Are you sure you want to duplicate this record?'
        answer = sg.popup_yes_no(msg, title='Confirm Duplicate', keep_on_top=True)
        if answer == 'No':
            return True

        ## https://stackoverflow.com/questions/1716320/how-to-insert-duplicate-rows-in-sqlite-with-a-unique-id
        ## This can be done using * syntax without having to know the schema of the table
        ## (other than the name of the primary key). The trick is to create a temporary table
        ## using the "CREATE TABLE AS" syntax.
        q = f'CREATE TEMPORARY TABLE tmp AS SELECT * FROM {self.table} WHERE {self.pk_column}={self.get_current(self.pk_column)}'
        self.con.execute(q)
        logger.debug(q)
        q = f'UPDATE tmp SET {self.pk_column} = NULL'
        self.con.execute(q)
        logger.debug(q)
        q = f'UPDATE tmp SET {self.description_column} = "Copy of " || {self.description_column}'
        self.con.execute(q)
        logger.debug(q)
        q = f'INSERT INTO {self.table} SELECT * FROM tmp'
        cur = self.con.execute(q)
        logger.debug(q)
        q = f'DROP TABLE tmp;'
        self.con.execute(q)
        logger.debug(q)
        
        # Now we save the new pk
        pk = cur.lastrowid
        
        # create list of which children we have duplicated
        child_duplicated = []             
        # Next, duplicate the child records!
        if cascade:
            for qry in self.frm.queries:
                for r in self.frm.relationships:
                    if r.parent == self.table and r.requery_table and (r.child not in child_duplicated):
                        q = f'CREATE TEMPORARY TABLE tmp AS SELECT * FROM {r.child} WHERE {r.fk}={self.get_current(self.pk_column)}'
                        self.con.execute(q)
                        logger.debug(q)
                        q = f'UPDATE tmp SET {self.frm[r.child].pk_column} = NULL'
                        self.con.execute(q)
                        logger.debug(q)
                        q = f'UPDATE tmp SET {r.fk} = {pk}'
                        self.con.execute(q)
                        logger.debug(q)
                        q = f'INSERT INTO {r.child} SELECT * FROM tmp'
                        self.con.execute(q)
                        logger.debug(q)
                        q = f'DROP TABLE tmp;'
                        self.con.execute(q)
                        logger.debug(q)
                        child_duplicated.append(r.child)
                        
        # callback
        if 'after_duplicate' in self.callbacks.keys():
            if not self.callbacks['after_duplicate'](self.frm, self.frm.window):
                self.con.rollback()
            else:
                self.con.commit()
        else:
            self.con.commit()
        self.con.commit()
        
        # move to new pk
        self.frm[r.child].requery(False)
        self.requery()
        self.set_by_pk(pk)
        self.requery_dependents()

        self.frm.update_elements()
        self.frm.window.refresh()

    def get_description_for_pk(self,pk):
        for row in self.rows:
            if row[self.pk_column]==pk:
                return row[self.description_column]
        return None

    def table_values(self,columns=None):
        # Populate entries
        values = []
        column_names=self.column_names if columns == None else columns
        for row in self.rows:
            lst = []
            rels = self.frm.get_relationships_for_table(self)
            for col in column_names:
                found = False
                for rel in rels:
                    if col == rel.fk:
                        lst.append(self.frm[rel.parent].get_description_for_pk(row[col]))
                        found = True
                        break
                if not found: lst.append(row[col])
            values.append(lst)
        return values

    def get_related_table_for_column(self,col):
        rels = self.frm.get_relationships_for_table(self)
        for rel in rels:
            if col == rel.fk:
                return rel.parent
        return self.name # None could be found, return ourself

    def quick_editor(self, pk_update_funct=None,funct_param=None):
        # Reset the keygen to keep consistent naming
        logger.info('Creating Quick Editor window')
        keygen_reset_all()
        query_name = self.name
        layout = []
        headings = self.column_names.copy()
        visible = [1] * len(headings); visible[0] = 0
        col_width=int(55/(len(headings)-1))
        for i in range(0,len(headings)):
            headings[i]=headings[i].ljust(col_width,' ')

        layout.append(
            [pysimplesql.selector('quick_edit2', query_name, sg.Table, num_rows=10, headings=headings, visible_column_map=visible)])
        layout.append([pysimplesql.actions("act_quick_edit2",query_name,edit_protect=False)])
        layout.append([sg.Text('')])
        layout.append([sg.HorizontalSeparator()])
        for col in self.column_names:
            column=f'{query_name}.{col}'
            if col!=self.pk_column:
                layout.append([pysimplesql.record(column)])

        quick_win = sg.Window(f'Quick Edit - {query_name}', layout, keep_on_top=True, finalize=True, ttk_theme=pysimplesql.get_ttk_theme()) ## Without specifying same ttk_theme, quick_edit will override user-set theme in main window
        quick_frm = Form(sqlite3_database=self.frm.con, bind=quick_win)


        # Select the current entry to start with
        if pk_update_funct is not None:
            if funct_param is None:
                quick_frm[query_name].set_by_pk(pk_update_funct())
            else:
                quick_frm[query_name].set_by_pk(pk_update_funct(funct_param))

        while True:
            event, values = quick_win.read()

            if quick_frm.process_events(event, values):
                logger.debug(f'PySimpleSQL Quick Editor event handler handled the event {event}!')
            if event == sg.WIN_CLOSED or event == 'Exit':
                break
            else:
                logger.debug(f'This event ({event}) is not yet handled.')
        quick_win.close()
        self.requery()



class Form:
    """
    @orm class
    Maintains an internal version of the actual database
    Queries can be accessed by key, I.e. frm['query_name"] to return a Query instance
    """
    instances = []  # Track our instances
    relationships = [] # Track our relationships

    def __init__(self, db_path=None, bind=None, sql_script=None, sqlite3_database=None, sql_commands=None, prefix_queries='', parent=None, filter=None):
        """
        Initialize a new @Form instance

        :param db_path: the name of the database file.  It will be created if it doesn't exist.
        :param bind: (PySimpleSQL Window) Bind this window to the Form
        :param sqlite3_database: A sqlite3 database object
        :param sql_commands: (str) SQL commands to run if @sqlite3_database is not present
        :param sql_script: (file) SQL commands to run if @sqlite3_database is not present
        :param prefix_queries: (optional) prefix auto generated query names with this value. Example 'qry_'
        :param parent: parent form to base queries off of
        :param filter: (optional) Only import elements with the same filter
        """
        Form.instances.append(self)

        if db_path is not None:
            logger.info(f'Opening database: {db_path}')
            new_database = not os.path.isfile(db_path)
            con = sqlite3.connect(db_path)  # Open our database

        self.imported_database=False
        if sqlite3_database is not None:
            con = sqlite3_database
            new_database = False
            self.imported_database=True

        self.filter = filter
        self.parent = parent
        self.db_path = db_path  # type: str
        self.window = None
        self._edit_protect=False
        self.queries = {}
        self.element_map = []
        self.event_map = [] # Array of dicts, {'event':, 'function':, 'table':}
        self.relationships = []
        self.callbacks = {}
        self.con = con
        self.con.row_factory = sqlite3.Row
        if sql_commands is not None and new_database:
            # run SQL script if the database does not yet exist
            logger.info(f'Executing sql commands passed in')
            logger.debug(sql_commands)
            self.con.executescript(sql_commands)
            self.con.commit()
        if sql_script is not None and new_database:
            # run SQL script from the file if the database does not yet exist
            logger.info('Executing sql script from file passed in')
            self.execute_script(sql_script)

        # Add our default queries and relationships
        self.auto_add_queries(prefix_queries)
        self.auto_add_relationships()
        self.requery_all(False)
        if bind!=None:
            self.window=bind
            self.bind(self.window)

    def __del__(self):
        # Only do cleanup if this is not an imported database
        if not self.imported_database:
            # optimize the database for long-term benefits
            if self.db_path != ':memory:':
                q = 'PRAGMA optimize;'
                self.con.execute(q)
            # Close the connection
            self.con.close()

    # Override the [] operator to retrieve queries by key
    def __getitem__(self, key:str) -> Query:
        return self.queries[key]

    def close(self,reset_keygen=True):
        # Safely close out the form
        # First delete the queries associated
        Query.purge_form(self,reset_keygen)

    def bind(self, win):
        """
        Bind the Window to the Form for the purpose of GUI element, event and relationship mapping
        This can happen automatically on@Form creation with a parameter.
        This function literally just groups all of the auto_* methods.  See" Form.auto_add_tables,
        Form.auto_add_relationships, Form.auto_map_elements, @orm.auto_map_events
        :param win: The PySimpleGUI window
        :return:  None
        """
        logger.info('Binding Window to Form')
        self.window = win
        self.auto_map_elements(win)
        self.auto_map_events(win)
        self.update_elements()
        logger.debug('Binding finished!')


    def execute_script(self,script):
        with open(script, 'r') as file:
            logger.info(f'Loading script {script} into database.')
            self.con.executescript(file.read())

    def execute(self, q):
        """
        Convenience function to pass along to sqlite3.execute()
        :param q: The query to execute
        :return: sqlite3.cursor
        """
        return self.con.execute(q)

    def commit(self):
        """
        Convience function to pass along to sqlite3.commit()
        :return: None
        """
        self.con.commit()

    def set_callback(self, callback, fctn):
        """
       Set @orm callbacks. A runtime error will be raised if the callback is not supported.
       The following callbacks are supported:
           update_elements Called after elements are updated via @Form.update_elements. This allows for other GUI manipulation on each update of the GUI
           edit_enable Called before editing mode is enabled. This can be useful for asking for a password for example
           edit_disable Called after the editing mode is disabled
           {element_name} Called while updating MAPPED element.  This overrides the default element update implementation.
           Note that the {element_name} callback function needs to return a value to pass to Win[element].update()

       :param callback: The name of the callback, from the list above

       :param fctn: The function to call.  Note, the function must take in two parameters, a Form instance, and a PySimpleGUI.Window instance
       :return: None
       """
        logger.info(f'Callback {callback} being set on Form')
        supported = ['update_elements', 'edit_enable', 'edit_disable']

        # Add in mapped elements
        for element in self.element_map:
            supported.append(element['element'].Key)

        # Add in other window elements
        for element in self.window.key_dict:
            supported.append(element)

        if callback in supported:
            self.callbacks[callback] = fctn
        else:
            raise RuntimeError(f'Callback "{callback}" not supported. callback: {callback} supported: {supported}')


    # Add a Query object
    def add_query(self, name, table, pk_column, description_column, query='', order=''):
        """
        Manually add a Query to the Form
        When you attach to an sqlite database, PySimpleSQL isn't aware of what it contains until this command is run
        Note that Form.auto_add_queries will do this automatically, which is also called from Form.auto_bind
        and even from the Form.__init__ with a parameter

        :param table: The name of the table (must match sqlite)
        :param pk_column: The primary key column
        :param description_column: The column to be used to display to users
        :param query: The initial query for the table.  Set to "SELECT * FROM {table}" if none is passed
        :param order: The initial sort order for the query
        :return: None
        """
        self.queries.update({name: Query(name,self, table, pk_column, description_column, query, order)})
        self[name].set_search_order([description_column])  # set a default sort order

    def add_relationship(self, join, child, fk, parent, pk, requery_table):
        """
        Add a foreign key relationship between two queries of the database
        When you attach an sqlite database, PySimpleSQL isn't aware of the relationships contained until queries are
        added via @Form.add_table, and the relationship of various queries is set with this function.
        Note that @Form.auto_add_relationships will do this automatically from the schema of the sqlite database,
        which also happens automatically with @Form.auto_bind and even from the @Form.__init__ with a parameter
        :param join: The join type of the relationship ('LEFT JOIN', 'INNER JOIN', 'RIGHT JOIN')
        :param child: The child table containing the foreign key
        :param fk: The foreign key column of the child table
        :param parent: The parent table containing the primary key
        :param pk: The primary key column of the parent table
        :param requery_table: Automatically requery the child table if the parent table changes (ON UPDATE CASCADE in sql)

        :return: None
        """
        self.relationships.append(Relationship(join, child, fk, parent, pk, requery_table))

    def get_relationships_for_table(self, table):
        """
        Return the relationships for the passed-in table.
        :param table: The table to get relationships for
        :return: A list of @Relationship objects
        """
        rel = []
        for r in self.relationships:
            if r.child == table.table:
                rel.append(r)
        return rel

    def get_cascaded_relationships(self):
        """
        Return a unique list of the relationships for this table that should requery with this table.
        :return: A unique list of table names
        """
        rel = []
        for r in self.relationships:
            if r.requery_table:
                rel.append(r.parent)
                rel.append(r.child)
        # make unique
        rel = list(set(rel))
        return rel

    def get_parent(self, table):
        """
        Return the parent table for the passed-in table
        :param table: The table (str) to get relationships for
        :return: The name of the Parent table, or '' if there is none
        """
        for r in self.relationships:
            if r.child == table and r.requery_table:
                return r.parent
        return None
    
    def auto_add_queries(self, prefix_queries=''):
        """
        Automatically add Query objects from a sqlite database by looping through the tables available and creating a query for each.
        When you attach to a sqlite database, PySimpleSQL isn't aware of what it contains until this command is run.
        This is also called by @Form.auto_bind() or even from the @Form.__init__ with a parameter
        Note that @Form.add_table can do this manually on a per-table basis.
        :return: None
        """
        logger.info('Automatically generating queries for each table in the sqlite database')
        # Ensure we clear any current queries so that successive calls will not double the entries
        self.queries = {}
        q = 'SELECT name FROM sqlite_master WHERE type="table" AND name NOT like "sqlite%";'
        cur = self.con.execute(q)
        records = cur.fetchall()  # TODO: new version of this w/o cur
        for t in records:
            # Now lets get the pk
            # TODO: should we capture on_update, on_delete and match from PRAGMA?
            q2 = f'PRAGMA table_info({t["name"]})'
            cur2 = self.con.execute(q2)
            records2 = cur2.fetchall()
            names = []

            # auto generate description column.  Default it to the 2nd column,
            # but can be overwritten below
            description_column = records2[1]['name']

            pk_column = None
            for t2 in records2:
                names.append(t2['name'])
                if t2['pk']:
                    pk_column = t2['name']
                if t2['name'] == 'name':
                    description_column = t2['name']

            query_name=prefix_queries+t['name']
            logger.debug(
                f'Adding query "{query_name}" on table {t["name"]} to Form with primary key {pk_column} and description of {description_column}')
            self.add_query(query_name,t['name'], pk_column, description_column)
            self.queries[query_name].column_names = names #TODO: use new add column names??

    # Make sure to send a list of table names to requery if you want
    # dependent queries to requery automatically
    # TODO: clear relationships first so that successive calls don't add multiple entries.
    def auto_add_relationships(self):
        """
        Automatically add a foreign key relationship between queries of the database. This is done by foregn key constrains
        within the sqlite database.  Automatically requery the child table if the parent table changes (ON UPDATE CASCADE in sql is set)
        When you attach an sqlite database, PySimpleSQL isn't aware of the relationships contained until queries are
        added and the relationship of various queries is set.
        Note that @Form.add_relationship() can do this manually.
        which also happens automatically with @Form.auto_bind and even from the @Form.__init__ with a parameter
        :return: None
        """
        logger.info(f'Automatically adding foreign key relationships')
        # Ensure we clear any current queries so that successive calls will not double the entries
        self.relationships = []
        for table in self.queries:
            rows = self.con.execute(f"PRAGMA foreign_key_list({table})")
            rows = rows.fetchall()

            for row in rows:
                # Add the relationship if it's in the requery list
                if row['on_update'] == 'CASCADE':
                    logger.debug(f'Setting table {table} to auto requery with table {row["table"]}')
                    requery_table = True
                else:
                    requery_table = False

                logger.debug(f'Adding relationship {table}.{row["from"]} = {row["table"]}.{row["to"]}')
                self.add_relationship('LEFT JOIN', table, row['from'], row['table'], row['to'], requery_table)

    # Map an element to a Query.
    # Optionally a where_column and a where_value.  This is useful for key,value pairs!
    def map_element(self, element, query, column, where_column=None, where_value=None):
        dic = {
            'element': element,
            'query': query,
            'column': column,
            'where_column': where_column,
            'where_value': where_value,
            # Element-level query clauses
            'where_clause': None,
            'order_clause': None,
            'join_clause': None
        }
        logger.debug(f'Mapping element {element.Key}')
        self.element_map.append(dic)

    def auto_map_elements(self, win, keys=None):
        logger.info('Automapping elements')
        # clear out any previously mapped elements to ensure successive calls doesn't produce duplicates
        self.element_map = []
        for key in win.key_dict.keys():
            element=win[key]

            # Skip this element if there is no metadata present
            if type(element.metadata) is not dict:
                continue


            # Process the filter to ensure this element should be mapped to this Form
            if element.metadata['filter'] == self.filter:
                element.metadata['Form'] = self

            # Skip this element if it's an event
            if element.metadata['type'] == TYPE_EVENT:
                continue

            if element.metadata['Form'] != self:
                continue
            # If we passed in a cutsom list of elements
            if keys is not None:
                if key not in keys: continue

            # Map Record Element
            if element.metadata['type']==TYPE_RECORD:
                # Does this record imply a where clause (indicated by ?) If so, we can strip out the information we need
                if '?' in key:
                    table_info, where_info = key.split('?')
                else:
                    table_info = key; where_info = None

                table, col = table_info.split('.')
                if where_info is None:
                    where_column=where_value=None
                else:
                    where_column,where_value=where_info.split('=')

                if table in self.queries:
                    if col in self[table].column_names:
                        # Map this element to table.column
                        self.map_element(element, self[table], col, where_column, where_value)

            # Map Selector Element
            if element.metadata['type']==TYPE_SELECTOR:
                k=element.metadata['table']
                if k is None: continue
                if element.metadata['Form'] != self: continue
                if '?' in k:
                    query_info, where_info = k.split('?')
                    where_column,where_value=where_info.split('=')
                else:
                    query_info = k;
                    where_info = where_column = where_value = None
                query= query_info

                if query in self.queries:
                    self[query].add_selector(element,query,where_column,where_value)
                else:
                    logger.debug(f'Can not add selector {str(element)}')

    def set_element_clause(self,element,where:str=None,order:str=None) -> None:
        for e in self.element_map:
            if e['element']==element:
                e['where_clause']=where
                e['order_clause']=order

    def map_event(self, event, fctn, table=None):
        dic = {
            'event': event,
            'function': fctn,
            'table': table
        }
        logger.debug(f'Mapping event {event} to function {fctn}')
        self.event_map.append(dic)

    def replace_event(self,event,function,table=None):
        for e in self.event_map:
            if e['event'] == event:
                e['function'] = function
                e['table'] = table if table is not None else e['table']

    def auto_map_events(self, win):
        logger.info(f'Automapping events')
        # clear out any previously mapped events to ensure successive calls doesn't produce duplicates
        self.event_map = []

        for key in win.key_dict.keys():
            #key = str(key)  # sometimes I end up with an integer element 0? TODO: Research
            element = win[key]
            # Skip this element if there is no metadata present
            if type(element.metadata) is not dict:
                logger.debug(f'Skipping mapping of {key}')
                continue
            if element.metadata['Form'] != self:
                continue
            if element.metadata['type'] == TYPE_EVENT:
                event_type=element.metadata['event_type']
                query=element.metadata['query']
                function=element.metadata['function']

                funct=None

                event_query=query if query in self.queries else None
                if event_type==EVENT_FIRST:
                    if query in self.queries: funct=self[query].first
                elif event_type==EVENT_PREVIOUS:
                    if query in self.queries: funct=self[query].previous
                elif event_type==EVENT_NEXT:
                    if query in self.queries: funct=self[query].next
                elif event_type==EVENT_LAST:
                    if query in self.queries: funct=self[query].last
                elif event_type==EVENT_SAVE:
                    if query in self.queries: funct=self[query].save_record
                elif event_type==EVENT_INSERT:
                    if query in self.queries: funct=self[query].insert_record
                elif event_type==EVENT_DELETE:
                    if query in self.queries: funct=self[query].delete_record
                elif event_type==EVENT_DUPLICATE:
                    if query in self.queries: funct=self[query].duplicate_record
                elif event_type==EVENT_EDIT_PROTECT_DB:
                    self.edit_protect() # Enable it!
                    funct=self.edit_protect
                elif event_type==EVENT_SAVE_DB:
                    funct=self.save_records
                elif event_type==EVENT_SEARCH:
                    # Build the search box name
                    search_element,command=key.split('.')
                    search_box=f'{search_element}.input_search'
                    if query in self.queries: funct=functools.partial(self[query].search, search_box)
                #elif event_type==EVENT_SEARCH_DB:
                elif event_type == EVENT_QUICK_EDIT:
                    t,c,e=key.split('.') #table, column, event
                    referring_table=query
                    query=self[query].get_related_table_for_column(c)
                    funct=functools.partial(self[query].quick_editor,self[referring_table].get_current,c)
                elif event_type == EVENT_FUNCTION:
                    funct=function
                else:
                    logger.debug(f'Unsupported event_type: {event_type}')

                if funct is not None:
                    self.map_event(key, funct, event_query)



    def edit_protect(self,event=None, values=None):
        logger.debug('Toggling edit protect mode.')
        # Callbacks
        if self._edit_protect:
            if 'edit_enable' in self.callbacks.keys():
                if not self.callbacks['edit_enable'](self, self.window):
                    return
        else:
            if 'edit_disable' in self.callbacks.keys():
                if not self.callbacks['edit_disable'](self, self.window):
                    return

        self._edit_protect = not self._edit_protect
        self.update_elements(edit_protect_only=True)

    def get_edit_protect(self):
        return self._edit_protect

    def prompt_save(self, autosave=False) -> int:
        """
        Prompt to save if any GUI changes are found the affect any table on this form
        :param autosave: True to autosave when changes are found without prompting the user
        :type autosave: bool
        :return: Prompt return value
        :rtype: int, one of Union[PROMPT_PROCEED, PROMPT_DISCARDED, PROMPT_NONE]
        """
        user_prompted = False # Has the user been prompted yet?
        for q in self.queries:
            if self[q]._prompt_save is False:
                continue

            if self[q].records_changed(recursive=False): # don't check children
                # we will only show the popup once, regardless of how many queries have changed
                if not user_prompted:
                    user_prompted = True
                    if autosave:
                        save_changes = 'Yes'
                    else:
                        save_changes = sg.popup_yes_no('You have unsaved changes! Would you like to save them first?')

                    if save_changes != 'Yes':
                        # update the elements to erase any GUI changes, since we are choosing not to save
                        self.update_elements()
                        return PROMPT_DISCARDED # We did have a change, regardless if the user chose not to save
                self[q].save_record(update_elements=False) # Don't update elements yet, as there may be more saving to do yet
        self.update_elements() # Now we are safe to update elements
        return PROMPT_PROCEED if user_prompted else PROMPT_NONE




    def save_records(self, cascade_only=False):
        logger.debug(f'Saving records in all queries...')
        msg = None
        i = 0
        tables = self.get_cascaded_relationships() if cascade_only else self.queries
        last_index = len(self.queries) - 1

        successes=0
        failures=0
        no_actions=0
        for t in tables:
            logger.debug(f'Saving records for table {t}...')
            result=self[t].save_record(False,update_elements=False)
            if result==SAVE_FAIL:
                failures+=1
            elif result==SAVE_SUCCESS:
                successes+=1
            elif result==SAVE_NONE:
                no_actions+=1
        logger.debug(f'Successes: {successes}, Failures: {failures}, No Actions: {no_actions}')

        if failures==0:
            if successes==0:
                sg.popup_quick_message('There was nothing to update.', keep_on_top=True)
            else:
                sg.popup_quick_message('Updates saved successfully!',keep_on_top=True)
        else:
            sg.popup('There was a problem saving some updates.', keep_on_top=True)

        self.update_elements()

    def set_prompt_save(self, value: bool) -> None:
        """
        Set the prompt to save action when navigating records for all queries associated with this form

        :param value: a boolean value, True to prompt to save, False for no prompt to save
        :type value: bool
        :return: None
        """
        for q in self.queries:
            self[q].set_prompt_save(value)

    def update_elements(self, table_name:str=None, edit_protect_only:bool=False) -> None:
        """
        Updated the GUI elements to reflect values from the database for this Form instance only

        Not to be confused with pysimplesql.update_elements(), which updates GUI elements for all Form instances.


        :param table_name: (optional) name of table to update elements for, otherwise updates elements for all queries
        :type table_name: str
        :param edit_protect_only: (default False) If true, only update items affected by edit_protect
        :type edit_protect_only: bool
        :returns: None
        :rtype: None
        """
        msg='edit protect' if edit_protect_only else 'PySimpleGUI'
        logger.debug(f'update_elements(): Updating {msg} elements')
        win = self.window
        # Disable/Enable action elements based on edit_protect or other situations
        for t in self.queries:
            for m in self.event_map:
                # Disable delete/duplicate and mapped elements for this table if there are no records in this table or edit protect mode
                hide = len(self[t].rows) == 0 or self._edit_protect
                if ('.table_delete' in m['event']) or ('.table_duplicate' in m['event']):
                    win[m['event']].update(disabled=hide)
                    self.update_element_states(t, hide)

                # Disable insert on children with no parent records or edit protect mode
                parent = self.get_parent(t)
                if parent is not None:
                    hide = len(self[parent].rows) == 0 or self._edit_protect
                else:
                    hide = self._edit_protect
                if '.table_insert' in m['event']:
                    if m['table'] == t:
                        win[m['event']].update(disabled=hide)

                # Disable db_save when needed
                hide = self._edit_protect
                if '.db_save' in m['event']:
                    win[m['event']].update(disabled=hide)

                # Disable table_save when needed
                hide = self._edit_protect
                if '.table_save' in m['event']:
                    win[m['event']].update(disabled=hide)

                # Enable/Disable quick edit buttons
                if '.quick_edit' in m['event']:
                    win[m['event']].update(disabled=hide)
        if edit_protect_only: return

        # d= dictionary (the element map dictionary)
        for d in self.element_map:
            # If the optional query parameter was passed, we will only update elements bound to that table
            if table_name is not None:
                if d['query'].table != table_name:
                    continue

            updated_val = None
            # If there is a callback for this element, use it
            if d['element'].Key in self.callbacks:
                self.callbacks[d['element'].Key]()

            elif d['where_column'] is not None:
                # We are looking for a key,value pair or similar.  Lets sift through and see what to put
                updated_val=d['query'].get_keyed_value(d['column'], d['where_column'], d['where_value'])
                if type(d['element']) in [sg.PySimpleGUI.CBox]: # TODO, may need to add more??
                    updated_val=int(updated_val)

            elif type(d['element']) is sg.PySimpleGUI.Combo:
                # Update elements with foreign queries first
                # This will basically only be things like comboboxes
                # TODO: move this to only compute if something else changes?
                # see if we can find the relationship to determine which table to get data from
                target_table=None
                rels = self.get_relationships_for_table(d['query'])
                for rel in rels:
                    if rel.fk == d['column']:
                        target_table = self[rel.parent]
                        pk = target_table.pk_column
                        description = target_table.description_column
                        break

                if target_table==None:
                    logger.warning(f"Error! Cound not find a related query for element {d['element'].Key} bound to query {d['query'].table}")
                    # we don't want to update the list in this case, as it was most likely supplied and not tied to a query
                    updated_val=d['query'][d['column']]

                # Populate the combobox entries
                else:
                    lst = []
                    for row in target_table.rows:
                        lst.append(Row(row[pk], row[description]))

    
                    # Map the value to the combobox, by getting the description_column and using it to set the value
                    for row in target_table.rows:
                        if row[target_table.pk_column] == d['query'][rel.fk]:
                            for entry in lst:
                                if entry.get_pk() == d['query'][rel.fk]:
                                    updated_val = entry
                                    break
                            break
                    d['element'].update(values=lst)
            elif type(d['element']) is sg.PySimpleGUI.Table:
                # Tables use an array of arrays for values.  Note that the headings can't be changed.
                values = d['query'].table_values()
                # Select the current one
                pk = d['query'].get_current_pk()
                index = 0
                found = False
                for v in values:
                    if v[0] == pk:
                        found = True
                        break
                    index += 1
                if not found:
                    index = []
                else:
                    index = [index]
                d['element'].update(values=values, select_rows=index)
                eat_events(self.window)
                continue

            elif type(d['element']) is sg.PySimpleGUI.InputText or type(d['element']) is sg.PySimpleGUI.Multiline:
                # Update the element in the GUI
                # For text objects, lets clear it first...
                d['element'].update('')  # HACK for sqlite query not making needed keys! This will blank it out at least
                updated_val = d['query'][d['column']]

            elif type(d['element']) is sg.PySimpleGUI.Checkbox:
                updated_val = d['query'][d['column']]
            elif type(d['element']) is sg.PySimpleGUI.Image:
                val = d['query'][d['column']]

                try:
                    val=eval(val)
                except:
                    # treat it as a filename
                    d['element'].update(val)
                else:
                    # update the bytes data
                    d['element'].update(data=val)
                updated_val=None # Prevent the update from triggering below, since we are doing it here
            else:
                sg.popup(f'Unknown element type {type(d["element"])}')

            # Finally, we will update the actual GUI element!
            if updated_val is not None:
                d['element'].update(updated_val)

        # ---------
        # SELECTORS
        # ---------
        # We can update the selector elements
        # We do it down here because it's not a mapped element...
        # Check for selector events
        for k, table in self.queries.items():
            if len(table.selector):
                for e in table.selector:
                    logger.debug(f'update_elements: SELECTOR FOUND')
                    element=e['element']
                    logger.debug(f'{type(element)}')
                    pk = table.pk_column
                    column = table.description_column
                    if element.Key in self.callbacks:
                        self.callbacks[element.Key]()

                    if type(element) == sg.PySimpleGUI.Listbox or type(element) == sg.PySimpleGUI.Combo:
                        logger.debug(f'update_elements: List/Combo selector found...')
                        lst = []
                        for r in table.rows:
                            if e['where_column'] is not None:
                                if str(r[e['where_column']]) == str(e['where_value']): # TODO: This is kind of a hackish way to check for equality...
                                    #print(f"{r[e['where_column']]} == {e['where_value']}")
                                    lst.append(Row(r[pk], r[column]))
                                else:
                                    pass
                                    #print(f"{r[e['where_column']]} != {e['where_value']}")
                            else:
                                lst.append(Row(r[pk], r[column]))

                        element.update(values=lst, set_to_index=table.current_index)
                    elif type(element) == sg.PySimpleGUI.Slider:
                        # We need to re-range the element depending on the number of records
                        l = len(table.rows)
                        element.update(value=table._current_index + 1, range=(1, l))

                    elif type(element) is sg.PySimpleGUI.Table:
                        logger.debug(f'update_elements: Table selector found...')
                        # Populate entries
                        values = table.table_values(element.metadata['columns'])

                        # Get the primary key to select.  We have to use the list above instead of getting it directly
                        # from the table, as the data has yet to be updated
                        pk = table.get_current_pk()
                        index = 0
                        found=False
                        for v in values:
                            if v[0] == pk:
                                found=True
                                break
                            index += 1
                        if not found:
                            index=[]
                        else:
                            index=[index]
                        logger.debug(f'Selector:: index:{index} found:{found}')
                        element.update(values=values,select_rows=index)
                        eat_events(self.window)

        # Run callbacks
        if 'update_elements' in self.callbacks.keys():
            # Running user update function
            logger.info('Running the update_elements callback...')
            self.callbacks['update_elements'](self, self.window)


    def requery_all(self, update_elements=True) -> None:
        """
        Requeries all queries in the database

        This effectively re-loads the data from the actual sqlite3 queries into Query class objects

        :param update_elements: True to update elements after this operation
        :type update_elements: bool
        :returns: None
        :rtype: None
        """
        logger.info('Requerying all queries')
        for k in self.queries.keys():
            self[k].requery(update_elements)

    def process_events(self, event:str, values:list) -> bool:
        """
        Process mapped events for this specific Form instance.

        Not to be confused with pysimplesql.process_events(), which processes events for ALL Form instances.
        This should be called once per iteration in your event loop
         .. note:: Events handled are responsible for requerying and updating elements as needed

        :param event: The event returned by PySimpleGUI.read()
        :type event: str
        :param values: the values returned by PySimpleGUI.read()
        :type values: list
        :returns: True if an event was handled, False otherwise
        :rtype: bool
        """
        if self.window is None:
            logger.info(f'***** Form appears to be unbound.  Do you have frm.bind(win) in your code? ***')
            return False
        elif event:
            for e in self.event_map:
                if e['event'] == event:
                    logger.debug(f"Executing event {event} via event mapping.")
                    e['function']()
                    logger.debug(f'Done processing event!')
                    return True

            # Check for  selector events
            for k, table in self.queries.items():
                if len(table.selector):
                    for e in table.selector:
                        element=e['element']
                        if element.Key in event and len(table.rows) > 0:
                            changed=False # assume that a change will not take place
                            if type(element) == sg.PySimpleGUI.Listbox:
                                row = values[element.Key][0]
                                table.set_by_pk(row.get_pk())
                                changed=True
                            elif type(element) == sg.PySimpleGUI.Slider:
                                table.set_by_index(int(values[event]) - 1)
                                changed=True
                            elif type(element) == sg.PySimpleGUI.Combo:
                                row = values[event]
                                table.set_by_pk(row.get_pk())
                                changed=True
                            elif type(element) is sg.PySimpleGUI.Table:
                                index = values[event][0]
                                pk = self.window[event].Values[index][0]
                                table.set_by_pk(pk, True)
                                changed=True
                            if changed:
                                if 'record_changed' in table.callbacks.keys():
                                    table.callbacks['record_changed'](self, self.window)
                            return changed
        return False

    def update_element_states(self, table_name:str, disable:bool=None, visible:bool=None) -> None:
        """
        Disable/enable and/or show/hide all elements assocated with a query.

        :param table_name: table name assocated with elements to disable/enable
        :type table_name: str
        :param disable: True/False to disable/enable element(s), None for no change
        :type disable: bool
        :param visible: True/False to make elements visible or not, None for no change
        :returns: None
        :rtype: None
        """
        for c in self.element_map:
            if c['query'].table != table_name:
                continue
            element=c['element']
            if type(element) is sg.PySimpleGUI.InputText or type(element) is sg.PySimpleGUI.MLine or type(
                    element) is sg.PySimpleGUI.Combo or type(element) is sg.PySimpleGUI.Checkbox:
                #if element.Key in self.window.key_dict.keys():
                logger.debug(f'Updating element {element.Key} to disabled: {disable}, visiblie: {visible}')
                if disable is not None:
                    element.update(disabled=disable)
                if visible is not None:
                    element.update(visible=visible)

# RECORD SELECTOR ICONS
_iconpack = {
    'ss_text' : {
        'edit_protect' : '\U0001F512',
        'quick_edit' : '\u270E',
        'save' : '\U0001f4be',
        'first' : '\u2770',
        'previous' : '\u276C',
        'next' : '\u276D',
        'last' : '\u2771',
        'insert' : '\u271A',
        'delete' : '\u274E',
        'duplicate' : '\u274F',
        'search' : 'Search',
    },
    'ss_small' : {
        'edit_protect' : b'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',
        'quick_edit' : b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAGJ3pUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjarVdZsuQmEPznFD4CVSwFx2GN8A18fCeiUG/zZtoRfnrdQoCKpDJJaDP++Xuav/DH7L3xQVLMMVr8+ewzFxSS3X/5+ibrr299sKfwUm/uBkaVw93tRynav6A+PF44Y1B9rTdJWzhpILoDX39ujbzK/Rkk6nnXk9dAeexCzEmeoVYN1LTjBUU//oa1b+vZvFQIstQDBnLMw5Gz13faCNz+FHwSvlGPftZllJ0jc92iBkNCXqZ3J9A+J+glyadk3rN/l96Sz0Xr3Vsuo+YIhV82UHird/cw/DywuxHxa0MaVj6mo585e5pz7NkVH5HRqIq6kk0nDDpWpNxdr0Vcgk9AWa4r40q22AbKu2224mqUicHKNOSpU6FJ47o3aoDoebDgztzYXXXJCWduYImcXxdNFjDWwSC7xsOAM+/4xkLXuPkar1HCyJ3QlQnBCK/8eJnfNf6Xy8zZVorIpjtXwMVL14CxmFvf6AVCaCpv4UrwuZR++6SfJVWPbivNCRMstu4QNdBDW+7i2aFfwH0vITLSNQBShLEDwJADAzaSCxTJCrMQIY8JBBUgZ+e5ggEKgTtAssfSYCOMJYOx8Y7Q1ZcDR17V8CYQEVx0Am6wpkCW9wH6EZ+goRJc8CGEGCQkE3Io0UUfQ4xR4jK5Ik68BIkikiRLSS75FFJMklLKqWTODh4YcsySU865FDYFAxXEKuhfUFO5uuprqLFKTTXX0iCf5ltosUlLLbfSubsOm+ixS0899zLIDDjF8COMOGSkkUeZ0Np0088w45SZZp7lZk1Z/bj+A2ukrPHF1OonN2uoNSInBC07CYszMMaewLgsBiBoXpzZRN7zYm5xZjNjUQQGyLC4MZ0WY6DQD+Iw6ebuwdxXvJmQvuKN/8ScWdT9H8wZUPfJ2y9Y62ufaxdjexWunFqH1Yf2kYrhVNamVr66TynlKlOengN5/LcEGP4KxHWInT2n0cr1xiiwKpqr29qb9N20X8QeqQ3otEeYEQ7Zhv8Wzwe+GvfAM1dnenTIwYWrtgGOx36Irqbh40boXZ/c+kIE7qMbO5TnvkHCis3bIDg8XHF6chNb7J6V/eJuroIbTVENSTP6svMDvy+0XHshmR5tTeD9qwlyrVEs7X5E0/jiNv4MvwpXtAz1F4VY69XV55qzhkiIP1hDlCaIj5JZ+dfAn3fpUV9AbzzYncCMhbdhYrPaWRmmYguAmve8cpu2VdHBGCsm00U61EoTqyfs9zP14vf0cU5C6rcg13kE60uVNti9of4BbOgHbANYYzUJt84cKNukAodmqmTNMBLk9wvSoRSXe1bEZubhaYjSBE35JHSTNtBx5x2ScjsdEf1fUJcVyvwAex7YEbB1cTTvdw+mEx6nIIVviHQJ0ZZpSHCJoUsI0lEhYL7DteDKESzAt+ULu6dtZnabpu1Pes7vunUgfbfDXfDQqtO8IsuKgszGA2KVNktdJxhEa1Snj8jMR05JjkhNsSKauQ6XcXDArCKssNX4G60e+mGIXczhuFvvd3icEarivBezf8WCwg2XdgGn2q0RbEJasLQXHza31s6oiYH0trbDzzxSb9ZIoDMVGM4YpMRikr2pC1xHeS2cmjunis2g5N5QYkJnSR43KwREPRx4/hOeeeAcVTsi2zNAMAp7Yl363YQDk8p7DLa6uvlCYF4pP5z4Uwib+pK8Tgp7+4hBZYUj1vBtJ/u35j530Vs15+bF6eLBjymhtucH0MVI9aq82poT5TAm/Lx8T522rV9Km1ZWnYRiE1Z/3WxjfDfCF3vQfK+6RjQQeir12E0Rqg8tgBp1y1axTSVtkpyJuko2azhjb61AfnL4TaDOvsnvpztN6X350aqrGoxP4zEXbQkZvzwUUIIyovDRCk4dDe6x9/413X6sYeak4u7rwX23S5on2+n9eHQ+/jdDP63l1n05sPPJSvTdbOsW6nCMWxTw4kCqieHKAqnnDpwUZ+Yft+wPTyz3+rv97qRR3MOS0m2C1by7oDu7dcR2FV6PSH8+RHwiuhNST0LKAXLOMtTqw5eiOWV3V9LZYb4V0nU3v1QYzoHmX+RGJBpl98L8AAABhGlDQ1BJQ0MgcHJvZmlsZQAAeJx9kT1Iw0AcxV9TpUVaBO0gopChOlkQFXHUKhShQqgVWnUwufQLmjQkLS6OgmvBwY/FqoOLs64OroIg+AHi5Oik6CIl/i8ptIjx4Lgf7+497t4BQqPMNKtrHND0qplKxMVMdlUMvCKIPoQxDFFmljEnSUl4jq97+Ph6F+NZ3uf+HGE1ZzHAJxLPMsOsEm8QT29WDc77xBFWlFXic+Ixky5I/Mh1xeU3zgWHBZ4ZMdOpeeIIsVjoYKWDWdHUiKeIo6qmU76QcVnlvMVZK9dY6578haGcvrLMdZpDSGARS5AgQkENJZRRRYxWnRQLKdqPe/gHHb9ELoVcJTByLKACDbLjB/+D391a+ckJNykUB7pfbPtjBAjsAs26bX8f23bzBPA/A1d6219pADOfpNfbWvQI6N0GLq7bmrIHXO4AA0+GbMqO5Kcp5PPA+xl9UxbovwV61tzeWvs4fQDS1FXyBjg4BEYLlL3u8e5gZ2//nmn19wNkDXKhWfC+CAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+QIEg0fJQXnbmsAAAKVSURBVDjLhZJPSFRRFMa/c++b55tGTZpSMZRStCyJFlEoLkSyWtQiyI1FUWRtIooWFS2yKHcG0aICN1IWCNWmQhfixqQokDAHpY3lFJiTZo7ju/e9e0+LwP6o9W3O6vvxfeccwjK6dPEirrS2IkmUE2loeCGkTBFwjIAxw4yinh4AAC0HMIlbSL0zmHs72SV7extldjaElDOS6CoDNwCgsLsbYjmA+q6Rk//xaN6p5kbRfIJDIjZK5YbWtjHQWRCNYqS+fukEmQebIYQTD3R6eJ7z883W83C8LZRpucRIJkl6HtZWVNBIIgH5t3n2fhUIBmxNu1K6WmdSUIl2aJLIab4MGEFhcvz41OfPgyGwuIIkA0Cc01o1KaXBzIC7Clnjd2j2yWFS1WsSBR2POiURNvX1/arw6W4ZYlEHjqD1YaAH5+f9XCEIvq8QiTgAiIIgNGZ4stDZ1ZIqaWwBfk9QFJdwBcOEpsv31UoiwFoGEUFKB8YYWLb7Ubk6FSZvLyQWAPD+1WPM2HKExlxXyt9mrWE34pIxhqJRD9ZastZ2Z2a/Pg2NRenZiQUAAUDHbmBvEzayj0FfF3qx2ArWWpMQPwMqpWbSGbXGy3KCdWdSf+xMAMDBZxorD5kGt67b8/KqGDwHImIpBRsTGiLsiXpuMOcvPrlYGMzlXulOxPbdI17biCwxTsYwMXOn6zovBQGbL6SWBjAzAGwgMNjNY7fuJnj7QxhZ8EFk5RxRyqL49JclP1YCgNYa/f3910pKSvLi8Tjp+TR9Q36XjhYf4NmxtFQTaHueXhJAZWVlcF0X1loeHR0NBgYG3sRisZORSGTo29QUampr8S8Jay2mp6dzieh1ZWXljpqamtogCIbCMPyvGQB+AKK0L000MH1KAAAAAElFTkSuQmCC',
        'save' : b'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',
        'first' : b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAHJHpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjarVdbkiQnDPznFD4CQoDgODwjfAMf3wmI6p7Z3vXa4anpgqJASJl6UGb89ec0f+DPefLGB0kxx2jx57PPrqCT7PnL+07W77s+2Nv5Mm6eFw5DjJbPoxSdXzAeXgvuHlS/jpukb1xSQVeyCuS1s0OnvyuJcXfGyaugPE4n5iTvqlZ32qYTtyr6Y9miHyHr2bwPeAFKPWAWOzeY2O57Ohrw+RX8Eu4YxzzLGX1mMmgCXxQByBfzHgDtO0BfQL498x39p/cNfFd0nL9hGRUjdD6+oPAZ/A3x28b8aOS+vZCH4R9AnrOnOcexrvgIRKN6lDUXnbUGEysg570s4hL8Avqyr4wr2WIbyOm22YqrUSYHVqYhT50KTRq7bdSgonfDCVrnmuM9llhcdg0sEft10XQCxjoYdNzcMKDOs3t0ob1v3vs1Sti5E6Y6gjDCkp9e5lcv/81l5mwLIrLpwQp6ueW5UGMxt+6YBUJoKm9hA3wvpd+++c9yVY9pC+YEA4utR0QN9PIt3jwz5gW0JyrISFcBgAh7ByhDDAZsJA4UyYpzQgQcEwgq0NyxdxUMUAiuQ0nnmaMz4hAy2BtrhPZcF1x0axi5CUQEjizgBjEFsrwP8B/xCT5UAgcfQohBQjIhhxI5+hhijBJXkivC4iVIFJEkWUri5FNIMUlKKaeSXWbkwJBjlpxyzqU4U7BRgayC+QUj1VWuvoYaq9RUcy0N7tN8Cy02aanlVrrr3JEmeuzSU8+9DDIDmWL4EUYcMtLIo0z42uTpZ5hxykwzz/Kwpqz+cP0L1khZc5upNU8e1jBqRK4IWukkLM7AGAoDGJfFABzaLc5sIu/dYm5xZrNDUAQHJcPixnRajIFCP8iFSQ93L+Z+izcT0m/x5v6JObOo+z+YM6DuR94+sNZXnWubsROFC1PLiD7MKS4Z/KzFbbU8nu5raM5vQ59b8/+ISSjZu4Xey4LdnYV4SCrkA/4RxbGvDoVE3QXeC0tr7Swszk+pS6Pi6hA/i3Vtz/fNPrJt2ctqn8imTmVAh9PLKbXTq8Im21liPKrkyiO3K+Z7O++ridI6xJaqKmfqLZitdHMgPiL7r4eaG1Q8hkmgVuAnx7YRaaQ8Qj7vspdSkM/2owkrsw2i4cJ53VFOmtRjZ5gZOg5/NvepwUa11nMDlmWcx2F8m9X/jAoeMerEDH+K7A4fvY3AI51pFd41ksEeh+Fa/YhYqVs0zx1lyyks2I/tGAfMMRiZYW4t4ZubXxz9EGHNX65zHqkqBE0kT/Zqox+Sh/R81ksLeUx7eLZ2Czqd3dJk7rquSEM9PsAheIDi0B0SEF4F88zsXhjrTFZCKI+errxR5awBNNJc7kHVchY0SFCtmLqVfLY2YUBbdlJ1gwG1ghOgqSRCFVgYg2pKi/D0MumraVDNX5OgQoePHTGeGnS4WjMNeCVfk5CQl8cdc41HxpFaL6JWcKBR/7Mhl6PXSsSHvoEEh5x1kCvIokU1MMMDRWg01TLkowhL3AuU7j5Ycg254HmzLMmZryWL4375t0tbuu9QCCcXtdLmtb2nZ3uD6OgKZBtIpKzoyJJ59PIr0o+AgsrQ2428PBoN2/cCI9UjKJF2laWW4HLjSFsn8K8t1Fd0u4NhKBZdNzDAvV4FoUWmFoMmARvVJZAAAiHDH7ZwPqEXFq2diDYB5enuF+SkrtTSKBpWFsdEbqwZKyDkEmrB0ASGxFROwjIfM1h9z2D+Jl2UL4ByVKHcwcNhJaJWTvPOA44PvqmZiN5o6wt42296vfulqEnb9q45OyUkhuZVjWBhz6iaXEZALs6/SFia6MxIyFjwuaPIKtplXohX0F/tVzhoikW/Dq+BWz2W1NnNcZQJSe0WBHwYaD1ZJ0etOV3TYQYP0F4rl7cDMDZ7y1FAOUr/rP7Wflzn9IiDerwRnxvmwT6s0HmQB+w29uttmZLGKXK4dH7Mwoc1InuX7Bo5t8cUtXydf1BX1OsiDh9wfX1qlT65vnn5fn0yGWpOcOqbSIByAGkLkKKYNSQmxQmhjIJipndaqIhb53LLT/c40ECg+jBq20RmhE+ojwsKOng8T90PAx9Va/Zh7GDUC4yD674ZU34Rx/OUo1V0oV3w6rqIXC2s6/vh0IJkObn2NyYQlkpMht9TM+UeWeAhZxGCuz9xLBhTiqCw1eCtOMs4BSHgcNvG9qN7DvGzalh/CGS6Rb4gqAVLFWoG0X64eAT1FOUyH/Fl2RVRakgc32V2PTSVNJCw1FwyhCMWaWabKDA4NkQNPAeHHf0e1uzrdINqja9gOTGptcCsTn4IsPyFE9Y4ya/CIcf4URGSM9QnAA2O8yeS8B3/xqgGOr4lNG4Hsszp4UNEDzcePtL1dGCgfj4qpvgzV/md1vzXhV98cs5pOuw3fwPVcY49zw+VVAAAAYRpQ0NQSUNDIHByb2ZpbGUAAHicfZE9SMNAHMVfU6VFWgTtIKKQoTpZEBVx1CoUoUKoFVp1MLn0C5o0JC0ujoJrwcGPxaqDi7OuDq6CIPgB4uTopOgiJf4vKbSI8eC4H+/uPe7eAUKjzDSraxzQ9KqZSsTFTHZVDLwiiD6EMQxRZpYxJ0lJeI6ve/j4ehfjWd7n/hxhNWcxwCcSzzLDrBJvEE9vVg3O+8QRVpRV4nPiMZMuSPzIdcXlN84FhwWeGTHTqXniCLFY6GClg1nR1IiniKOqplO+kHFZ5bzFWSvXWOue/IWhnL6yzHWaQ0hgEUuQIEJBDSWUUUWMVp0UCynaj3v4Bx2/RC6FXCUwciygAg2y4wf/g9/dWvnJCTcpFAe6X2z7YwQI7ALNum1/H9t28wTwPwNXettfaQAzn6TX21r0COjdBi6u25qyB1zuAANPhmzKjuSnKeTzwPsZfVMW6L8Fetbc3lr7OH0A0tRV8gY4OARGC5S97vHuYGdv/55p9fcDZA1yoVnwvggAAAAGYktHRAAAAAAAAPlDu38AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfkCBINHzPxM9s6AAACZ0lEQVQ4y6WTTUhUURTHf/e9N/PemxmnydGgUkvLzEhLcyEG5SYwgqKs3BhCEYiB7SKqVZG4MAhcGLUKXLQRw0X7ojZZiz7IjAGxxUBj2jif+mbevS1mpiKnVWd1zrn3/vify/kLpRQAQggASvXf8a9zoZRCKcWJseesJFM0Vwf5nllHCkNMDXcqy7IBuDDxWuCkVc5VvIvFmRs9A4BWosdTaeI5OVFX5Vd+j6Fq9naow5dHEUJw/v5LJoc8KmgZX7aFrNTnRC5cUqCVkmVHMh936rra6wkHLR6eCu5cS/3g9L0XJDMZLo4nIt8ybuPRgzVZZuPmBoBRqGQyK1nPF3qfno4zvdBGpd8bad9X0zAVc8jkFJi//8AoJR4BCMgqhVvsHbvzjC3Bt5FN4dCuJx9iNIV8ZHMS/IINCjRAF+BIDUnhQihgzbc2ba1ZSEuqAhaVfpO1vAJPGQW6gLAGjhQoBL3XH/TU1m/f8yrqELQtAILorLkKDFVOgcJC4qAjBUyNDr6xV6Oz4Qob0/Riml4Clo2jNBDuRoBAYaDICw1VGGHp7sDNszIamamwTGyvl4Bt4rgClCwHAAOFxIMqbl1lbezr46s9w7az+t7yWfhsL3mhg3LLA3RA6gZCFParuqUbbqcWx861nFyOzM0ELKsAyJcBGJrA1kUykUwnc/mcC2Q1oeN71AWwOHmle9hNLH9MptcTgQpdlrxByQsD0yt0XBrZQXN/Z2PvjUN/wgN1rdwCaOpvMI8Mth3ou+Ytvf1lJk3TikMU5YV3M9h3nNb9zQAMDY0AUUCCCLC09JWq8OYC4H/iJ/tM8z9RaTk0AAAAAElFTkSuQmCC',
        'previous' : b'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',
        'next' : b'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',
        'last' : b'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',
        'insert' : b'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',
        'delete' : b' ',
        'duplicate' : b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw1AUhU9TRZGKQztIcchQnSyIFnHUKhShQqgVWnUweekfNGlIUlwcBdeCgz+LVQcXZ10dXAVB8AfE1cVJ0UVKvC8ptIjxwuN9nHfP4b37AKFZZZrVMwFoum1mUkkxl18V+14hIIAwokjIzDLmJCkN3/q6p16quzjP8u/7swbVgsWAgEg8ywzTJt4gnt60Dc77xBFWllXic+Jxky5I/Mh1xeM3ziWXBZ4ZMbOZeeIIsVjqYqWLWdnUiBPEMVXTKV/Ieaxy3uKsVeusfU/+wlBBX1nmOq0RpLCIJUgQoaCOCqqwEaddJ8VChs6TPv6o65fIpZCrAkaOBdSgQXb94H/we7ZWcWrSSwolgd4Xx/kYBfp2gVbDcb6PHad1AgSfgSu94681gZlP0hsdLXYEDG0DF9cdTdkDLneA4SdDNmVXCtISikXg/Yy+KQ+Eb4GBNW9u7XOcPgBZmlX6Bjg4BMZKlL3u8+7+7rn929Oe3w9rHnKk7x4JKQAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+cCARMnD1HzB0IAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAABJUlEQVQ4y6WTT2qDQBTGvxnLwFTETZfZZCu9hPdwJei2B3GThZcovUJAkx6hdXqBisxOycI/YF43VWxiTEo+eAy8gW9+35sZMMYeAWxM0zwAoEvFOSfbtvcA1piIAdhEUfTieR4451iSUgqu634BcMamaZqHoihoqqZpLtYv0WpqTFprIiLK85x836elKJP6GOKMBr7vU5ZldIuSJCEhxHY0GPBuldaaDMOg5akBqOsaYRjO7vV9j6sEZVnO9rXWBIAelk7uug5VVQHAuEopIYTA2S2cEgRBMDv9OI7/EIBzflcEblnWu1IK92gNQA2Ip2rbdsSeI5garf77DqSUx+ktfAP4TNP02XGcq9i73Q51Xb+dxRFCbA3DWPwHUsojgFfG2NMPCKbWh17KiKEAAAAASUVORK5CYII=',
        'search': 'Search',
        },
    'ss_large' : {
        'edit_protect' : b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAADqUlEQVR42qWUW2gUVxjHvzO7O7ubzDYWUZIGY7IumqiNTdHYSB9UlJS+tIog+OAFfKqJVqqg1Ni8iW0JVKxCHrRiElvSFsGHCM2lERWjsKKBjbrgBY2ablxwL9ndc/U7k1WISepuPHCYmd2Z/++c3/fNEJjBeLxuXa3DMC4QwxgmANuLL168Pd29JN/wO/X1VgFjQQwPGIaBByONIU0JzlsC3d3yvQD3hkKGeW3gL9XW9rUhpdIAJAAeFZ79i7fswN08mjEgGTq3k5Z80ZoMhYCfOAEwPKzD7ZkFvcTABoS05w1ItC+dTUDcTs36rMS5vIlQZira0UF4V5cOUVldGiRjjC2o7O19mBdgrGNJJ6OZTZRmVAp8xLWiWbnmrSVycBBSx48rFY3aAFT1IiaEf3FfXyxnAIZvFZz+lslQoJSClAIoEwDlG6Fw5UEwlQmp1lagly4BcTg2z+/p6cxZUbJ98Xwl+S0MLtIApaTiXBAhxHiRfRWgPj2sfGWrCAkGuz5cv/7LnIv84OQiY46P9KCa1TpcSokApRhj+jnldruJ/o1ypXhgR8Rauu3zkvKqcM6ARFvlfs7oUa2FMQ5OpwMVMcDVg2m6AHsUOOf6Wklw1vv3jnS/nTEtIH520TIpxDUsqhsBxOVyYZDUNbDVuN2mrUoDcBe/lO998e1UOVMCnrYucFtu4zqGfYwAu88djvHV68CCAq8N0+c4Q6hoxcL90VTOgNiZwM+o5Ltsxyivt4AwRm0AqrF3gP/jDjjF1a/C1QenMzEJ8PJMYA2q+QeL6sBigmUVKikkySCM4N2mqdVwVCMUuv++bE/kyP/VcQIgPPC3Z+6TX++kI3fLtHev14OFdSl9rnV4PB67oOMAfjk2JlcvOTAqcgYwlqlTLHUlduUHoOFO+MBn2S9WVg22KGS7hsexBjXzdv93H94xJgDw4c3Y5r+jVyWe9BB+oxlo/DnGEqJbNFtUVCN3ljY8P/Wu8KkA+xDwkwbgJHIsApmBQ8oZuaqdv179+ZJvnm3IJXwqwDEENOi3c8K8/yfwmz8CS8dHsAGqP9r1LDIjwOjo6PmioqKv3uxgHKC/DiQ5MpRhN5o3lG3B73MeYwKgtrY2WFdXV9PY2KhKS0ttQDqdFtFo9I9kItH8SU1NOJ/wSYCqqiq99dmWZUFLS4uGXEgkEk3V1dWD+QZPAvj9/kLs8zjq0Fq6i4uLm/r7+wdmGjwJUFFRsRDf0tMYfigcDve9b/Dr8QptdEU3XH9lbwAAAABJRU5ErkJggg==',
        'quick_edit' : b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAADqUlEQVR42qWUW2gUVxjHvzO7O7ubzDYWUZIGY7IumqiNTdHYSB9UlJS+tIog+OAFfKqJVqqg1Ni8iW0JVKxCHrRiElvSFsGHCM2lERWjsKKBjbrgBY2ablxwL9ndc/U7k1WISepuPHCYmd2Z/++c3/fNEJjBeLxuXa3DMC4QwxgmANuLL168Pd29JN/wO/X1VgFjQQwPGIaBByONIU0JzlsC3d3yvQD3hkKGeW3gL9XW9rUhpdIAJAAeFZ79i7fswN08mjEgGTq3k5Z80ZoMhYCfOAEwPKzD7ZkFvcTABoS05w1ItC+dTUDcTs36rMS5vIlQZira0UF4V5cOUVldGiRjjC2o7O19mBdgrGNJJ6OZTZRmVAp8xLWiWbnmrSVycBBSx48rFY3aAFT1IiaEf3FfXyxnAIZvFZz+lslQoJSClAIoEwDlG6Fw5UEwlQmp1lagly4BcTg2z+/p6cxZUbJ98Xwl+S0MLtIApaTiXBAhxHiRfRWgPj2sfGWrCAkGuz5cv/7LnIv84OQiY46P9KCa1TpcSokApRhj+jnldruJ/o1ypXhgR8Rauu3zkvKqcM6ARFvlfs7oUa2FMQ5OpwMVMcDVg2m6AHsUOOf6Wklw1vv3jnS/nTEtIH520TIpxDUsqhsBxOVyYZDUNbDVuN2mrUoDcBe/lO998e1UOVMCnrYucFtu4zqGfYwAu88djvHV68CCAq8N0+c4Q6hoxcL90VTOgNiZwM+o5Ltsxyivt4AwRm0AqrF3gP/jDjjF1a/C1QenMzEJ8PJMYA2q+QeL6sBigmUVKikkySCM4N2mqdVwVCMUuv++bE/kyP/VcQIgPPC3Z+6TX++kI3fLtHev14OFdSl9rnV4PB67oOMAfjk2JlcvOTAqcgYwlqlTLHUlduUHoOFO+MBn2S9WVg22KGS7hsexBjXzdv93H94xJgDw4c3Y5r+jVyWe9BB+oxlo/DnGEqJbNFtUVCN3ljY8P/Wu8KkA+xDwkwbgJHIsApmBQ8oZuaqdv179+ZJvnm3IJXwqwDEENOi3c8K8/yfwmz8CS8dHsAGqP9r1LDIjwOjo6PmioqKv3uxgHKC/DiQ5MpRhN5o3lG3B73MeYwKgtrY2WFdXV9PY2KhKS0ttQDqdFtFo9I9kItH8SU1NOJ/wSYCqqiq99dmWZUFLS4uGXEgkEk3V1dWD+QZPAvj9/kLs8zjq0Fq6i4uLm/r7+wdmGjwJUFFRsRDf0tMYfigcDve9b/Dr8QptdEU3XH9lbwAAAABJRU5ErkJggg==',
        'save' : b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAEp0lEQVR42qWWf0zUZRzH35+7+95PDksKmagXjCTAUNB+2FbN1XZXNJrhlo7MLF1WGksry7GiVm6pqS1tmStnxrIRFVaKrGmuqWvNIA1EYR5gkK418Hvc7/ve0+f5fr8I+ef53T483+fZ7v269/t5ns9BMJ5crhe5yrgsyOzZxHX82kXiyoPN9ivur52OKbMIpOuLe6dZqSrPjiyPW3jcTnI7HXA6HFAUm0in0xRPJEQ0lqBwNIbm7kHRtuPdEMJqNX/22LWALbhv+ToULhTmXAcsutNNK0qzMMnrEd4sN3lcLricdhCRGFIjdPofVSTjcfJZNHzSqYqmX7oILfWjGL3yKH/+yETAQTyyKYCcYp6RsWK1YMndWXiu/AZke9zsQoo7odisSCSTaDl/CS8f78UkxYJd5TnY0xPFdx1JIDEAfLshitDIQlZpGwMcQtXmAHKLBWwW4mIAiWV3eWnN7Bx4OSK3y0kOu4KUpiEai4sfugep/li3yFastLniZjT2p8SPPVaClgZifwk0r49BHa6R2gageksAU0sYYDUANhIr5nnppcpccDTC6bTr0cViCYSjUXGo8yJtPNopsuxWqq/Mw9eXINqG3IQUp5xKC8QGCU2vxnHl30UGoGabH9NKoYsrFn1cVenFK3PzOBoHc62IJ5KI8IaGwhG0911GS0cQlNawIN+DA8N2/KxOluJGJbmGfgeaXj9sABZ/EMCMMgFlzIFFrJ6TRa/Ny4edT00ypVE0FsNoOAo1HBGqGqYRNSSG1RANj4TQGnGLE1o+mQCBpEYY6AT217UagKU7AvAxwG4CFKuY7NDoJoe8FRYhICgtj5ZIc8z8V0uTpmkizWAtpWEUDhElF7HwOKCPAftWm4CnP/KjYBYL26T41Zh4LyRAbvr4CdMPsU4DWAua+H80EiIreAb47Hkzomc/DqCwXHdwz/RszJ/qFSxKsOjCgu826YBxeWKAQFofJUgwiE4OhXDioiqQYAcXTgO7VpkOXtjtR1E5GIDztWUoynZk1Ct61ThmNnI0CXbQy4CdK00HdZ8GMHO27iC4uBQ+jyL4xupfmb/o1feJ84nrY+99owkU7O8yHPT8AWx/xnSwbk8AxXMEHAx4rPj6AN+cE4gz4FwH8P5yE7B+bwC3mYDqW+FzZwgIM+BAjwHoZsB7y0zAhs/9KKkAAxB8uIgBtoz2oD+SQsHBXjAAONsObHzS3IP6fQGUVgg4bRT0F8LnsmXmIJJEweEL3CrYQRff5HeWmg7e+CKAskoD8OAt1wf4qY8BKUInA95+wgQ0NPoZAAYg+IAPPmeGEcU4oiP9QDQFHdBQa0b0VqPhwKVQcMEMCcjMQZQdHB0wHPx5CnizttX4wWlofAi3z9Uj2lt2I6qmeMVYY+B7KiY0iavzietj799fDuGpzmEDcOaUdKD/HmzDkrV1qFmpRyRPEmRCcnSYc7tZivn/gOw58rbKkicmnjJGHvQ1GVHzbuDLrdslIB+K/Tc8viYPFfMJLocU1e+EKW60cSlutRhdjvsOQ4yuaUCMsy/fI3GB9pOErz78G8nEHWPW87nWcpVgQhwZPrIZnuXayjX4H7Qeh+TT7afMAAAAAElFTkSuQmCC',
        'first' : b' ',
        'previous' : b' ',
        'next' : b' ',
        'last' : b' ',
        'insert' : b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAFJElEQVR42qWWa1CUZRTHz3n3wrLLAnKNi7dEYCW5Vo4gaJI2pgx8yIb64ocosssoVqbN9KmZGMsBHafM5Itfisk+wKA5XlMR7AaIhgsIiYTI6rLALqwv7767p/O+LMiOaUXPzH9299lnz+85/+e851mExw89ax2rkJXGivLP21kdrLOs0yzpUQHwEfMG1jbQYAUui4xhISaYQRumTAPJYyLSbRfR9WFk2cBL1Ty/nyX+G0AGq1abF5caUpQMuZYcejbWgknhiRCqN6kApzSBPaMD9IvNis3WFhhv6Ca56U4Xf1fKan8cYC0atXXGMkvIyjV5ULykgIMapxZh4GIiFr86JTfU916Ey+ebwF1jHSe3XMLT5/4OkMHBGyM+yDBvyC2k7JhUFDgEIpDocaPD7ZiJrfwuwhhBBp0RFZAPkFrvduKJ5rPg+LzdxZD86UymAQZ+1xZVkZaav3YVpEctJQEJWSAwYFlEKpY8WeTfORHyqPujga47OtGnAAiJIXj1Xjc0nmsie3VHF28jSzmTacCH5tWxlZat2bAqPpvPlkAjAEwBiIHp8NKS0gAvv++thav2q0pwVV4f8FkjXBpsBevBFnBduLubl+1RAHrUYH9SVWZMTvJyjDRwtXDiGoF4WoVQRvTT+EryawEZfNtdQ+33WlANTkAcHGUfgkN00W/d17BnxxUbTy5QABtDc8KPWXZaKC0iCXUCgVYgYgj6s6Cs6JX4asq7AYBvug5Q273L6N89yX6Ax4fU4ehB62dWcLaMblIAVYvLFm5P2jgfEkxRoOegC4OfUrwH/yGDJWo5bFzycoBFx3u/A6v9GvgPWX3tE38HyQswOGGHGz/8CTcP39qnAE5mV6asT0ibR2wPmnRaOLD6uLrL2Tt+UJ5Tn2fPT79/5/yLMOHxkEMcx4GOEWjd3XVKWdBScMiSFZ0YDGF6A5h0Othf8CPMZWy7+By4PR4YlUSwD9yHC+XWNhWwviYlOzJBR2a9HkM4g72rfppTBu81roBxzsAleXD4tgdOlXW1qhatq17MFhnIpAMG6KEyt21OgF1NmQyQyO0BtkiE0xU3VYuqcrc9UZFeHEbBGi8adQI8E7uJuJKQpTwTFGfMwrTILQGAjuEjNORuQ64e4OohFv5qO8YW+Uj0arC9fgya9w9Vq2W6KC+koeTTOAjWelk+MLCCNFPSCT5ICi+G/LiDAX433tkKPaP1XJYCTHqRpQFRFuC+X3UfDUFf03iR+qAJWuh/8+jCmJh45HakALxk0PjQD6FFoSW4IvbrgAx+tr1Bfc46lLwCiF6Bdy2gKGuU4GQbJPxq8y2bT4YFM60iu9hcufnjeSrAqCXiLNDgBywwF2NG1OEAQLv9dep31c8AODC6ZQQ3A45+MoKt9a5d061iptmVfxGdkpmvAzOXqlEHEOy3Kd5UBMnhXwZY1D36Fj9QDWwNW8LigwUXl+iVRgkOvW1/qNmp7doYipd2HokMsaQFUXiQkg0BZ8HZACo+cn9Sk/DygUo+mUQZUFQAMtLI5Ah2dkzCni3DLreTHmrXMxeOKQzrd+wLNeUXhmJkUCLbpSfOAvWcidJlVQCbxNYQ755tkWB4coAazzqxarvTNTFGj7xwHlw8CLUbSvUp5e8bYOmiaDDro7m6wrgagtQFkm+Sdz0GLuku3Oizw6G9Ipyolbq4H/3jlTk91Etfq4OKguc1MYUvIOZkEsyPV9oaUP+ggK1XkM6cJLx4xmuTPfCfLv3Z43//bfkLo1muAZZ9QHcAAAAASUVORK5CYII=',
        'delete' : b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAEe0lEQVR42rWV21MTVxzHf5sLWQJjEyBAIgkhQHBEHNAXL0/MtC9KbRWofUz+AP8gn3bfnKojrZfptF4Yp30oF2VRp6ZAS7jkTshuuAUDpN9zyIbUALUPzczOZnd2P5/fOb/vOSvQ//wTyi9+aGqyWez2UdHh6NVmZoJfJRLyp0BGnM6A49w5KRsOK9urq/3XYzG1QsDgYmPjaM+tW71GUaQ/79+n5Ph48Ot4XP43uHdgQHJeukRr8/P0x507yof19f4bRYnwMVwQBIo+fUqmEydIC4dp9d27IyUPAAdYart6lZIvXtCHbJbIYqGYoij5jY3+G9GoygU/9fRM6fClx49pBw8aqqpIdLn2JaFQEMOWP4Y3X7wo+YrwtVCI37c4nUSYgdj0tPLl7GwfF4wNDRW8167R4sgI5VWMrFCgwt4eGcxmEk+eJG1hgTLoiS554HIFmi9ckHxXrlAK8GwRzt5j71g7Oig1M0OfT04K+hQFGlGNaLNRbnGR9nZ2iHZ38fz+C9UtLaQuLVFmbi7Ini/Bnz+ntffvDxJjMJDY2kobmkYJjHowGpVLTf6+uTng6OuTrA0NtIWKuQSjYAebLite1FIpMtfUEJuW1LNnfFoEFMHhRiOJHg+tMzhGOxiJyBUxZZKGs2cla309lxQg4QAmQfNq2tvJgbSkX76k7Nu3VMjnOYDBq71e2tjcpDhGqcMrBKVMnzkjWe12LiE2Ekh2WUJwNppMlItEiAXCwA5cWzs7aQOyVCIRHCqDHyrQJQ2nT0vW2lrSXr+mnXR6v4GYYwYVimfWnxo0lMET8XgF/EiBnpTP6uslC4a/NTfHp4pDGZwdDI7K1xk8FgsOLS8fulaOFdT5/ZLn8mUK375NlMsdwHGwZrOpiSeTpKpqcPi/CHR4O6KYffWK4k+ecKixCDdiVOy/CZJqTFE0FqPVTOZQiXAsfHKSYoDrYBPiygV4zgCJSZcgXQvRKK2k08FvlpaObjJb/jpcQ+UxbBt65Qxe4/fz/3lsH0zCR8JEkIg+H/2FxZhMpf4hKQlYcmyAdwCussqLcF45GloL+CZWN7u2W620gwgziQkx5RLcq2pro1nsqHEmWVw8WGgMbgecLX91YoKijx6VKjeicgbfAhxRlNm6dbvdgQZEeA8VG5Esc1FihMSMFf87UhfFmrgJCRf8fP584dTwMGVQeeThw4NmFivPAZ5MJmVEke9F99xuqa21NeDAlk7Ly7wXJUl1NQluN41PT9MXb97sb3Y/dndPdQwM9M7LMu1mMgfwri4OX2HwSCRY3q+7kHT6fAEnVryAlW0GnEtwna+ro1/GxpSBUKhPnyKbyWIZteFTuY2K9rAtMPg29qB0KlUBL5ec8vsDLQAaEdUqnKmpiX6dmFAQ2/6bCwuqUJYgm1kUR+2QCKh6G3tQZmVFHjwCXpJ4PFJ3V1fAAzBh1L9NTSlpwL8FvDKmLpcNiRnF9PTmNjdl7OfHwvXfd5B40XhtbU1Z1bQS/KiFZsPJi++p8inwMkkvTmEkRy2//zcpYDQ3Hbr/xQAAAABJRU5ErkJggg==',
        'duplicate' : b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw1AUhU9TRZGKQztIcchQnSyIFnHUKhShQqgVWnUweekfNGlIUlwcBdeCgz+LVQcXZ10dXAVB8AfE1cVJ0UVKvC8ptIjxwuN9nHfP4b37AKFZZZrVMwFoum1mUkkxl18V+14hIIAwokjIzDLmJCkN3/q6p16quzjP8u/7swbVgsWAgEg8ywzTJt4gnt60Dc77xBFWllXic+Jxky5I/Mh1xeM3ziWXBZ4ZMbOZeeIIsVjqYqWLWdnUiBPEMVXTKV/Ieaxy3uKsVeusfU/+wlBBX1nmOq0RpLCIJUgQoaCOCqqwEaddJ8VChs6TPv6o65fIpZCrAkaOBdSgQXb94H/we7ZWcWrSSwolgd4Xx/kYBfp2gVbDcb6PHad1AgSfgSu94681gZlP0hsdLXYEDG0DF9cdTdkDLneA4SdDNmVXCtISikXg/Yy+KQ+Eb4GBNW9u7XOcPgBZmlX6Bjg4BMZKlL3u8+7+7rn929Oe3w9rHnKk7x4JKQAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+cCARMnDMj6VvgAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAACVUlEQVRIx7WWQUsbURSFv5nMZDJOFwHb7VuELFxk0y6EgK3QVXHjDxC6aKH9C4IFQZGCq5KVgl1oQRBB/AdCbKLEhRuhC10EMRCVQqAZZJJ5ud3E0CapZmI9MIt53Lnn3nPe3PcAMAxjBJhNp9NXgER5MpnML2CxnaMHlmEYIyLyHXg+MzPD6OgoUVCr1Z6cnp5+CoJgMhaLvdZah90xs4AcHx/LsCiVSrcdve+pIJ1OX83Pz8tD0Gq1xHEcAVZ7JDo7O3vWT5ZKpUKpVKLZbA4kldYa4FWbpOB53jff98XqF1woFJiYmMCyLGzbHojAtm1s2x7TWo81Go0Pvu+/M03zDYDkcrlOuxcXFwLI2tqa3NzcRJZLay3FYvFWssUegp2dHbEsa6jkf2JhYUGUUtdmd6vNZhPbtkkkEjwEyWSS8/Pzp+YwH+fzeZaXlweKHYqgXC6zu7v7eARR8OgE1qCal8vlzvv+/j7VapWNjY3Omuu6TE9PE4/HoxMcHh7+pXm1WqVSqbCystJZS6VSTE1N9RD0/AdbW1viuu6de3x9fV2y2eydMblcTgAxM5lMvVarPZ7JJycnX5aWljg6OkJE/r/JhmF8DoJgcnx8/KXjOGit7x1wruuSSqUGZ4nFYlb7sFgFftznQRAEUq/XB/OgPctD4CvwEchrrWm1Wv8sKB6P43nenUWHYYhSqu9h8haQYrE49CS9vLyUbDYrwKbRnd3zPMP3/T3HcSbn5uZIJpORTA3DkO3tbQ4ODn4CL/pvLdNMAItKqeuo1xilVAPYBBTAb9rfs0kjJGFsAAAAAElFTkSuQmCC',
        'search': 'Search',
        },
    }

## Use SimpleNamespace instead of passing dict['key'] via f-string. Can't pass b' (bytes) via f-string.
icon = SimpleNamespace(**_iconpack['ss_small'])

def load_iconpack(pack):
    """
    Appends user-defined iconpack to internal _iconpack dict.
    PySimpleSql comes with 'ss_small' and 'ss_large'
  	
  	For Base64, you can convert a whole folder using https://github.com/PySimpleGUI/PySimpleGUI-Base64-Encoder
  	Remember to us b'' around the string.
  	
  	For Text buttons, yan can even add Emoji's. 
  	https://carpedm20.github.io/emoji/ and copy-paste the 'Python Unicode name:' (less the variable)
  	Format like f'\N{WASTEBASKET} Delete',
  	
    Example structure:
        'example' : {
            'edit_protect' : either base64 image (eg b''), or string eg '', f''
            'quick_edit' : either base64 image (eg b''), or string eg '', f''
            'save' : either base64 image (eg b''), or string eg '', f''
            'first' : either base64 image (eg b''), or string eg '', f''
            'previous' : either base64 image (eg b''), or string eg '', f''
            'next' : either base64 image (eg b''), or string eg '', f''
            'insert' : either base64 image (eg b''), or string eg '', f''
            'search' : either base64 image (eg b''), or string eg '', f''
            'duplicate' : either base64 image (eg b''), or string eg '', f''
        }
    :param pack: iconpack. Key name of iconpack
    :return: None
    """
    global _iconpack
    for k in pack.keys():
        if k not in _iconpack.keys():
            _iconpack |= pack
    
def set_iconpack(name):
    """
    Sets which iconpack to use in gui
    PySimpleSql comes with 'ss.small' (default) 'ss.large' and 'ss_text'
    :param name: name of iconpack to set as active
    :return: None
    """
    global icon
    icon = SimpleNamespace(**_iconpack[name])
    
_keygen={}
def keygen(key,separator=':'):
    global _keygen
    # Generate a unique key by attaching a sequential integer to the end
    if key not in _keygen:
        _keygen[key]=0
    k=key
    if _keygen[key]>0:k+=f'{separator}{str(_keygen[key])}' # only modify the key if it is a duplicate!
    logger.debug(f'Key generated: {k}')
    _keygen[key] += 1
    return k
def keygen_reset(key):
    global _keygen
    del _keygen[key]
def keygen_reset_from_form(frm:Form):
    # reset keys related to form
    for e in frm.element_map:
        keygen_reset(e['element'].key)

def keygen_reset_all():
    global _keygen
    _keygen={}

def get_record_info(record):
    """
    Take a table.column string and return a tuple of the same
    :param record: A table.column string that needs separated
    :return: (table,column) Tuple of table and column
    """
    return record.split('.')









def process_events(event:str, values:list) -> bool:
    """
        Process mapped events for ALL Form instances.

        Not to be confused with pysimplesql.Form.process_events(), which processes events for individual Form instances.
        This should be called once per iteration in your event loop
         .. note:: Events handled are responsible for requerying and updating elements as needed

        :param event: The event returned by PySimpleGUI.read()
        :type event: str
        :param values: the values returned by PySimpleGUI.read()
        :type values: list
        :returns: True if an event was handled, False otherwise
        :rtype: bool
        """
    handled=False
    for i in Form.instances:
        if i.process_events(event, values): handled=True
    return handled

def update_elements(query:str = None, edit_protect_only:bool = False) -> None:
    """
    Updated the GUI elements to reflect values from the database for ALL Form instances

    Not to be confused with pysimplesql.Form.update_elements(), which updates GUI elements for individual instances.


    :param query: (optional) name of query to update elements for, otherwise updates elements for all queries
    :type query: str
    :param edit_protect_only: (default False) If true, only update items affected by edit_protect
    :type edit_protect_only: bool
    :returns: None
    :rtype: None
    """
    for i in Form.instances:
        i.update_elements(query, edit_protect_only)

def bind(win:sg.Window) -> None:
    """
    Bind ALL forms to window

    Not to be confused with pysimplesql.Form.bind(), which binds specific forms to the window.
    :param win: The PySimpleGUI window to bind all forms to
    :type win: PysimpleGUI.Window
    :returns: None
    :rtype: None
    """
    for i in Form.instances:
        i.bind(win)

# TODO: clean up.  just slapping this together for testing
def form_relationship(child, fk, parent, pk) -> None:
    Form.relationships.append(Relationship('LEFT JOIN', child, fk, parent, pk, True))
    logger.info(f'***** Setting form relationship between {child} and {parent}')


# ----------------------------------------------------------------------------------------------------------------------
# CONVENIENCE FUNCTIONS
# ----------------------------------------------------------------------------------------------------------------------
# TODO: How to save Form in metadata?  Perhaps give forms names and reference them that way??
# For exapmle - give forms names!  and reference them by name string
# They could even be converted later to a real form during form creation?

# Global variables to set default sizes for the record function below
_default_label_size = (15, 1)
_default_element_size = (30, 1)
_default_mline_size = (30, 7)
_default_ttk_theme = 'default'

def set_label_size(w, h):
    """
    Sets the default label (text) size when record() is used"
    :param w: the width desired
    :param h: the height desired
    :return: None
    """
    global _default_label_size
    _default_label_size = (w, h)

def set_element_size(w, h):
    """
    Sets the default text (label) size when @record is used.  The size parameter of @record will override this
    :param w: the width desired
    :param h: the height desired
    :return: None
    """
    global _default_element_size
    _default_element_size = (w, h)

def set_mline_size(w, h):
    """
    Sets the default multi-line text size when @record is used.  The size parameter of @record will override this
    :param w: the width desired
    :param h: the height desired
    :return: None
    """
    global _default_mline_size
    _default_mline_size = (w, h)
    
def set_ttk_theme(name):
    """
    Advise users to set their ttk theme here, so we can use in quick_edit popup. Otherwise it changes all the buttons.
    Available: 'winnative' 'clam' 'alt' 'default' 'classic' 'vista' 'xpnative'
    :param name: name of ttk_theme.
    :return: None
    """
    global _default_ttk_theme
    _default_ttk_theme = name
    
def get_ttk_theme():
    """
    Advise users to query this to fix window changing theme when you go to use quick_edit.
    :return: _default_ttk_theme
    """
    return _default_ttk_theme

# Define a custom element for quickly adding database rows.
# The automatic functions of PySimpleSQL require the elements to have a properly setup metadata
def record(table, element=sg.I, key=None, size=None, label='', no_label=False, label_above=False, quick_editor=True, filter=None, **kwargs):
    """
    Convenience function for adding PySimpleGUI elements to the window
    The automatic functionality of PySimpleSQL relies on PySimpleGUI elements to have the key {Query}.{name}
    This convenience function will create a text label, along with a element with this naming convention.
    See @set_label_size and @set_element_size for setting default sizes of these elements.

    :param record: The table.column in the database this element will be mapped to #TODO Rename!
    :param element: The element type desired (defaults to PySimpleGUI.Input)
    :param size: Overrides the default element size that was set with @set_element_size, for this element element only
    :param label: The text/label will automatically be generated from the @column name. If a different text/label is
                 desired, it can be specified here.
    :param no_label: Do not automatically generate a label for this element
    :param label_above: Place the label above the element instead of to the left
    :param quick_editor: For records that reference another table, place a quick edit button next to this element
    :param key: ???????
    :param filter: Can be used to reference different Forms in the same layout.  Use a matching filter when creating
            the form
    :return: An element to be used in the creation of PySimpleGUI layouts.  Note that this is already an array, so it
             will not need to be wrapped in [] in your layout code.
    """
    # TODO: See what the metadata does after initial setu is complete

    # Does this record imply a where clause (indicated by ?) If so, we can strip out the information we need
    if '?' in table:
        query_info, where_info = table.split('?')
        label_text = where_info.split('=')[1].replace('fk', '').replace('_', ' ').capitalize() + ':'
    else:
        query_info = table
        where_info = None
        label_text = query_info.split('.')[1].replace('fk', '').replace('_', ' ').capitalize() + ':'
    query, column = query_info.split('.')

    key=table if key is None else key

    key=keygen(key)

    if 'values' in kwargs:
        first_param=kwargs['values']
        del kwargs['values']  # make sure we don't put it in twice
    else:
        first_param=''

    if element.__name__ == 'Multiline':
        layout_element = element(first_param, key=key, size=size or _default_mline_size, metadata={'type': TYPE_RECORD, 'Form': None, 'filter': filter}, **kwargs)
    else:
        layout_element = element(first_param, key=key, size=size or _default_element_size, metadata={'type': TYPE_RECORD, 'Form': None, 'filter': filter}, **kwargs)
    layout_label = sg.T(label_text if label == '' else label, size=_default_label_size)
    if no_label:
        layout = [[layout_element]]
    elif label_above:
        layout = [[layout_label], [layout_element]]
    else:
        layout = [[layout_label , layout_element]]

    # Add the quick editor button where appropriate
    if element == sg.Combo and quick_editor:
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_QUICK_EDIT, 'query': query, 'function': None, 'Form': None, 'filter': filter}
        if type(icon.quick_edit) is bytes:
            layout[-1].append(sg.B('', key=keygen(f'{key}.quick_edit'), size=(1, 1), image_data=icon.quick_edit, metadata=meta))
        else:
            layout[-1].append(sg.B(icon.quick_edit, key=keygen(f'{key}.quick_edit'), metadata=meta, use_ttk_buttons = True))
    return sg.Col(layout=layout)

def actions(key, query, default=True, edit_protect=None, navigation=None, insert=None, delete=None, duplicate=None, save=None,
            search=None, search_size=(30, 1), bind_return_key=True, filter=None):
    """
    Allows for easily adding record navigation and elements to the PySimpleGUI window
    The navigation elements are separated into different sections as detailed by the parameters.
    :param key: The key to give these controls
    :param query: The table that this "element" will provide actions for
    :param default: Default edit_protect, navigation, insert, delete, save and search to either true or false (defaults to True)
                    The individual keyword arguments will trump the default parameter
    :param edit_protect: An edit protection mode to prevent accidental changes in the database. It is a button that toggles
                    the ability on an off to prevent accidental changes in the database by enabling/disabling the insert,
                    edit and save buttons.
    :param navigation: The standard << < > >> (First, previous, next, last) buttons for navigation
    :param insert: Button to insert new records
    :param delete: Button to delete current record
    :param duplicate: Button to duplicate current record
    :param save: Button to save record.  Note that the save button feature saves changes made to any table, therefore only one
                 save button is needed per window. This parameter only works if the @actions parameter is set.
    :param search: A search Input element. Size can be specified with the @search_size parameter
    :param search_size: The size of the search input element
    :param bind_return_key: Bind the return key to the search button. Defaults to true
    :return: An element to be used in the creation of PySimpleGUI layouts.  Note that this is already an array, so it
             will not need to be wrapped in [] in your layout code.
    """
    edit_protect = default if edit_protect is None else edit_protect
    navigation = default if navigation is None else navigation
    insert = default if insert is None else insert
    delete = default if delete is None else delete
    duplicate = default if duplicate is None else duplicate
    save = default if save is None else save
    search = default if search is None else search

    layout = []
    meta = {'type': TYPE_EVENT, 'event_type': None, 'query': None, 'function': None, 'Form': None, 'filter': filter}

    # Form-level events
    if edit_protect:
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_EDIT_PROTECT_DB, 'query': None, 'function': None, 'Form': None, 'filter': filter}
        if type(icon.edit_protect) is bytes:
            layout.append(sg.B('', key=keygen(f'{key}.edit_protect'), size=(1, 1), button_color=('orange', 'yellow'),
                               image_data=icon.edit_protect, metadata=meta))
        else:
            layout.append(sg.B(icon.edit_protect, key=keygen(f'{key}.edit_protect'), metadata=meta, use_ttk_buttons = True))
    if save:
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_SAVE_DB, 'query': None, 'function': None, 'Form': None, 'filter': filter}
        if type(icon.save) is bytes:
            layout.append(sg.B('', key=keygen(f'{key}.db_save'), size=(1, 1), button_color=('white', 'white'), image_data=icon.save,
                               metadata=meta))
        else:
            layout.append(sg.B(icon.save, key=keygen(f'{key}.db_save'), metadata=meta, use_ttk_buttons = True))

    # Query-level events
    if navigation:
        # first
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_FIRST, 'query': query, 'function': None, 'Form': None, 'filter': filter}
        if type(icon.first) is bytes:
            layout.append(sg.B('', key=keygen(f'{key}.table_first'), size=(1, 1), image_data=icon.first, metadata=meta))
        else:
            layout.append(sg.B(icon.first, key=keygen(f'{key}.table_first'), metadata=meta, use_ttk_buttons = True))
        # previous
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_PREVIOUS, 'query': query, 'function': None, 'Form': None, 'filter': filter}
        if type(icon.previous) is bytes:
            layout.append(sg.B('', key=keygen(f'{key}.table_previous'), size=(1, 1), image_data=icon.previous, metadata=meta))
        else:
            layout.append(sg.B(icon.previous, key=keygen(f'{key}.table_previous'), metadata=meta, use_ttk_buttons = True))
        # next
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_NEXT, 'query': query, 'function': None, 'Form': None, 'filter': filter}
        if type(icon.next) is bytes:
            layout.append(sg.B('', key=keygen(f'{key}.table_next'), size=(1, 1), image_data=icon.next, metadata=meta))
        else:
            layout.append(sg.B(icon.next, key=keygen(f'{key}.table_next'), metadata=meta, use_ttk_buttons = True))
        # last
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_LAST, 'query': query, 'function': None, 'Form': None, 'filter': filter}
        if type(icon.last) is bytes:
            layout.append(sg.B('', key=keygen(f'{key}.table_last'), size=(1, 1), image_data=icon.last, metadata=meta))
        else:
            layout.append(sg.B(icon.last, key=keygen(f'{key}.table_last'), metadata=meta, use_ttk_buttons = True))
    if duplicate:
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_DUPLICATE, 'query': query, 'function': None, 'Form': None,
                'filter': filter}
        if type(icon.duplicate) is bytes:
            layout.append(sg.B('', key=keygen(f'{key}.table_duplicate'), size=(1, 1), button_color=('orange', 'orange'),
                               image_data=icon.duplicate, metadata=meta))
        else:
            layout.append(
                sg.B(icon.duplicate, key=keygen(f'{key}.table_duplicate'), metadata=meta, use_ttk_buttons=True))
    if insert:
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_INSERT, 'query': query, 'function': None, 'Form': None, 'filter': filter}
        if type(icon.insert) is bytes:
            layout.append(sg.B('', key=keygen(f'{key}.table_insert'), size=(1, 1), button_color=('black', 'chartreuse3'),
                               image_data=icon.insert, metadata=meta))
        else:
            layout.append(sg.B(icon.insert, key=keygen(f'{key}.table_insert'), metadata=meta, use_ttk_buttons = True))
    if delete:
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_DELETE, 'query': query, 'function': None, 'Form': None, 'filter': filter}
        if type(icon.delete) is bytes:
            layout.append(sg.B('', key=keygen(f'{key}.table_delete'), size=(1, 1), button_color=('white', 'red'),
                            image_data=icon.delete, metadata=meta))
        else:
            layout.append(sg.B(icon.delete, key=keygen(f'{key}.table_delete'), metadata=meta, use_ttk_buttons = True))
    if search:
        meta = {'type': TYPE_EVENT, 'event_type': EVENT_SEARCH, 'query': query, 'function': None, 'Form': None, 'filter': filter}
        if type(icon.search) is bytes:
            layout+=[sg.Input('', key=keygen(f'{key}.input_search'), size=search_size),sg.B('', key=keygen(f'{key}.table_search'), bind_return_key=bind_return_key, size=(1, 1), button_color=('white', 'red'),
                                                                                            image_data=icon.delete, metadata=meta, use_ttk_buttons = True)]
        else:
            layout+=[sg.Input('', key=keygen(f'{key}.input_search'), size=search_size),sg.B(icon.search, key=keygen(f'{key}.table_search'), bind_return_key=bind_return_key, metadata=meta, use_ttk_buttons = True)]
    return sg.Col(layout=[layout])



def selector(key, table, element=sg.LBox, size=None, columns=None, filter=None, **kwargs):
    key=keygen(key)
    meta = {'type': TYPE_SELECTOR, 'table': table, 'Form': None, 'filter': filter}
    if element == sg.Listbox:
        layout = element(values=(), size=size or _default_element_size, key=key,
                    select_mode=sg.LISTBOX_SELECT_MODE_SINGLE,
                    enable_events=True, metadata=meta)
    elif element == sg.Slider:
        layout = element(enable_events=True, size=size or _default_element_size, orientation='h',
                          disable_number_display=True, key=key, metadata=meta)
    elif element == sg.Combo:
        w = _default_element_size[0]
        layout = element(values=(), size=size or (w, 10), readonly=True, enable_events=True, key=key,
                          auto_size_text=False, metadata=meta)
    elif element == sg.Table:
        required_kwargs = ['headings', 'visible_column_map', 'num_rows']
        for kwarg in required_kwargs:
            if kwarg not in kwargs:
                raise RuntimeError(f'Query selectors must use the {kwarg} keyword argument.')

        # Make an empty list of values
        vals = []
        vals.append([''] * len(kwargs['headings']))
        meta['columns'] = columns
        layout = element(
                values=vals, headings=kwargs['headings'], visible_column_map=kwargs['visible_column_map'],
                num_rows=kwargs['num_rows'], enable_events=True, key=key, select_mode=sg.TABLE_SELECT_MODE_BROWSE,
                justification='left', metadata=meta
            )
    else:
        raise RuntimeError(f'Element type "{element}" not supported as a selector.')

    return layout

# ======================================================================================================================
# ALIASES
# ======================================================================================================================
Database=Form
Table=Query
