blob: 67b07c2edef3e34e04930be9cffe741bcd559d56 [file] [log] [blame]
#!/usr/bin/env python3
#
# Simple ttk treeview with scrollbar and select button
import os
import json
import tkinter
from tkinter import ttk
import natural_sort
#------------------------------------------------------
# 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 = natural_sort.natural_sort(itemlist)
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