| #!/usr/bin/env python3 |
| # |
| # Simple ttk treeview with scrollbar and select button |
| |
| import os |
| import json |
| |
| import tkinter |
| from tkinter import ttk |
| import natsort |
| #------------------------------------------------------ |
| # Tree view used as a multi-column list box |
| #------------------------------------------------------ |
| |
| class TreeViewChoice(ttk.Frame): |
| def __init__(self, parent, fontsize=11, markDir=False, deferLoad=False, selectVal=None, natSort=False, *args, **kwargs): |
| ttk.Frame.__init__(self, parent, *args, **kwargs) |
| s = ttk.Style() |
| s.configure('normal.TLabel', font=('Helvetica', fontsize)) |
| s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold')) |
| s.configure('normal.TButton', font=('Helvetica', fontsize), |
| border = 3, relief = 'raised') |
| s.configure('Treeview.Heading', font=('Helvetica', fontsize, 'bold')) |
| s.configure('Treeview.Column', font=('Helvetica', fontsize)) |
| self.markDir = markDir |
| self.initSelected = selectVal |
| self.natSort = natSort |
| self.emptyMessage1 = '(no items)' |
| self.emptyMessage = '(loading...)' if deferLoad else self.emptyMessage1 |
| |
| # Last item is a list of 2-item lists, each containing the name of a button |
| # to place along the button bar at the bottom, and a callback function to |
| # run when the button is pressed. |
| |
| def populate(self, title, itemlist=[], buttons=[], height=10, columns=[0], |
| versioning=False): |
| self.itemlist = itemlist[:] |
| |
| treeFrame = ttk.Frame(self) |
| treeFrame.pack(side='top', padx=5, pady=5, fill='both', expand='true') |
| |
| scrollBar = ttk.Scrollbar(treeFrame) |
| scrollBar.pack(side='right', fill='y') |
| self.treeView = ttk.Treeview(treeFrame, selectmode='browse', columns=columns, height=height) |
| self.treeView.pack(side='left', fill='both', expand='true') |
| scrollBar.config(command=self.treeView.yview) |
| self.treeView.config(yscrollcommand=scrollBar.set) |
| self.treeView.heading('#0', text=title, anchor='w') |
| buttonFrame = ttk.Frame(self) |
| buttonFrame.pack(side='bottom', fill='x') |
| |
| self.treeView.tag_configure('odd',background='white',foreground='black') |
| self.treeView.tag_configure('even',background='gray90',foreground='black') |
| self.treeView.tag_configure('select',background='darkslategray',foreground='white') |
| |
| self.func_buttons = [] |
| for button in buttons: |
| func = button[2] |
| # Each func_buttons entry is a list of two items; first is the |
| # button widget, and the second is a boolean that is True if the |
| # button is to be present always, False if the button is only |
| # present when there are entries in the itemlist. |
| if(button[0]=='Flow'): |
| self.flowcallback = func |
| |
| self.func_buttons.append([ttk.Button(buttonFrame, text=button[0], |
| style = 'normal.TButton', |
| command = lambda func=func: self.func_callback(func)), |
| button[1]]) |
| |
| self.selectcallback = None |
| self.lastselected = None |
| self.lasttag = None |
| self.treeView.bind('<<TreeviewSelect>>', self.retag) |
| self.repopulate(itemlist, versioning) |
| self.treeView.bind('<Double-1>', self.double_click) |
| |
| def double_click(self, event): |
| self.flowcallback(self.treeView.item(self.treeView.selection())) |
| |
| def get_button(self, index): |
| if index >= 0 and index < len(self.func_buttons): |
| return self.func_buttons[index][0] |
| else: |
| return None |
| |
| def retag(self, value): |
| treeview = value.widget |
| try: |
| selection = treeview.selection()[0] |
| oldtag = self.treeView.item(selection, 'tag')[0] |
| except IndexError: |
| # No items in view; can't select the "(no items)" line. |
| return |
| |
| self.treeView.item(selection, tag='selected') |
| if self.lastselected: |
| try: |
| self.treeView.item(self.lastselected, tag=self.lasttag) |
| except: |
| # Last selected item got deleted. Ignore this. |
| pass |
| if self.selectcallback: |
| self.selectcallback(value) |
| if (selection!=self.lastselected): |
| self.lastselected = selection |
| self.lasttag = oldtag |
| |
| #Populate the project view |
| def repopulate(self, itemlist=[], versioning=False): |
| # Remove all children of treeview |
| self.treeView.delete(*self.treeView.get_children()) |
| |
| if self.natSort: |
| self.itemlist = natsort.natsorted( itemlist, |
| alg=natsort.ns.INT | |
| natsort.ns.UNSIGNED | |
| natsort.ns.IGNORECASE ) |
| else: |
| self.itemlist = itemlist[:] |
| self.itemlist.sort() |
| |
| mode = 'even' |
| m=0 |
| for item in self.itemlist: |
| # Special handling of JSON files. The following reads a JSON file and |
| # finds key 'ip-name' in dictionary 'data-sheet', if such exists. If |
| # not, it looks for key 'project' in the top level. Failing that, it |
| # lists the name of the JSON file (which is probably an ungainly hash |
| # name). |
| |
| fileext = os.path.splitext(item) |
| if fileext[1] == '.json': |
| # Read contents of JSON file |
| with open(item, 'r') as f: |
| try: |
| datatop = json.load(f) |
| except json.decoder.JSONDecodeError: |
| name = os.path.split(item)[1] |
| else: |
| name = [] |
| if 'data-sheet' in datatop: |
| dsheet = datatop['data-sheet'] |
| if 'ip-name' in dsheet: |
| name = dsheet['ip-name'] |
| if not name and 'project' in datatop: |
| name = datatop['project'] |
| if not name: |
| name = os.path.split(item)[1] |
| elif versioning == True: |
| # If versioning is true, then the last path component is the |
| # version number, and the penultimate path component is the |
| # name. |
| version = os.path.split(item)[1] |
| name = os.path.split(os.path.split(item)[0])[1] + ' (v' + version + ')' |
| else: |
| name = os.path.split(item)[1] |
| |
| # Watch for duplicate items! |
| n = 0 |
| origname = name |
| while self.treeView.exists(name): |
| n += 1 |
| name = origname + '(' + str(n) + ')' |
| # Note: iid value with spaces in it is a bad idea. |
| if ' ' in name: |
| name = name.replace(' ', '_') |
| |
| # optionally: Mark directories with trailing slash |
| if self.markDir and os.path.isdir(item): |
| origname += "/" |
| if os.path.islink(item): |
| origname += " (link)" |
| |
| if ('subcells' not in item): |
| mode = 'even' if mode == 'odd' else 'odd' |
| self.treeView.insert('', 'end', text=origname, iid=item, value=item, tag=mode) |
| else: |
| self.treeView.insert('', 'end', text=origname, iid=item, value=item, tag='odd') |
| |
| if 'subcells' in os.path.split(item)[0]: |
| # If a project is a subproject, move it under its parent |
| parent_path = os.path.split(os.path.split(item)[0])[0] |
| parent_name = os.path.split(parent_path)[1] |
| self.treeView.move(item,parent_path,m) |
| m+=1 |
| else: |
| # If its not a subproject, create a "subproject" of itself |
| # iid shouldn't be repeated since it starts with '.' |
| self.treeView.insert('', 'end', text=origname, iid='.'+item, value=item, tag='odd') |
| self.treeView.move('.'+item,item,0) |
| m=1 |
| |
| |
| if self.initSelected and self.treeView.exists(self.initSelected): |
| if 'subcells' in self.initSelected: |
| # ancestor projects must be expanded before setting current |
| item_path = self.initSelected |
| ancestors = [] |
| while 'subcells' in item_path: |
| item_path = os.path.split(os.path.split(item_path)[0])[0] |
| ancestors.insert(0,item_path) |
| for a in ancestors: |
| self.treeView.item(a, open=True) |
| self.setselect(self.initSelected) |
| elif self.initSelected[0]=='.': |
| parent_path = self.initSelected[1:] |
| self.treeView.item(parent_path, open=True) |
| self.setselect(self.initSelected) |
| else: |
| self.setselect(self.initSelected) |
| self.initSelected = None |
| |
| |
| for button in self.func_buttons: |
| button[0].pack_forget() |
| |
| if len(self.itemlist) == 0: |
| self.treeView.insert('', 'end', text=self.emptyMessage) |
| self.emptyMessage = self.emptyMessage1 # discard optional special 1st loading... message |
| for button in self.func_buttons: |
| if button[1]: |
| button[0].pack(side='left', padx = 5) |
| else: |
| for button in self.func_buttons: |
| button[0].pack(side='left', padx = 5) |
| |
| # Return values from the treeview |
| def getvaluelist(self): |
| valuelist = [] |
| itemlist = self.treeView.get_children() |
| for item in itemlist: |
| value = self.treeView.item(item, 'values') |
| valuelist.append(value) |
| return valuelist |
| |
| # Return items (id's) from the treeview |
| def getlist(self): |
| return self.treeView.get_children() |
| |
| # This is a bit of a hack way to populate a second column, |
| # but it works. It only works for one additional column, |
| # though, or else tuples will have to be generated differently. |
| |
| def populate2(self, title, itemlist=[], valuelist=[]): |
| # Populate the pdk column |
| self.treeView.heading(1, text = title) |
| self.treeView.column(1, anchor='center') |
| children=list(self.getlist()) |
| |
| # Add id's of subprojects |
| i=1 |
| def add_ids(grandchildren): |
| # Recursively add id's of all descendants to the list |
| nonlocal i |
| for g in grandchildren: |
| children.insert(i,g) |
| i+=1 |
| descendants=self.treeView.get_children(item=g) |
| add_ids(descendants) |
| |
| for c in list(self.getlist()): |
| grandchildren=self.treeView.get_children(item=c) |
| add_ids(grandchildren) |
| i+=1 |
| |
| n = 0 |
| for item in valuelist: |
| child = children[n] |
| # Get the value at this index |
| oldvalue = self.treeView.item(child, 'values') |
| newvalue = (oldvalue, item) |
| self.treeView.item(child, values = newvalue) |
| # Add pdk for the "copy" of the project that is made in the treeview |
| if (n+1<len(children) and children[n+1]=='.'+child): |
| valuelist.insert(n,item) |
| n += 1 |
| |
| def func_callback(self, callback, event=None): |
| callback(self.treeView.item(self.treeView.selection())) |
| |
| def bindselect(self, callback): |
| self.selectcallback = callback |
| |
| def setselect(self, value): |
| self.treeView.selection_set(value) |
| |
| def selected(self): |
| value = self.treeView.item(self.treeView.selection()) |
| if value['values']: |
| return value |
| else: |
| return None |