blob: 67b07c2edef3e34e04930be9cffe741bcd559d56 [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
Tim Edwards7519dfb2022-02-10 11:39:09 -050010import natural_sort
11
emayecs5966a532021-07-29 10:07:02 -040012#------------------------------------------------------
13# Tree view used as a multi-column list box
14#------------------------------------------------------
15
16class TreeViewChoice(ttk.Frame):
17 def __init__(self, parent, fontsize=11, markDir=False, deferLoad=False, selectVal=None, natSort=False, *args, **kwargs):
18 ttk.Frame.__init__(self, parent, *args, **kwargs)
19 s = ttk.Style()
20 s.configure('normal.TLabel', font=('Helvetica', fontsize))
21 s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold'))
22 s.configure('normal.TButton', font=('Helvetica', fontsize),
23 border = 3, relief = 'raised')
24 s.configure('Treeview.Heading', font=('Helvetica', fontsize, 'bold'))
25 s.configure('Treeview.Column', font=('Helvetica', fontsize))
26 self.markDir = markDir
27 self.initSelected = selectVal
28 self.natSort = natSort
29 self.emptyMessage1 = '(no items)'
30 self.emptyMessage = '(loading...)' if deferLoad else self.emptyMessage1
31
32 # Last item is a list of 2-item lists, each containing the name of a button
33 # to place along the button bar at the bottom, and a callback function to
34 # run when the button is pressed.
35
36 def populate(self, title, itemlist=[], buttons=[], height=10, columns=[0],
37 versioning=False):
38 self.itemlist = itemlist[:]
39
40 treeFrame = ttk.Frame(self)
41 treeFrame.pack(side='top', padx=5, pady=5, fill='both', expand='true')
42
43 scrollBar = ttk.Scrollbar(treeFrame)
44 scrollBar.pack(side='right', fill='y')
45 self.treeView = ttk.Treeview(treeFrame, selectmode='browse', columns=columns, height=height)
46 self.treeView.pack(side='left', fill='both', expand='true')
47 scrollBar.config(command=self.treeView.yview)
48 self.treeView.config(yscrollcommand=scrollBar.set)
49 self.treeView.heading('#0', text=title, anchor='w')
50 buttonFrame = ttk.Frame(self)
51 buttonFrame.pack(side='bottom', fill='x')
52
53 self.treeView.tag_configure('odd',background='white',foreground='black')
54 self.treeView.tag_configure('even',background='gray90',foreground='black')
55 self.treeView.tag_configure('select',background='darkslategray',foreground='white')
56
57 self.func_buttons = []
58 for button in buttons:
59 func = button[2]
60 # Each func_buttons entry is a list of two items; first is the
61 # button widget, and the second is a boolean that is True if the
62 # button is to be present always, False if the button is only
63 # present when there are entries in the itemlist.
emayecsca79e462021-08-19 16:18:50 -040064 if(button[0]=='Flow'):
65 self.flowcallback = func
66
emayecs5966a532021-07-29 10:07:02 -040067 self.func_buttons.append([ttk.Button(buttonFrame, text=button[0],
68 style = 'normal.TButton',
69 command = lambda func=func: self.func_callback(func)),
70 button[1]])
emayecsca79e462021-08-19 16:18:50 -040071
emayecs5966a532021-07-29 10:07:02 -040072 self.selectcallback = None
73 self.lastselected = None
74 self.lasttag = None
75 self.treeView.bind('<<TreeviewSelect>>', self.retag)
76 self.repopulate(itemlist, versioning)
emayecs4cd7f232021-08-19 16:22:35 -040077 self.treeView.bind('<Double-1>', self.double_click)
emayecs5966a532021-07-29 10:07:02 -040078
emayecs4cd7f232021-08-19 16:22:35 -040079 def double_click(self, event):
emayecsca79e462021-08-19 16:18:50 -040080 self.flowcallback(self.treeView.item(self.treeView.selection()))
81
emayecs5966a532021-07-29 10:07:02 -040082 def get_button(self, index):
83 if index >= 0 and index < len(self.func_buttons):
84 return self.func_buttons[index][0]
85 else:
86 return None
87
88 def retag(self, value):
89 treeview = value.widget
90 try:
91 selection = treeview.selection()[0]
92 oldtag = self.treeView.item(selection, 'tag')[0]
93 except IndexError:
94 # No items in view; can't select the "(no items)" line.
95 return
96
97 self.treeView.item(selection, tag='selected')
98 if self.lastselected:
99 try:
100 self.treeView.item(self.lastselected, tag=self.lasttag)
101 except:
102 # Last selected item got deleted. Ignore this.
103 pass
104 if self.selectcallback:
105 self.selectcallback(value)
emayecs12e85282021-08-11 09:37:00 -0400106 if (selection!=self.lastselected):
107 self.lastselected = selection
108 self.lasttag = oldtag
emayecsc707a0a2021-08-06 17:13:27 -0400109
110 #Populate the project view
emayecs5966a532021-07-29 10:07:02 -0400111 def repopulate(self, itemlist=[], versioning=False):
emayecs5966a532021-07-29 10:07:02 -0400112 # Remove all children of treeview
113 self.treeView.delete(*self.treeView.get_children())
114
115 if self.natSort:
Tim Edwards7519dfb2022-02-10 11:39:09 -0500116 self.itemlist = natural_sort.natural_sort(itemlist)
emayecs5966a532021-07-29 10:07:02 -0400117 else:
118 self.itemlist = itemlist[:]
119 self.itemlist.sort()
120
121 mode = 'even'
emayecsc707a0a2021-08-06 17:13:27 -0400122 m=0
emayecs5966a532021-07-29 10:07:02 -0400123 for item in self.itemlist:
124 # Special handling of JSON files. The following reads a JSON file and
125 # finds key 'ip-name' in dictionary 'data-sheet', if such exists. If
126 # not, it looks for key 'project' in the top level. Failing that, it
127 # lists the name of the JSON file (which is probably an ungainly hash
128 # name).
129
130 fileext = os.path.splitext(item)
131 if fileext[1] == '.json':
132 # Read contents of JSON file
133 with open(item, 'r') as f:
134 try:
135 datatop = json.load(f)
136 except json.decoder.JSONDecodeError:
137 name = os.path.split(item)[1]
138 else:
139 name = []
140 if 'data-sheet' in datatop:
141 dsheet = datatop['data-sheet']
142 if 'ip-name' in dsheet:
143 name = dsheet['ip-name']
144 if not name and 'project' in datatop:
145 name = datatop['project']
146 if not name:
147 name = os.path.split(item)[1]
148 elif versioning == True:
149 # If versioning is true, then the last path component is the
150 # version number, and the penultimate path component is the
151 # name.
emayecsc707a0a2021-08-06 17:13:27 -0400152 version = os.path.split(item)[1]
emayecs5966a532021-07-29 10:07:02 -0400153 name = os.path.split(os.path.split(item)[0])[1] + ' (v' + version + ')'
154 else:
155 name = os.path.split(item)[1]
156
157 # Watch for duplicate items!
158 n = 0
159 origname = name
160 while self.treeView.exists(name):
161 n += 1
162 name = origname + '(' + str(n) + ')'
emayecs5966a532021-07-29 10:07:02 -0400163 # Note: iid value with spaces in it is a bad idea.
164 if ' ' in name:
165 name = name.replace(' ', '_')
emayecsc707a0a2021-08-06 17:13:27 -0400166
emayecs5966a532021-07-29 10:07:02 -0400167 # optionally: Mark directories with trailing slash
168 if self.markDir and os.path.isdir(item):
169 origname += "/"
emayecs582bc382021-08-13 12:32:12 -0400170 if os.path.islink(item):
171 origname += " (link)"
emayecs12e85282021-08-11 09:37:00 -0400172
173 if ('subcells' not in item):
174 mode = 'even' if mode == 'odd' else 'odd'
175 self.treeView.insert('', 'end', text=origname, iid=item, value=item, tag=mode)
176 else:
177 self.treeView.insert('', 'end', text=origname, iid=item, value=item, tag='odd')
emayecsc707a0a2021-08-06 17:13:27 -0400178
emayecsc707a0a2021-08-06 17:13:27 -0400179 if 'subcells' in os.path.split(item)[0]:
180 # If a project is a subproject, move it under its parent
181 parent_path = os.path.split(os.path.split(item)[0])[0]
182 parent_name = os.path.split(parent_path)[1]
emayecs55354d82021-08-08 15:57:32 -0400183 self.treeView.move(item,parent_path,m)
emayecsc707a0a2021-08-06 17:13:27 -0400184 m+=1
185 else:
186 # If its not a subproject, create a "subproject" of itself
187 # iid shouldn't be repeated since it starts with '.'
emayecs12e85282021-08-11 09:37:00 -0400188 self.treeView.insert('', 'end', text=origname, iid='.'+item, value=item, tag='odd')
emayecs55354d82021-08-08 15:57:32 -0400189 self.treeView.move('.'+item,item,0)
emayecsc707a0a2021-08-06 17:13:27 -0400190 m=1
emayecs12e85282021-08-11 09:37:00 -0400191
192
emayecs5966a532021-07-29 10:07:02 -0400193 if self.initSelected and self.treeView.exists(self.initSelected):
emayecs55354d82021-08-08 15:57:32 -0400194 if 'subcells' in self.initSelected:
emayecsa71088b2021-08-14 18:02:58 -0400195 # ancestor projects must be expanded before setting current
196 item_path = self.initSelected
197 ancestors = []
198 while 'subcells' in item_path:
199 item_path = os.path.split(os.path.split(item_path)[0])[0]
200 ancestors.insert(0,item_path)
201 for a in ancestors:
202 self.treeView.item(a, open=True)
203 self.setselect(self.initSelected)
emayecs12e85282021-08-11 09:37:00 -0400204 elif self.initSelected[0]=='.':
emayecsa71088b2021-08-14 18:02:58 -0400205 parent_path = self.initSelected[1:]
206 self.treeView.item(parent_path, open=True)
207 self.setselect(self.initSelected)
emayecs55354d82021-08-08 15:57:32 -0400208 else:
209 self.setselect(self.initSelected)
emayecs5966a532021-07-29 10:07:02 -0400210 self.initSelected = None
emayecs12e85282021-08-11 09:37:00 -0400211
emayecs5966a532021-07-29 10:07:02 -0400212
213 for button in self.func_buttons:
214 button[0].pack_forget()
215
216 if len(self.itemlist) == 0:
217 self.treeView.insert('', 'end', text=self.emptyMessage)
218 self.emptyMessage = self.emptyMessage1 # discard optional special 1st loading... message
219 for button in self.func_buttons:
220 if button[1]:
221 button[0].pack(side='left', padx = 5)
222 else:
223 for button in self.func_buttons:
224 button[0].pack(side='left', padx = 5)
225
226 # Return values from the treeview
227 def getvaluelist(self):
228 valuelist = []
229 itemlist = self.treeView.get_children()
230 for item in itemlist:
231 value = self.treeView.item(item, 'values')
232 valuelist.append(value)
233 return valuelist
234
emayecsc707a0a2021-08-06 17:13:27 -0400235 # Return items (id's) from the treeview
emayecs5966a532021-07-29 10:07:02 -0400236 def getlist(self):
237 return self.treeView.get_children()
238
239 # This is a bit of a hack way to populate a second column,
240 # but it works. It only works for one additional column,
241 # though, or else tuples will have to be generated differently.
242
243 def populate2(self, title, itemlist=[], valuelist=[]):
emayecs582bc382021-08-13 12:32:12 -0400244 # Populate the pdk column
emayecs5966a532021-07-29 10:07:02 -0400245 self.treeView.heading(1, text = title)
246 self.treeView.column(1, anchor='center')
emayecsc707a0a2021-08-06 17:13:27 -0400247 children=list(self.getlist())
248
249 # Add id's of subprojects
250 i=1
emayecs582bc382021-08-13 12:32:12 -0400251 def add_ids(grandchildren):
252 # Recursively add id's of all descendants to the list
253 nonlocal i
emayecsc707a0a2021-08-06 17:13:27 -0400254 for g in grandchildren:
255 children.insert(i,g)
256 i+=1
emayecs582bc382021-08-13 12:32:12 -0400257 descendants=self.treeView.get_children(item=g)
258 add_ids(descendants)
emayecsc707a0a2021-08-06 17:13:27 -0400259
emayecs582bc382021-08-13 12:32:12 -0400260 for c in list(self.getlist()):
261 grandchildren=self.treeView.get_children(item=c)
262 add_ids(grandchildren)
emayecsa71088b2021-08-14 18:02:58 -0400263 i+=1
emayecs582bc382021-08-13 12:32:12 -0400264
emayecs5966a532021-07-29 10:07:02 -0400265 n = 0
266 for item in valuelist:
emayecsc707a0a2021-08-06 17:13:27 -0400267 child = children[n]
268 # Get the value at this index
emayecs5966a532021-07-29 10:07:02 -0400269 oldvalue = self.treeView.item(child, 'values')
270 newvalue = (oldvalue, item)
271 self.treeView.item(child, values = newvalue)
emayecsc707a0a2021-08-06 17:13:27 -0400272 # Add pdk for the "copy" of the project that is made in the treeview
273 if (n+1<len(children) and children[n+1]=='.'+child):
274 valuelist.insert(n,item)
emayecs5966a532021-07-29 10:07:02 -0400275 n += 1
emayecsc707a0a2021-08-06 17:13:27 -0400276
emayecs5966a532021-07-29 10:07:02 -0400277 def func_callback(self, callback, event=None):
278 callback(self.treeView.item(self.treeView.selection()))
279
280 def bindselect(self, callback):
281 self.selectcallback = callback
282
283 def setselect(self, value):
284 self.treeView.selection_set(value)
emayecsa71088b2021-08-14 18:02:58 -0400285
emayecs5966a532021-07-29 10:07:02 -0400286 def selected(self):
287 value = self.treeView.item(self.treeView.selection())
288 if value['values']:
289 return value
290 else:
291 return None