blob: ef1e5801b7ebdbf261cac737ad4f09c3aa98da07 [file] [log] [blame]
emayecs5656b2b2021-08-04 12:44:13 -04001#!/usr/bin/env python3
emayecs5966a532021-07-29 10:07:02 -04002#
3# Simple ttk treeview with scrollbar and select button
4
5import os
6import json
7
8import tkinter
9from tkinter import ttk
10import natsort
11#------------------------------------------------------
12# Tree view used as a multi-column list box
13#------------------------------------------------------
14
15class TreeViewChoice(ttk.Frame):
16 def __init__(self, parent, fontsize=11, markDir=False, deferLoad=False, selectVal=None, natSort=False, *args, **kwargs):
17 ttk.Frame.__init__(self, parent, *args, **kwargs)
18 s = ttk.Style()
19 s.configure('normal.TLabel', font=('Helvetica', fontsize))
20 s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold'))
21 s.configure('normal.TButton', font=('Helvetica', fontsize),
22 border = 3, relief = 'raised')
23 s.configure('Treeview.Heading', font=('Helvetica', fontsize, 'bold'))
24 s.configure('Treeview.Column', font=('Helvetica', fontsize))
25 self.markDir = markDir
26 self.initSelected = selectVal
27 self.natSort = natSort
28 self.emptyMessage1 = '(no items)'
29 self.emptyMessage = '(loading...)' if deferLoad else self.emptyMessage1
30
31 # Last item is a list of 2-item lists, each containing the name of a button
32 # to place along the button bar at the bottom, and a callback function to
33 # run when the button is pressed.
34
35 def populate(self, title, itemlist=[], buttons=[], height=10, columns=[0],
36 versioning=False):
37 self.itemlist = itemlist[:]
38
39 treeFrame = ttk.Frame(self)
40 treeFrame.pack(side='top', padx=5, pady=5, fill='both', expand='true')
41
42 scrollBar = ttk.Scrollbar(treeFrame)
43 scrollBar.pack(side='right', fill='y')
44 self.treeView = ttk.Treeview(treeFrame, selectmode='browse', columns=columns, height=height)
45 self.treeView.pack(side='left', fill='both', expand='true')
46 scrollBar.config(command=self.treeView.yview)
47 self.treeView.config(yscrollcommand=scrollBar.set)
48 self.treeView.heading('#0', text=title, anchor='w')
49 buttonFrame = ttk.Frame(self)
50 buttonFrame.pack(side='bottom', fill='x')
51
52 self.treeView.tag_configure('odd',background='white',foreground='black')
53 self.treeView.tag_configure('even',background='gray90',foreground='black')
54 self.treeView.tag_configure('select',background='darkslategray',foreground='white')
55
56 self.func_buttons = []
57 for button in buttons:
58 func = button[2]
59 # Each func_buttons entry is a list of two items; first is the
60 # button widget, and the second is a boolean that is True if the
61 # button is to be present always, False if the button is only
62 # present when there are entries in the itemlist.
emayecsca79e462021-08-19 16:18:50 -040063 if(button[0]=='Flow'):
64 self.flowcallback = func
65
emayecs5966a532021-07-29 10:07:02 -040066 self.func_buttons.append([ttk.Button(buttonFrame, text=button[0],
67 style = 'normal.TButton',
68 command = lambda func=func: self.func_callback(func)),
69 button[1]])
emayecsca79e462021-08-19 16:18:50 -040070
emayecs5966a532021-07-29 10:07:02 -040071 self.selectcallback = None
72 self.lastselected = None
73 self.lasttag = None
74 self.treeView.bind('<<TreeviewSelect>>', self.retag)
75 self.repopulate(itemlist, versioning)
emayecs4cd7f232021-08-19 16:22:35 -040076 self.treeView.bind('<Double-1>', self.double_click)
emayecs5966a532021-07-29 10:07:02 -040077
emayecs4cd7f232021-08-19 16:22:35 -040078 def double_click(self, event):
emayecsca79e462021-08-19 16:18:50 -040079 self.flowcallback(self.treeView.item(self.treeView.selection()))
80
emayecs5966a532021-07-29 10:07:02 -040081 def get_button(self, index):
82 if index >= 0 and index < len(self.func_buttons):
83 return self.func_buttons[index][0]
84 else:
85 return None
86
87 def retag(self, value):
88 treeview = value.widget
89 try:
90 selection = treeview.selection()[0]
91 oldtag = self.treeView.item(selection, 'tag')[0]
92 except IndexError:
93 # No items in view; can't select the "(no items)" line.
94 return
95
96 self.treeView.item(selection, tag='selected')
97 if self.lastselected:
98 try:
99 self.treeView.item(self.lastselected, tag=self.lasttag)
100 except:
101 # Last selected item got deleted. Ignore this.
102 pass
103 if self.selectcallback:
104 self.selectcallback(value)
emayecs12e85282021-08-11 09:37:00 -0400105 if (selection!=self.lastselected):
106 self.lastselected = selection
107 self.lasttag = oldtag
emayecsc707a0a2021-08-06 17:13:27 -0400108
109 #Populate the project view
emayecs5966a532021-07-29 10:07:02 -0400110 def repopulate(self, itemlist=[], versioning=False):
emayecs5966a532021-07-29 10:07:02 -0400111 # Remove all children of treeview
112 self.treeView.delete(*self.treeView.get_children())
113
114 if self.natSort:
115 self.itemlist = natsort.natsorted( itemlist,
116 alg=natsort.ns.INT |
117 natsort.ns.UNSIGNED |
118 natsort.ns.IGNORECASE )
119 else:
120 self.itemlist = itemlist[:]
121 self.itemlist.sort()
122
123 mode = 'even'
emayecsc707a0a2021-08-06 17:13:27 -0400124 m=0
emayecs5966a532021-07-29 10:07:02 -0400125 for item in self.itemlist:
126 # Special handling of JSON files. The following reads a JSON file and
127 # finds key 'ip-name' in dictionary 'data-sheet', if such exists. If
128 # not, it looks for key 'project' in the top level. Failing that, it
129 # lists the name of the JSON file (which is probably an ungainly hash
130 # name).
131
132 fileext = os.path.splitext(item)
133 if fileext[1] == '.json':
134 # Read contents of JSON file
135 with open(item, 'r') as f:
136 try:
137 datatop = json.load(f)
138 except json.decoder.JSONDecodeError:
139 name = os.path.split(item)[1]
140 else:
141 name = []
142 if 'data-sheet' in datatop:
143 dsheet = datatop['data-sheet']
144 if 'ip-name' in dsheet:
145 name = dsheet['ip-name']
146 if not name and 'project' in datatop:
147 name = datatop['project']
148 if not name:
149 name = os.path.split(item)[1]
150 elif versioning == True:
151 # If versioning is true, then the last path component is the
152 # version number, and the penultimate path component is the
153 # name.
emayecsc707a0a2021-08-06 17:13:27 -0400154 version = os.path.split(item)[1]
emayecs5966a532021-07-29 10:07:02 -0400155 name = os.path.split(os.path.split(item)[0])[1] + ' (v' + version + ')'
156 else:
157 name = os.path.split(item)[1]
158
159 # Watch for duplicate items!
160 n = 0
161 origname = name
162 while self.treeView.exists(name):
163 n += 1
164 name = origname + '(' + str(n) + ')'
emayecs5966a532021-07-29 10:07:02 -0400165 # Note: iid value with spaces in it is a bad idea.
166 if ' ' in name:
167 name = name.replace(' ', '_')
emayecsc707a0a2021-08-06 17:13:27 -0400168
emayecs5966a532021-07-29 10:07:02 -0400169 # optionally: Mark directories with trailing slash
170 if self.markDir and os.path.isdir(item):
171 origname += "/"
emayecs582bc382021-08-13 12:32:12 -0400172 if os.path.islink(item):
173 origname += " (link)"
emayecs12e85282021-08-11 09:37:00 -0400174
175 if ('subcells' not in item):
176 mode = 'even' if mode == 'odd' else 'odd'
177 self.treeView.insert('', 'end', text=origname, iid=item, value=item, tag=mode)
178 else:
179 self.treeView.insert('', 'end', text=origname, iid=item, value=item, tag='odd')
emayecsc707a0a2021-08-06 17:13:27 -0400180
emayecsc707a0a2021-08-06 17:13:27 -0400181 if 'subcells' in os.path.split(item)[0]:
182 # If a project is a subproject, move it under its parent
183 parent_path = os.path.split(os.path.split(item)[0])[0]
184 parent_name = os.path.split(parent_path)[1]
emayecs55354d82021-08-08 15:57:32 -0400185 self.treeView.move(item,parent_path,m)
emayecsc707a0a2021-08-06 17:13:27 -0400186 m+=1
187 else:
188 # If its not a subproject, create a "subproject" of itself
189 # iid shouldn't be repeated since it starts with '.'
emayecs12e85282021-08-11 09:37:00 -0400190 self.treeView.insert('', 'end', text=origname, iid='.'+item, value=item, tag='odd')
emayecs55354d82021-08-08 15:57:32 -0400191 self.treeView.move('.'+item,item,0)
emayecsc707a0a2021-08-06 17:13:27 -0400192 m=1
emayecs12e85282021-08-11 09:37:00 -0400193
194
emayecs5966a532021-07-29 10:07:02 -0400195 if self.initSelected and self.treeView.exists(self.initSelected):
emayecs55354d82021-08-08 15:57:32 -0400196 if 'subcells' in self.initSelected:
emayecsa71088b2021-08-14 18:02:58 -0400197 # ancestor projects must be expanded before setting current
198 item_path = self.initSelected
199 ancestors = []
200 while 'subcells' in item_path:
201 item_path = os.path.split(os.path.split(item_path)[0])[0]
202 ancestors.insert(0,item_path)
203 for a in ancestors:
204 self.treeView.item(a, open=True)
205 self.setselect(self.initSelected)
emayecs12e85282021-08-11 09:37:00 -0400206 elif self.initSelected[0]=='.':
emayecsa71088b2021-08-14 18:02:58 -0400207 parent_path = self.initSelected[1:]
208 self.treeView.item(parent_path, open=True)
209 self.setselect(self.initSelected)
emayecs55354d82021-08-08 15:57:32 -0400210 else:
211 self.setselect(self.initSelected)
emayecs5966a532021-07-29 10:07:02 -0400212 self.initSelected = None
emayecs12e85282021-08-11 09:37:00 -0400213
emayecs5966a532021-07-29 10:07:02 -0400214
215 for button in self.func_buttons:
216 button[0].pack_forget()
217
218 if len(self.itemlist) == 0:
219 self.treeView.insert('', 'end', text=self.emptyMessage)
220 self.emptyMessage = self.emptyMessage1 # discard optional special 1st loading... message
221 for button in self.func_buttons:
222 if button[1]:
223 button[0].pack(side='left', padx = 5)
224 else:
225 for button in self.func_buttons:
226 button[0].pack(side='left', padx = 5)
227
228 # Return values from the treeview
229 def getvaluelist(self):
230 valuelist = []
231 itemlist = self.treeView.get_children()
232 for item in itemlist:
233 value = self.treeView.item(item, 'values')
234 valuelist.append(value)
235 return valuelist
236
emayecsc707a0a2021-08-06 17:13:27 -0400237 # Return items (id's) from the treeview
emayecs5966a532021-07-29 10:07:02 -0400238 def getlist(self):
239 return self.treeView.get_children()
240
241 # This is a bit of a hack way to populate a second column,
242 # but it works. It only works for one additional column,
243 # though, or else tuples will have to be generated differently.
244
245 def populate2(self, title, itemlist=[], valuelist=[]):
emayecs582bc382021-08-13 12:32:12 -0400246 # Populate the pdk column
emayecs5966a532021-07-29 10:07:02 -0400247 self.treeView.heading(1, text = title)
248 self.treeView.column(1, anchor='center')
emayecsc707a0a2021-08-06 17:13:27 -0400249 children=list(self.getlist())
250
251 # Add id's of subprojects
252 i=1
emayecs582bc382021-08-13 12:32:12 -0400253 def add_ids(grandchildren):
254 # Recursively add id's of all descendants to the list
255 nonlocal i
emayecsc707a0a2021-08-06 17:13:27 -0400256 for g in grandchildren:
257 children.insert(i,g)
258 i+=1
emayecs582bc382021-08-13 12:32:12 -0400259 descendants=self.treeView.get_children(item=g)
260 add_ids(descendants)
emayecsc707a0a2021-08-06 17:13:27 -0400261
emayecs582bc382021-08-13 12:32:12 -0400262 for c in list(self.getlist()):
263 grandchildren=self.treeView.get_children(item=c)
264 add_ids(grandchildren)
emayecsa71088b2021-08-14 18:02:58 -0400265 i+=1
emayecs582bc382021-08-13 12:32:12 -0400266
emayecs5966a532021-07-29 10:07:02 -0400267 n = 0
268 for item in valuelist:
emayecsc707a0a2021-08-06 17:13:27 -0400269 child = children[n]
270 # Get the value at this index
emayecs5966a532021-07-29 10:07:02 -0400271 oldvalue = self.treeView.item(child, 'values')
272 newvalue = (oldvalue, item)
273 self.treeView.item(child, values = newvalue)
emayecsc707a0a2021-08-06 17:13:27 -0400274 # Add pdk for the "copy" of the project that is made in the treeview
275 if (n+1<len(children) and children[n+1]=='.'+child):
276 valuelist.insert(n,item)
emayecs5966a532021-07-29 10:07:02 -0400277 n += 1
emayecsc707a0a2021-08-06 17:13:27 -0400278
emayecs5966a532021-07-29 10:07:02 -0400279 def func_callback(self, callback, event=None):
280 callback(self.treeView.item(self.treeView.selection()))
281
282 def bindselect(self, callback):
283 self.selectcallback = callback
284
285 def setselect(self, value):
286 self.treeView.selection_set(value)
emayecsa71088b2021-08-14 18:02:58 -0400287
emayecs5966a532021-07-29 10:07:02 -0400288 def selected(self):
289 value = self.treeView.item(self.treeView.selection())
290 if value['values']:
291 return value
292 else:
293 return None