Advertisement
Uno-Dan

Untitled

Nov 7th, 2018
199
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 21.29 KB | None | 0 0
  1. ########################################################################################################################
  2. #    File: base.py
  3. #  Author: Dan Huckson, https://github.com/unodan
  4. #    Date: 2018-08-24
  5. ########################################################################################################################
  6.  
  7. from os import path
  8. from logging import basicConfig
  9. from bisect import bisect
  10. from tkinter import Tk, Toplevel, PanedWindow, Canvas
  11. from tkinter.ttk import Frame, LabelFrame, Scrollbar, Button
  12. from tkinter.font import nametofont
  13. from importlib import import_module
  14.  
  15.  
  16. from tkapp.Misc.uricache import URICache
  17. from tkapp.functions import make_directories
  18. from tkapp.constants import GRID_OPTIONS, CONFIG_OPTIONS, LOG_FILE, LOG_LEVEL, LOG_FORMAT, LOG_DATEFMT, CONFIG_RUNTIME
  19.  
  20.  
  21. class _Admin(Tk):
  22.     def __init__(self, **kwargs):
  23.         self.admin_open = kwargs.pop('admin_open', None)
  24.         self.admin_close = kwargs.pop('admin_close', True)
  25.         self.defaults = kwargs.pop('defaults')
  26.         self.window_title = kwargs.pop('title', '')
  27.         super().__init__(**kwargs)
  28.         basicConfig(filename=LOG_FILE, level=LOG_LEVEL, format=LOG_FORMAT, datefmt=LOG_DATEFMT)
  29.  
  30.         self.withdraw()
  31.         self.geometry('+2000+224')
  32.  
  33.         make_directories((
  34.             'logs',
  35.             'configs/backups',
  36.             'images/buttons',
  37.         ))
  38.  
  39.         self.hooks = {'after_open': {}}
  40.         self.cache = None
  41.         self.windows = {}
  42.         self.wm_stackorder = {}
  43.  
  44.         self.init_cache()
  45.         self.title(self.window_title)
  46.         self.protocol('WM_DELETE_WINDOW', self.exit_app)
  47.         self.option_add('*Font', 'TkDefaultFont')
  48.         self.font = nametofont("TkDefaultFont")
  49.  
  50.         def show_window(key):
  51.             if key not in self.windows:
  52.                 self.windows[key] = Window(
  53.                     key,
  54.                     self.cache.get(key),
  55.                     before_open=self.before_open,
  56.                     after_open=self.after_open,
  57.                     before_close=self.before_close,
  58.                     after_close=self.after_close
  59.                 )
  60.  
  61.         Button(self, text='Window1', command=lambda: show_window('window1')).grid(row=0, column=0)
  62.         Button(self, text='Window2', command=lambda: show_window('window2')).grid(row=0, column=1)
  63.         Button(self, text='Window3', command=lambda: show_window('window3')).grid(row=0, column=2)
  64.         Button(self, text='Window4', command=lambda: show_window('window4')).grid(row=0, column=3)
  65.  
  66.         self.config(padx=40, pady=50, bg='purple')
  67.  
  68.         if self.admin_open:
  69.             self.deiconify()
  70.  
  71.     def exit_app(self):
  72.         wm_stackorder = self.tk.eval('wm stackorder {}'.format(self)).split()
  73.  
  74.         if '.' in wm_stackorder:
  75.             wm_stackorder.remove('.')
  76.  
  77.         stackorder = []
  78.         for index, target in enumerate(wm_stackorder):
  79.             for key, window in self.windows.items():
  80.                 if str(window) == target:
  81.                     stackorder.append(window.id)
  82.  
  83.         self.cache.set(stackorder, 'wm_stackorder')
  84.  
  85.         for key, window in self.windows.copy().items():
  86.             window.close(True)
  87.  
  88.         self.destroy()
  89.         exit()
  90.  
  91.     def get_hooks(self, window, name):
  92.         hooks = []
  93.         if name in self.hooks:
  94.             for hook in self.hooks[name]:
  95.                 if window in hook:
  96.                     hooks.append(hook)
  97.  
  98.         return hooks
  99.  
  100.     def init_cache(self):
  101.         if path.isfile(CONFIG_RUNTIME):
  102.             self.cache = URICache(CONFIG_RUNTIME)
  103.         else:
  104.             self.cache = URICache(data=self.defaults)
  105.             self.cache.save(CONFIG_RUNTIME)
  106.  
  107.     def get_window(self, key):
  108.         if key in self.windows:
  109.             return self.windows[key].container
  110.  
  111.     def init_windows(self):
  112.         wm_stackorder = self.cache.get('wm_stackorder', {})
  113.  
  114.         for key in wm_stackorder:
  115.             content = self.cache.get(key)
  116.  
  117.             if isinstance(content, dict):
  118.                 if content.get('is_visible') or content.get('auto_start'):
  119.  
  120.                     Window(
  121.                         key,
  122.                         content,
  123.                         before_open=self.before_open,
  124.                         after_open=self.after_open,
  125.                         before_close=self.before_close,
  126.                         after_close=self.after_close,
  127.                     )
  128.  
  129.     def before_open(self, window):
  130.         for hook in self.get_hooks(window.id, 'before_open'):
  131.             if window.id in hook:
  132.                 self.hooks['before_open'][hook]()
  133.  
  134.     def after_open(self, window):
  135.         self.windows[window.id] = window
  136.  
  137.         for hook in self.get_hooks(window.id, 'after_open'):
  138.             if window.id in hook:
  139.                 self.hooks['after_open'][hook](window)
  140.  
  141.     def before_close(self, window):
  142.         for hook in self.get_hooks(window.id, 'before_close'):
  143.             if window.id in hook:
  144.                 self.hooks['before_close'][hook]()
  145.  
  146.     def after_close(self, window):
  147.         if window.auto_save or window.save_geometry:
  148.             if window.auto_save:
  149.                 self.cache.update(window.values())
  150.  
  151.             if window.save_geometry:
  152.                 self.cache.update({window.id: window.cache.get()})
  153.  
  154.         self.cache.save(CONFIG_RUNTIME)
  155.  
  156.         window.destroy()
  157.         del self.windows[window.id]
  158.  
  159.         for hook in self.get_hooks(window.id, 'after_close'):
  160.             if window.id in hook:
  161.                 self.hooks['after_close'][hook]()
  162.  
  163.         if not len(self.windows) and self.admin_close:
  164.             self.destroy()
  165.             exit()
  166.  
  167.     def register_callback(self, hook, target, callback):
  168.  
  169.         if hook not in self.hooks:
  170.             self.hooks[hook] = {}
  171.  
  172.         self.hooks[hook].update({target: callback})
  173.  
  174.  
  175. class App(object):
  176.     instance = None
  177.  
  178.     def __new__(cls, **kwargs):
  179.         if not cls.instance:
  180.             cls.instance = super(App, cls).__new__(cls)
  181.             cls.instance.admin = _Admin(**kwargs)
  182.  
  183.         return cls.instance.admin
  184.  
  185.     def mainloop(self):
  186.         self.instance.admin.mainloop()
  187.  
  188.  
  189. class Cell(object):
  190.     def __init__(self, parent, key, data, **kwargs):
  191.         self.key = key
  192.         self.parent = parent
  193.         self.members = {}
  194.         self.grid_args = {}
  195.         self.config_args = {}
  196.  
  197.         self.row = None
  198.         self.rowspan = None
  199.         self.column = None
  200.         self.columnspan = None
  201.  
  202.         self.padx = None
  203.         self.pady = None
  204.         self.ipadx = None
  205.         self.ipady = None
  206.         self.index = None
  207.  
  208.         self.type = data.get('type', None)
  209.         self.label = data.get('label', None)
  210.         self.style = data.get('style', None)
  211.         self.width = data.get('width', None)
  212.         self.height = data.get('height', None)
  213.         self.sticky = data.get('sticky', None)
  214.         self.rowconfigure = data.get('rowconfigure', 0)
  215.         self.columnconfigure = data.get('columnconfigure', 0)
  216.  
  217.         if self.type == 'LabelFrame':
  218.             self.frame = LabelFrame(parent, text=self.label)
  219.         else:
  220.             self.frame = Frame(parent)
  221.  
  222.         self.frame.rowconfigure(self.rowconfigure, weight=1)
  223.         self.frame.columnconfigure(self.columnconfigure, weight=1)
  224.         self.frame.grid(sticky=self.sticky)
  225.  
  226.     def get(self, uri=None):
  227.         if not uri:
  228.             return self.frame
  229.  
  230.         uri = uri.replace('\\', '/').rstrip('/')
  231.         if '/' in uri:
  232.             def parse(parent, uri_part):
  233.                 parts = uri_part.split('/', 1)
  234.  
  235.                 child = parent.get(parts[0])
  236.                 if len(parts) > 1:
  237.                     child = parse(child, parts[1])
  238.  
  239.                 return child
  240.  
  241.             return parse(self, uri)
  242.  
  243.         if isinstance(self.members.get(uri, None), Field):
  244.             return self.members.get(uri).field
  245.  
  246.         return self.members.get(uri, None)
  247.  
  248.     def add(self, data):
  249.         key = next(iter(data.keys()))
  250.         item = next(iter(data.values()))
  251.         item_type = item.get('type')
  252.  
  253.         if key not in self.members:
  254.             if item_type == 'PanedWindow':
  255.                 self.members[key] = Pane(self.frame, key, item)
  256.             elif item_type in ('Frame', 'LabelFrame'):
  257.                 self.members[key] = Group(self.frame, key, item)
  258.             else:
  259.                 self.members[key] = Field(self.frame, key, item)
  260.  
  261.         return self.members[key]
  262.  
  263.     def item(self, uri=None):
  264.         return self.get(uri)
  265.  
  266.     def remove(self, uri):
  267.         if not uri:
  268.             return self.frame
  269.  
  270.         uri = uri.replace('\\', '/').rstrip('/')
  271.         if '/' in uri:
  272.             def parse(parent, uri_part):
  273.                 parts = uri_part.split('/', 1)
  274.  
  275.                 child = parent.get(parts[0])
  276.                 if len(parts) > 1:
  277.                     child = parse(child, parts[1])
  278.  
  279.                 return child
  280.  
  281.             return parse(self, uri)
  282.         self.members[uri].grid_remove()
  283.  
  284.         del self.members[uri]
  285.  
  286.     def values(self, data=None):
  287.         cache = URICache(data=data)
  288.  
  289.         def process(uri, members):
  290.  
  291.             for key, widget in members.items():
  292.                 if isinstance(widget, Field):
  293.                     target = uri + '/' + key + '/value'
  294.                     value = None if not data else cache.get(target, '')
  295.  
  296.                     if widget.type in ('Treeview', 'Progressbar', 'Button', 'Separator', 'Label'):
  297.                         continue
  298.  
  299.                     if widget.type == 'Notebook':
  300.                         for tab, page in widget.field.page.items():
  301.                             if hasattr(page, 'form'):
  302.                                 process(uri + '/' + key + '/items/' + tab + '/content', page.form.members)
  303.  
  304.                     elif widget.type == 'Text':
  305.                         if data:
  306.                             if value is None:
  307.                                 value = ''
  308.                             widget.field.text.delete('1.0', 'end')
  309.                             widget.field.text.insert('end', value)
  310.                         else:
  311.                             cache.set(widget.field.text.get('1.0', 'end'), target)
  312.  
  313.                     elif widget.type == 'Combobox':
  314.                         if data:
  315.                             item = ''
  316.                             if widget.items:
  317.                                 if isinstance(value, str):
  318.                                     value = 0
  319.                                 item = widget.items[value]
  320.  
  321.                             widget.field.set(item)
  322.                             if value:
  323.                                 widget.field.combobox.current(value)
  324.                         else:
  325.                             cache.set(widget.items.index(widget.field.var.get()), target)
  326.  
  327.                     else:
  328.                         if data:
  329.                             widget.field.set(value)
  330.                         else:
  331.                             cache.set(widget.field.var.get(), target)
  332.                 elif widget.type != 'PanedWindow':
  333.                     process(uri + '/' + key, widget.members)
  334.                 else:
  335.                     for k, v in widget.members.items():
  336.                         process(uri + '/' + key + '/' + k, v.members)
  337.  
  338.         process(self.key, self.members)
  339.  
  340.         return cache.get()
  341.  
  342.  
  343. class Field(Frame):
  344.     def __init__(self, parent, key, data):
  345.         super().__init__(parent)
  346.         self.rowconfigure(0, weight=1)
  347.         self.columnconfigure(0, weight=1)
  348.  
  349.         self.key = key
  350.         self.data = data
  351.         self.parent = parent
  352.  
  353.         self.row = data.get('row', 0)
  354.         self.rowspan = data.get('rowspan')
  355.         self.column = data.get('column', 0)
  356.         self.columnspan = data.get('columnspan')
  357.  
  358.         self.type = data.get('type')
  359.         self.padx = data.get('padx')
  360.         self.pady = data.get('pady')
  361.         self.label = data.get('label')
  362.         self.items = data.get('items')
  363.         self.sticky = data.get('sticky', 'W')
  364.  
  365.         module = import_module('tkapp')
  366.         widget = getattr(module, '_' + self.type)
  367.         self.field = widget(self, data)
  368.  
  369.         self.grid(
  370.             row=self.row,
  371.             column=self.column,
  372.             rowspan=self.rowspan,
  373.             columnspan=self.columnspan,
  374.             padx=self.padx,
  375.             pady=self.pady,
  376.             sticky=self.sticky,
  377.         )
  378.  
  379.     def __eq__(self, obj):
  380.         if isinstance(self, obj):
  381.             return self.value == obj.value
  382.         else:
  383.             return NotImplemented
  384.  
  385.     def value(self, value=None):
  386.         if value is None:
  387.             value = self.field.get()
  388.         else:
  389.             self.field.set(value)
  390.         return value
  391.  
  392.     def insert(self, value, row, column):
  393.         if self.type == 'Text':
  394.             self.field.insert(value, row, column)
  395.  
  396.     def append(self, value):
  397.         if self.type == 'Text':
  398.             self.field.append(value)
  399.  
  400.  
  401. class ScrollFrame(Frame):
  402.     def __init__(self, parent, **kwargs):
  403.         title = kwargs.pop('title', None)
  404.         scrollbars = kwargs.pop('scrollbars', None)
  405.         super().__init__(parent, **kwargs)
  406.  
  407.         self.canvas = Canvas(parent, borderwidth=0, highlightthickness=0)
  408.         self.canvas.grid(sticky=kwargs.get('sticky', 'nw'))
  409.  
  410.         if scrollbars == 'both' or scrollbars == 'vertical' or scrollbars is True:
  411.             scrollbar_y = AutoScrollbar(parent, orient='vertical', command=self.canvas.yview)
  412.             scrollbar_y.grid(row=0, column=1, sticky='ns')
  413.             self.canvas.configure(yscrollcommand=scrollbar_y.set)
  414.  
  415.         if scrollbars == 'both' or scrollbars == 'horizontal' or scrollbars is True:
  416.             scrollbar_x = AutoScrollbar(parent, orient='horizontal', command=self.canvas.xview)
  417.             scrollbar_x.grid(row=1, column=0, sticky='ew')
  418.             self.canvas.configure(xscrollcommand=scrollbar_x.set)
  419.  
  420.         if not title:
  421.             self.content = Frame(self.canvas)
  422.         else:
  423.             self.content = LabelFrame(self.canvas, text=title)
  424.  
  425.         self.content.bind('<Configure>', self.do_configure)
  426.         self.window_id = self.canvas.create_window((0, 0), window=self.content, anchor='nw')
  427.  
  428.     def do_configure(self, event):
  429.         self.canvas.configure(
  430.             width=self.content.winfo_width(),
  431.             height=self.content.winfo_height(),
  432.             scrollregion=self.canvas.bbox('all'),
  433.         )
  434.         return event
  435.  
  436.     def update_idletasks(self):
  437.         self.content.update_idletasks()
  438.  
  439.  
  440. class Pane(Cell):
  441.     def __init__(self, parent, key, data):
  442.         super().__init__(parent, key, data)
  443.  
  444.         self.relief = data.get('relief', 'flat')
  445.         self.orient = data.get('orient', None)
  446.         self.sashwidth = data.get('sashwidth', None)
  447.         self.showhandle = data.get('showhandle', None)
  448.         self.background = data.get('background', None)
  449.         self.borderwidth = data.get('borderwidth', 0)
  450.  
  451.         self.panedwindow = PanedWindow(
  452.             self.frame,
  453.             orient=self.orient,
  454.             relief=self.relief,
  455.             sashwidth=self.sashwidth,
  456.             showhandle=self.showhandle,
  457.             background=self.background,
  458.             borderwidth=self.borderwidth,
  459.         )
  460.         self.panedwindow.id = key
  461.  
  462.         self.panedwindow.grid(sticky='nsew')
  463.  
  464.         for key, item in data.items():
  465.             if isinstance(item, dict) and item.get('type') in ('Frame', 'LabelFrame'):
  466.                 self.members[key] = Group(self.panedwindow, key, item)
  467.                 self.panedwindow.add(self.members[key].frame)
  468.  
  469.         self.panedwindow.update()
  470.  
  471.  
  472. class Group(Cell):
  473.     def __init__(self, parent, key, data):
  474.         super().__init__(parent, key, data)
  475.         for key, item in data.items():
  476.             if isinstance(item, dict):
  477.                 item_type = item.get('type')
  478.  
  479.                 if item_type in ('Frame', 'LabelFrame', 'PanedWindow'):
  480.                     if key not in self.members:
  481.                         self.members[key] = self.add({key: item})
  482.                 elif key not in self.members:
  483.                     self.members[key] = Field(self.frame, key, item)
  484.                     self.members[key].grid()
  485.  
  486.             else:
  487.                 setattr(self, key, item)
  488.                 if key in GRID_OPTIONS:
  489.                     self.grid_args[key] = item
  490.                 elif bisect(CONFIG_OPTIONS, key) and key not in ('type', 'label'):
  491.                     self.config_args[key] = item
  492.  
  493.         self.grid_args.update({
  494.             'padx': self.grid_args.pop('ipadx', None),
  495.             'pady': self.grid_args.pop('ipady', None),
  496.         })
  497.  
  498.         self.frame.grid(**self.grid_args)
  499.  
  500.  
  501. class Container(Cell):
  502.     def __init__(self, parent, key, content):
  503.         super().__init__(parent, key, content)
  504.  
  505.         for key, item in content.items():
  506.             if isinstance(item, dict):
  507.                 self.members[key] = self.add({key: item})
  508.             else:
  509.                 setattr(self, key, item)
  510.                 if key in GRID_OPTIONS:
  511.                     self.grid_args[key] = item
  512.                 elif bisect(CONFIG_OPTIONS, key) and key not in ('type', 'label'):
  513.                     self.config_args[key] = item
  514.  
  515.         self.frame.grid(
  516.             padx=self.grid_args.get('padx'),
  517.             pady=self.grid_args.get('pady'),
  518.             sticky=self.grid_args.get('sticky'),
  519.         )
  520.  
  521.  
  522. class Window(Toplevel):
  523.     def __init__(self, key, content, *args, **kwargs):
  524.         self.auto_save = kwargs.pop('auto_save', False)
  525.         self.save_geometry = kwargs.pop('save_geometry', False)
  526.         self.before_open = kwargs.pop('before_open', None)
  527.         self.after_open = kwargs.pop('after_open', None)
  528.         self.before_close = kwargs.pop('before_close', None)
  529.         self.after_close = kwargs.pop('after_close', None)
  530.         super().__init__(*args, **kwargs)
  531.  
  532.         self.rowconfigure(0, weight=1)
  533.         self.columnconfigure(0, weight=1)
  534.  
  535.         self.id = key
  536.         self.cache = URICache(data=content)
  537.  
  538.         self.style = None
  539.         self.theme = None
  540.         self.themes = None
  541.         self.theme_index = None
  542.  
  543.         self.container = None
  544.         self.is_visible = None
  545.         self.window_position = None
  546.         self.window_geometry = None
  547.  
  548.         self.window_title = content.get('window_title', '')
  549.  
  550.         self.protocol('WM_DELETE_WINDOW', self.close)
  551.  
  552.         for attribute, value in content.items():
  553.             if not isinstance(value, dict):
  554.                 setattr(self, attribute, value)
  555.  
  556.         self.open(key, content)
  557.         self.title(self.window_title)
  558.  
  559.     def get(self, uri):
  560.         return self.item(uri)
  561.  
  562.     def item(self, uri):
  563.         return self.container.get(uri)
  564.  
  565.     def open(self, window, content):
  566.  
  567.         if self.before_open:
  568.             self.before_open(self)
  569.  
  570.         geometry = self.cache.get('window_geometry', None)
  571.         if geometry:
  572.             self.geometry(geometry)
  573.  
  574.         self.container = Container(self, window, content)
  575.         self.container.values({window: content})
  576.  
  577.         self.sash_coords(content, 1)  # 1 = place/apply
  578.         self.cache.set(True, 'is_visible')
  579.  
  580.         if self.after_open:
  581.             self.after_open(self)
  582.  
  583.     def close(self, is_visible=False):
  584.  
  585.         if self.before_close:
  586.             self.before_close(self)
  587.  
  588.         self.sash_coords(self.cache.get(), 0)  # 0 = save/update
  589.  
  590.         self.cache.update({'is_visible': is_visible, 'window_geometry': self.geometry()})
  591.  
  592.         if self.after_close:
  593.             self.after_close(self)
  594.  
  595.     def values(self):
  596.         return self.container.values()
  597.  
  598.     def sash_coords(self, data, mode, uri=''):
  599.         for key, item in data.items():
  600.             if isinstance(item, dict) and item.get('type') == 'PanedWindow':
  601.                 cnt = 0
  602.                 panedwindow = self.container.get(uri + key).panedwindow
  603.  
  604.                 panedwindow.grid(sticky='nsew')
  605.  
  606.                 limit = len(panedwindow.panes()) - 1
  607.  
  608.                 for _key, value in item.items():
  609.                     if isinstance(value, dict):
  610.  
  611.                         if mode == 1 and 'sash_coord' in value:
  612.                             x, y = value.get('sash_coord')
  613.                             panedwindow.sash_place(cnt, x, y)
  614.                             panedwindow.update()
  615.  
  616.                         elif mode == 0:
  617.                             coords = panedwindow.sash_coord(cnt)
  618.                             self.cache.set(coords, uri + key + '/' + _key + '/sash_coord')
  619.                         else:
  620.                             continue
  621.  
  622.                         cnt += 1
  623.                         if cnt == limit:
  624.                             break
  625.  
  626.                 self.sash_coords(item, mode, uri + key + '/')
  627.  
  628.             elif isinstance(item, dict) and item.get('type') in ('Frame', 'LabelFrame'):
  629.                 self.sash_coords(item, mode, uri + key + '/')
  630.  
  631.  
  632. class AutoScrollbar(Scrollbar):
  633.     def set(self, lo, hi):
  634.         if float(lo) <= 0.0 and float(hi) >= 1.0:
  635.             self.grid_forget()
  636.         else:
  637.             if str(self.cget("orient")) == 'vertical':
  638.                 self.grid(row=0, column=1,  sticky='NS')
  639.             else:
  640.                 self.grid(row=1, column=0,  sticky='EW')
  641.  
  642.         Scrollbar.set(self, lo, hi)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement