Advertisement
Uno-Dan

Treeview

Mar 13th, 2020
297
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 21.94 KB | None | 0 0
  1. class Treeview(Base, ttk.Treeview):
  2.     def __init__(self, parent, key, **kwargs):
  3.         Base.__init__(self, parent, key, **kwargs)
  4.         self.columns = self.get_setting('columns', {})
  5.         self.headings = self.get_setting('headings', {})
  6.         self.popup_menus_config = self.get_setting('popup_menus', {})
  7.         ttk.Treeview.__init__(self, parent, columns=tuple(range(1, len(self.columns))), **self.get_options())
  8.  
  9.         self.bindings = {}
  10.         self.column_sort = {}
  11.         self.column_number = '#0'
  12.  
  13.         self.dlg = \
  14.             self.xsb = \
  15.             self.ysb = \
  16.             self.levels = \
  17.             self.largest = \
  18.             self.was_cut = \
  19.             self.xscroll = \
  20.             self.yscroll = \
  21.             self.pointer_x = \
  22.             self.pointer_y = \
  23.             self.direction = \
  24.             self.selections = \
  25.             self.root_selection = None
  26.  
  27.         self.init()
  28.  
  29.     def init(self):
  30.         def set_columns():
  31.             for index, column in enumerate(self.columns):
  32.                 self.column(f'#{index}', **dict(column))
  33.  
  34.         def set_headings():
  35.             for index, heading in enumerate(self.headings):
  36.                 self.column_sort[f'#{index}'] = True
  37.                 self.heading(f'#{index}', **dict(heading))
  38.  
  39.         def set_scrolling():
  40.             scroll = self.get_setting('scroll')
  41.             if scroll:
  42.                 self.add_scrollbars(*scroll)
  43.  
  44.         set_columns()
  45.         set_headings()
  46.         set_scrolling()
  47.  
  48.         self.set_bindings()
  49.         self.populate('', **self.get_setting('content', {}))
  50.         self.render()
  51.  
  52.     def separator_double_click(self, column):
  53.  
  54.         def get_size(item):
  55.             frame = ttk.Frame(self.app)
  56.             text = self.item(item).get('text') if column == '#0' else self.set(item, column)
  57.             lbl = tk.Label(frame, text=text)
  58.             lbl.grid()
  59.             frame.update_idletasks()
  60.             size = lbl.winfo_width()
  61.             lbl.grid_forget()
  62.  
  63.             return size
  64.  
  65.         def walk(children):
  66.             for child in children:
  67.                 size = get_size(child) + 8
  68.                 if size + (self.levels * 20) > self.largest:
  69.                     self.largest = size
  70.  
  71.                     if len(child.split('_')) > self.levels:
  72.                         self.levels = len(child.split('_'))
  73.  
  74.                 if self.item(child)['open']:
  75.                     walk(self.get_children(child))
  76.  
  77.             return self.levels, self.largest
  78.  
  79.         self.levels = 1
  80.         self.largest = 0
  81.         depth, width = walk(self.get_children())
  82.         if column == '#0':
  83.             width += depth * 20
  84.         self.column(column, width=width)
  85.  
  86.     ######################################################################
  87.  
  88.     def tags_add(self, item, *tags):
  89.         _tags = list(self.item(item)['tags'])
  90.         _tags = [] if not _tags else _tags
  91.  
  92.         for tag in tags[0]:
  93.             if tag not in _tags:
  94.                 _tags.append(tag)
  95.         self.item(item, tags=_tags)
  96.  
  97.     def tags_remove(self, item, *tags):
  98.         _tags = []
  99.         for tag in tags[0]:
  100.             _tags = list(self.item(item)['tags'])
  101.             if tag in _tags:
  102.                 _tags.pop(_tags.index(tag))
  103.                 self.item(item, tags=_tags)
  104.  
  105.     def tags_remove_all(self, *tags):
  106.         def walk(_child):
  107.             self.tags_remove(_child, list(tags))
  108.             _children = self.get_children(_child)
  109.             if _children:
  110.                 for node in _children:
  111.                     walk(node)
  112.  
  113.         children = self.get_children()
  114.         for child in children:
  115.             walk(child)
  116.  
  117.     def tags_refresh(self, _=None):
  118.  
  119.         def walk(_child, _tag):
  120.             self.tags_remove(_child, ['odd', 'even', 'selected'])
  121.  
  122.             _tags = list(self.item(_child)['tags'])
  123.             _tags.append(_tag)
  124.             self.item(_child, tags=_tags)
  125.  
  126.             for node in self.get_children(_child):
  127.                 _prev = self.prev(node)
  128.                 _children = self.get_children(node)
  129.                 self.tags_remove(node, ['odd', 'even', 'selected'])
  130.                 _tag = 'even' if _tag == 'odd' else 'odd'
  131.                 _tags = list(self.item(node)['tags'])
  132.                 _tags.append(_tag)
  133.                 self.item(node, tags=_tags)
  134.  
  135.                 if _children and self.item(node)['open']:
  136.                     _tag = walk(node, _tag)
  137.  
  138.             return _tag
  139.  
  140.         tag = 'even'
  141.         for child in self.get_children():
  142.             prev = self.prev(child)
  143.             if prev and self.item(prev)['open']:
  144.                 prev = self.get_last_node(prev)
  145.                 if prev:
  146.                     tag = 'even' if 'odd' in self.item(prev)['tags'] else 'odd'
  147.             elif prev:
  148.                 tags = self.item(prev)['tags']
  149.                 tag = 'even' if 'odd' in tags else 'odd'
  150.  
  151.             walk(child, tag)
  152.  
  153.     def cut(self, _=None):
  154.         self.copy()
  155.         self.was_cut = True
  156.  
  157.     def copy(self):
  158.         def get_items(_node):
  159.             def get_nodes(child):
  160.                 _items = []
  161.                 for child in self.get_children(child):
  162.                     _items.append(child)
  163.                     if self.get_children(child):
  164.                         _items += get_items(child)
  165.                 return _items
  166.  
  167.             if self.item(node)['open']:
  168.                 results = [] if next((x for x in selections if x.startswith(_node)), False) else get_nodes(_node)
  169.             else:
  170.                 results = get_nodes(_node)
  171.             return results
  172.  
  173.         self.tags_remove_all('copy', 'selected')
  174.         self.tags_refresh()
  175.  
  176.         items = []
  177.         selections = sorted(self.selection())
  178.         while selections:
  179.             node = selections.pop(0)
  180.             items.append(node)
  181.  
  182.             if self.item(node)['values'][0].lower() == 'menu':
  183.                 items += get_items(node)
  184.  
  185.         for item in items:
  186.             self.tags_add(item, ['copy', 'selected'])
  187.             self.tags_remove(item, ['odd', 'even'])
  188.  
  189.         self.popup.enable_items(['paste'])
  190.         self.pointer_x, self.pointer_y = self.winfo_toplevel().winfo_pointerxy()
  191.  
  192.     def paste(self):
  193.         def set_items(parent, _node):
  194.             for child in self.get_children(_node):
  195.                 if child in nodes:
  196.                     nodes.pop(nodes.index(child))
  197.                     _item = self.item(child)
  198.                     _iid = self.append(parent, _item['values'][0], _item['text'], _item['values'])
  199.                     set_items(_iid, child)
  200.  
  201.         for selection in self.selection():
  202.             nodes = sorted(self.tag_has('copy'))
  203.  
  204.             while nodes:
  205.                 node = nodes.pop(0)
  206.                 item = self.item(node)
  207.  
  208.                 iid = self.append(selection, item['values'][0], item['text'], item['values'])
  209.                 if item['values'][0].lower() == 'menu':
  210.                     set_items(iid, node)
  211.  
  212.         if self.was_cut:
  213.             self.was_cut = False
  214.             self.delete(*self.tag_has('copy'))
  215.  
  216.         self.tags_remove_all('selected')
  217.         self.tags_refresh()
  218.  
  219.     def insert(self, parent, index=tk.END, **kwargs):
  220.         iid = kwargs['iid'] = self.get_iid(parent)
  221.         text = kwargs.get('text')
  222.         config = kwargs.pop('config', {'type': kwargs.pop('type', None)})
  223.  
  224.         try:
  225.             for child in self.get_children(parent):
  226.                 if text == self.item(child)['text']:
  227.                     raise NameError(text)
  228.  
  229.             item = super(Treeview, self).insert(parent, index, **kwargs)
  230.  
  231.             self.update_config(iid, **config)
  232.             self.tags_refresh()
  233.             return item
  234.  
  235.         except NameError as text:
  236.             messagebox.showerror(
  237.                 title='Name Error',
  238.                 message=f'The name you entered, ({text}) already exists. '
  239.                         f'Choose another name and try again.'
  240.                 )
  241.  
  242.     def append(self, parent, _type, text, values, **kwargs):
  243.         kwargs['type'] = _type
  244.         kwargs['text'] = text
  245.         kwargs['values'] = values
  246.         return self.insert(parent, **kwargs)
  247.  
  248.     def remove(self):
  249.         if not self.selection():
  250.             return
  251.  
  252.         def delete(uri):
  253.             target = uri.rsplit('/').pop()
  254.             content = self.get_setting('content', {})
  255.  
  256.             def walk(_content):
  257.                 data = _content.copy()
  258.                 for key in data.keys():
  259.                     if key == target:
  260.                         del _content[key]
  261.                         break
  262.                     if isinstance(data[key], dict):
  263.                         walk(_content[key])
  264.  
  265.             walk(content)
  266.             self.set_setting('content', content)
  267.  
  268.         for selection in sorted(self.selection(), reverse=True):
  269.             delete(tvi2uri(selection))
  270.  
  271.         selected = self.selection()[0]
  272.         _prev = self.prev(selected)
  273.         _next = self.next(selected)
  274.         _parent = super(Treeview, self).parent(selected)
  275.  
  276.         for selection in sorted(self.selection(), reverse=True):
  277.             self.delete(selection)
  278.  
  279.         if _next and self.exists(_next):
  280.             self.selection_set(_next)
  281.             self.focus(_next)
  282.         elif _prev and self.exists(_prev) and not self.item(_prev)['open']:
  283.             self.selection_set(_prev)
  284.             self.focus(_prev)
  285.         else:
  286.             if _parent and not _prev:
  287.                 node = _parent
  288.             elif self.get_children(_prev):
  289.                 node = list(self.get_children(_prev)).pop()
  290.             else:
  291.                 return
  292.  
  293.             if node:
  294.                 self.selection_set(node)
  295.  
  296.         self.refresh()
  297.  
  298.     def escape(self, _):
  299.         self.was_cut = False
  300.         self.tags_remove_all('selected')
  301.         self.selection_set('')
  302.         self.tags_refresh()
  303.  
  304.     def expand(self, _):
  305.         self.after(0, self.tags_refresh)
  306.  
  307.     def actions(self, event):
  308.         region = self.identify('region', event.x, event.y)
  309.  
  310.         if region == 'tree':
  311.             item = self.identify('item', event.x, event.y)
  312.  
  313.     def refresh(self):
  314.         font = tkfont.nametofont('TkTextFont')
  315.         self.style.configure('Treeview', rowheight=font.metrics('linespace') + 5)
  316.         self.style.configure("Treeview.Heading", font=tkfont.nametofont('TkHeadingFont'))
  317.         self.set_column_widths()
  318.  
  319.     def collapse(self, _):
  320.         self.after(0, self.tags_refresh)
  321.  
  322.     def populate(self, parent, index=tk.END, **data):
  323.  
  324.         def walk(_parent, _data):
  325.             for _iid, value in _data.copy().items():
  326.                 if isinstance(value, dict):
  327.                     _iid = f'{parent}_{_iid}'.strip("_")
  328.                     _attrs = {
  329.                         'iid': _iid,
  330.                         'text': value.get('text', ''),
  331.                         'image': value.get('image', None),
  332.                         'values': value.get('values', []),
  333.                         'open': value.get('open', 0),
  334.                         'tags': value.get('tags', []),
  335.                         'type': value.get('type', None),
  336.                     }
  337.                     self.insert(_parent, index, **_attrs)
  338.                     walk(_iid, value)
  339.  
  340.         for _id, data in data.items():
  341.             if isinstance(data, dict):
  342.                 iid = f'{parent}_{_id}'.strip("_")
  343.                 attrs = {
  344.                     'iid': iid,
  345.                     'text': data.get('text', ''),
  346.                     'image': data.get('image', None),
  347.                     'values': data.get('values', []),
  348.                     'open': data.get('open', 0),
  349.                     'tags': data.get('tags', []),
  350.                     'type': data.get('type', None),
  351.                 }
  352.                 self.insert(parent, tk.END, **attrs)
  353.                 walk(iid, data)
  354.  
  355.         self.refresh()
  356.  
  357.     def get_iid(self, parent=''):
  358.         idx = len(self.get_children(parent))
  359.         iid = f'{parent}_!{idx}'.strip('_')
  360.  
  361.         while iid in self.get_children(parent):
  362.             parts = iid.rsplit('_', 1)
  363.             value = int(parts.pop().lstrip('!'))
  364.             iid = f'{parts.pop()}_!{value-1}'
  365.         return iid
  366.  
  367.     def get_last_node(self, _child):
  368.         if self.get_children(_child):
  369.             _child = list(self.get_children(_child)).pop()
  370.  
  371.             if self.item(_child)['open']:
  372.                 _child = self.get_last_node(_child)
  373.             return _child
  374.         return None
  375.  
  376.     def set_bindings(self):
  377.  
  378.         def control_a(_):
  379.             def walk(children):
  380.                 for child in children:
  381.                     self.selection_add(child)
  382.                     _children = self.get_children(child)
  383.                     if _children:
  384.                         walk(_children)
  385.             walk(self.get_children())
  386.  
  387.         def control_x(_):
  388.             self.copy()
  389.             self.remove()
  390.  
  391.         def control_c(_):
  392.             self.copy()
  393.  
  394.         def control_v(_):
  395.             self.paste()
  396.  
  397.         def control_z(e):
  398.             tree = e.widget
  399.             tree.selection_set(super(Treeview, self).parent(tree.focus()))
  400.             self.paste()
  401.  
  402.         def key_press(event):
  403.             tree = event.widget
  404.             if 'Shift' in event.keysym:
  405.                 if tree.selection() and len(tree.selection()) <= 1:
  406.                     self.root_selection = tree.focus()
  407.  
  408.         def shift_up(_):
  409.             cur_item = self.focus()
  410.             get_parent = super(Treeview, self).parent
  411.  
  412.             if not get_parent(cur_item) and not self.index(cur_item):
  413.                 return 'break'
  414.  
  415.             if not self.direction:
  416.                 self.direction = 'up'
  417.  
  418.             def prev_item(node):
  419.                 _prev = self.prev(node)
  420.                 if not _prev:
  421.                     _prev = get_parent(node)
  422.                 elif self.get_children(_prev) and self.item(_prev)['open']:
  423.                     def walk(_node):
  424.                         if self.get_children(_node) and self.item(_node)['open']:
  425.                             return walk(list(self.get_children(_node)).pop())
  426.                         return _node
  427.                     _prev = walk(list(self.get_children(_prev)).pop())
  428.  
  429.                 return _prev
  430.  
  431.             item = prev_item(cur_item)
  432.             if self.direction == 'up':
  433.                 if item:
  434.                     self.selection_add(item)
  435.                     self.focus(item)
  436.                 else:
  437.                     _parent = get_parent(item)
  438.                     if not self.item(_parent)['open']:
  439.                         item = _parent
  440.             else:
  441.                 self.selection_remove(cur_item)
  442.                 self.focus(item)
  443.  
  444.             if item == self.root_selection:
  445.                 self.direction = None
  446.  
  447.             return 'break'
  448.  
  449.         def shift_down(_):
  450.             cur_item = self.focus()
  451.  
  452.             if not self.direction:
  453.                 self.direction = 'down'
  454.  
  455.             get_parent = super(Treeview, self).parent
  456.  
  457.             def next_item(node):
  458.                 _next = self.next(node)
  459.  
  460.                 if self.get_children(node) and self.item(node)['open']:
  461.                     _next = self.get_children(node)[0]
  462.                 elif not _next:
  463.                     def walk(_node):
  464.                         _parent = get_parent(_node)
  465.                         _item = self.next(_parent)
  466.                         if not _item and get_parent(_parent):
  467.                             return walk(get_parent(_parent))
  468.                         return _item
  469.  
  470.                     if get_parent(cur_item):
  471.                         _next = walk(cur_item)
  472.  
  473.                 return _next
  474.  
  475.             item = next_item(cur_item)
  476.  
  477.             if not item:
  478.                 return
  479.  
  480.             if self.direction == 'down':
  481.                 self.selection_add(item)
  482.                 self.focus(item)
  483.             else:
  484.                 self.selection_remove(cur_item)
  485.                 self.focus(cur_item)
  486.  
  487.             if item == self.root_selection and item != self.focus():
  488.                 self.direction = None
  489.  
  490.             self.focus(item)
  491.  
  492.             return 'break'
  493.  
  494.         def header_dbl_click(event):
  495.             region = self.identify("region", event.x, event.y)
  496.             if region == "separator":
  497.                 column_number = self.identify_column(event.x)
  498.                 self.separator_double_click(column_number)
  499.  
  500.         bindings = {
  501.             '<Key>': key_press,
  502.             '<Control-a>': control_a,
  503.             '<Control-x>': control_x,
  504.             '<Control-c>': control_c,
  505.             '<Control-v>': control_v,
  506.             '<Control-z>': control_z,
  507.             # '<Up>': self.clear_selections,
  508.             # '<Down>': self.clear_selections,
  509.             '<Escape>': self.escape,
  510.             '<Button-1>': self.actions,
  511.             '<Button-3>': self.do_popup_menu,
  512.             '<Double-1>': header_dbl_click,
  513.             '<Shift-Up>': shift_up,
  514.             '<Shift-Down>': shift_down,
  515.             '<<TreeviewOpen>>': self.expand,
  516.             '<<TreeviewClose>>': self.collapse,
  517.             '<ButtonRelease-1>': self.update_column_widths,
  518.         }
  519.         for command, callback in bindings.items():
  520.             self.bindings[command] = self.bind(command, callback)
  521.  
  522.     def set_column_widths(self):
  523.         column_widths = self.get_setting('column_widths')
  524.         if column_widths:
  525.             for idx in range(len(self.cget('columns')) + 1):
  526.                 self.column(f'#{idx}', width=column_widths[idx])
  527.  
  528.     def set_content(self, iid, **kwargs):
  529.         def do_set(data):
  530.             for key, value in data.items():
  531.                 if not isinstance(value, dict):
  532.                     continue
  533.                 if key == iid:
  534.                     value.update(kwargs)
  535.                     return 'break'
  536.                 if do_set(value) == 'break':
  537.                     break
  538.         do_set(self.get_setting('content', {}))
  539.  
  540.     def get_content(self, iid=None):
  541.         def do_get(data):
  542.             for _key, _value in data.items():
  543.                 if not isinstance(_value, dict):
  544.                     continue
  545.                 if _key == iid:
  546.                     return _value
  547.                 do_get(_value)
  548.             return None
  549.  
  550.         content = self.get_setting('content', {})
  551.         if not iid or not content:
  552.             return content
  553.         elif iid in content:
  554.             _data = {}
  555.             for key, value in content[iid].items():
  556.                 if not isinstance(value, dict):
  557.                     _data[key] = value
  558.             return _data
  559.         return do_get(content)
  560.  
  561.     def update_config(self, iid, **config):
  562.         entries = {**self.item(iid), **{
  563.             'type': config.pop('type', None),
  564.             'state': config.pop('state', tk.NORMAL),
  565.             'inherit': (('colors', '0'), ),
  566.             'options': config.get('options', ()) if config else (),
  567.         }}
  568.  
  569.         config = self.get_setting('content', {})
  570.  
  571.         uri = tvi2uri(iid)
  572.         parts = uri.split('/')
  573.         _id = parts.pop(0)
  574.         if config and _id in config:
  575.             data = config[_id]
  576.             while parts:
  577.                 _id = parts.pop(0)
  578.                 if not parts:
  579.                     data[_id] = entries
  580.                 else:
  581.                     data = data[_id]
  582.         else:
  583.             config[uri] = entries
  584.  
  585.         self.set_setting('content', config)
  586.  
  587.     def update_column_widths(self, _=None):
  588.         values = []
  589.         for idx in range(len(self.cget('columns')) + 1):
  590.             values.append(self.column(f'#{idx}', 'width'))
  591.         self.set_setting('column_widths', values)
  592.  
  593.     def add_scrollbars(self, xscroll, yscroll):
  594.         if not xscroll and not yscroll:
  595.             return
  596.  
  597.         self.xscroll, self.yscroll = xscroll, yscroll
  598.  
  599.         if xscroll:
  600.             sb = self.parent.add_element('scrollbar1', **scrollbar1)
  601.             sb.parent = self
  602.             sb.configure(command=self.xview)
  603.             self.configure(xscrollcommand=sb.set_scroll)
  604.  
  605.         if yscroll:
  606.             sb = self.parent.add_element('scrollbar0', **scrollbar0)
  607.             sb.parent = self
  608.             sb.configure(command=self.yview)
  609.             self.configure(yscrollcommand=sb.set_scroll)
  610.  
  611.     def do_scroll(self, event):
  612.         wdg = event.widget
  613.  
  614.         options = wdg.get_options()
  615.         if wdg.type == 'Scrollbar' and options.get('orient', tk.HORIZONTAL) == tk.HORIZONTAL:
  616.             event.state = (event.state | 1)
  617.  
  618.         c_width, c_height = self.winfo_width(), self.winfo_height()
  619.  
  620.         autosave_state = self.get_setting('autosave_state', True)
  621.  
  622.         if event.state & 0x1 and self.xscroll:
  623.             if event.num == 4:
  624.                 if c_width <= self.winfo_reqwidth():
  625.                     self.xview_scroll(-1, tk.UNITS)
  626.             elif event.num == 5:
  627.                 self.xview_scroll(1, tk.UNITS)
  628.  
  629.             if autosave_state:
  630.                 self.set_setting('xview', self.xview())
  631.  
  632.         elif self.yscroll:
  633.             if event.num == 4:
  634.                 if c_height <= self.winfo_reqheight():
  635.                     self.yview_scroll(-1, tk.UNITS)
  636.             elif event.num == 5:
  637.                 self.yview_scroll(1, tk.UNITS)
  638.  
  639.             if autosave_state:
  640.                 self.set_setting('yview', self.yview())
  641.  
  642.     def do_popup_menu(self, event):
  643.         item = self.identify('item', event.x, event.y)
  644.  
  645.         if not self.popup:
  646.             return
  647.  
  648.         self.popup.tk_popup(event.x_root, event.y_root)
  649.         # This needs to be fixed not portable.
  650.         items = ['add_project', 'cut', 'copy', 'delete', 'add_item', 'add_menu', 'select_all', 'save', 'save_all']
  651.  
  652.         if item:
  653.             self.popup.enable_items(items)
  654.         else:
  655.             items.pop(0)
  656.             items.append('paste')
  657.             self.popup.disable_items(items)
  658.  
  659.         if item not in self.selection():
  660.             self.selection_set('')
  661.             self.selection_add(item)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement