]>
Commit | Line | Data |
---|---|---|
580b1120 LTL |
1 | # @ ConfigEditor.py\r |
2 | #\r | |
3 | # Copyright(c) 2018 - 2021, Intel Corporation. All rights reserved.<BR>\r | |
4 | # SPDX-License-Identifier: BSD-2-Clause-Patent\r | |
5 | #\r | |
6 | ##\r | |
7 | \r | |
8 | import os\r | |
9 | import sys\r | |
10 | import marshal\r | |
11 | import tkinter\r | |
12 | import tkinter.ttk as ttk\r | |
13 | import tkinter.messagebox as messagebox\r | |
14 | import tkinter.filedialog as filedialog\r | |
15 | \r | |
16 | from pathlib import Path\r | |
17 | from GenYamlCfg import CGenYamlCfg, bytes_to_value, \\r | |
18 | bytes_to_bracket_str, value_to_bytes, array_str_to_value\r | |
19 | from ctypes import sizeof, Structure, ARRAY, c_uint8, c_uint64, c_char, \\r | |
20 | c_uint32, c_uint16\r | |
21 | from functools import reduce\r | |
22 | \r | |
23 | sys.path.insert(0, '..')\r | |
24 | from FspDscBsf2Yaml import bsf_to_dsc, dsc_to_yaml # noqa\r | |
25 | \r | |
26 | \r | |
27 | sys.dont_write_bytecode = True\r | |
28 | \r | |
29 | \r | |
30 | class create_tool_tip(object):\r | |
31 | '''\r | |
32 | create a tooltip for a given widget\r | |
33 | '''\r | |
34 | in_progress = False\r | |
35 | \r | |
36 | def __init__(self, widget, text=''):\r | |
37 | self.top_win = None\r | |
38 | self.widget = widget\r | |
39 | self.text = text\r | |
40 | self.widget.bind("<Enter>", self.enter)\r | |
41 | self.widget.bind("<Leave>", self.leave)\r | |
42 | \r | |
43 | def enter(self, event=None):\r | |
44 | if self.in_progress:\r | |
45 | return\r | |
46 | if self.widget.winfo_class() == 'Treeview':\r | |
47 | # Only show help when cursor is on row header.\r | |
48 | rowid = self.widget.identify_row(event.y)\r | |
49 | if rowid != '':\r | |
50 | return\r | |
51 | else:\r | |
52 | x, y, cx, cy = self.widget.bbox("insert")\r | |
53 | \r | |
54 | cursor = self.widget.winfo_pointerxy()\r | |
55 | x = self.widget.winfo_rootx() + 35\r | |
56 | y = self.widget.winfo_rooty() + 20\r | |
57 | if cursor[1] > y and cursor[1] < y + 20:\r | |
58 | y += 20\r | |
59 | \r | |
60 | # creates a toplevel window\r | |
61 | self.top_win = tkinter.Toplevel(self.widget)\r | |
62 | # Leaves only the label and removes the app window\r | |
63 | self.top_win.wm_overrideredirect(True)\r | |
64 | self.top_win.wm_geometry("+%d+%d" % (x, y))\r | |
65 | label = tkinter.Message(self.top_win,\r | |
66 | text=self.text,\r | |
67 | justify='left',\r | |
68 | background='bisque',\r | |
69 | relief='solid',\r | |
70 | borderwidth=1,\r | |
71 | font=("times", "10", "normal"))\r | |
72 | label.pack(ipadx=1)\r | |
73 | self.in_progress = True\r | |
74 | \r | |
75 | def leave(self, event=None):\r | |
76 | if self.top_win:\r | |
77 | self.top_win.destroy()\r | |
78 | self.in_progress = False\r | |
79 | \r | |
80 | \r | |
81 | class validating_entry(tkinter.Entry):\r | |
82 | def __init__(self, master, **kw):\r | |
83 | tkinter.Entry.__init__(*(self, master), **kw)\r | |
84 | self.parent = master\r | |
85 | self.old_value = ''\r | |
86 | self.last_value = ''\r | |
87 | self.variable = tkinter.StringVar()\r | |
88 | self.variable.trace("w", self.callback)\r | |
89 | self.config(textvariable=self.variable)\r | |
90 | self.config({"background": "#c0c0c0"})\r | |
91 | self.bind("<Return>", self.move_next)\r | |
92 | self.bind("<Tab>", self.move_next)\r | |
93 | self.bind("<Escape>", self.cancel)\r | |
94 | for each in ['BackSpace', 'Delete']:\r | |
95 | self.bind("<%s>" % each, self.ignore)\r | |
96 | self.display(None)\r | |
97 | \r | |
98 | def ignore(self, even):\r | |
99 | return "break"\r | |
100 | \r | |
101 | def move_next(self, event):\r | |
102 | if self.row < 0:\r | |
103 | return\r | |
104 | row, col = self.row, self.col\r | |
105 | txt, row_id, col_id = self.parent.get_next_cell(row, col)\r | |
106 | self.display(txt, row_id, col_id)\r | |
107 | return "break"\r | |
108 | \r | |
109 | def cancel(self, event):\r | |
110 | self.variable.set(self.old_value)\r | |
111 | self.display(None)\r | |
112 | \r | |
113 | def display(self, txt, row_id='', col_id=''):\r | |
114 | if txt is None:\r | |
115 | self.row = -1\r | |
116 | self.col = -1\r | |
117 | self.place_forget()\r | |
118 | else:\r | |
119 | row = int('0x' + row_id[1:], 0) - 1\r | |
120 | col = int(col_id[1:]) - 1\r | |
121 | self.row = row\r | |
122 | self.col = col\r | |
123 | self.old_value = txt\r | |
124 | self.last_value = txt\r | |
125 | x, y, width, height = self.parent.bbox(row_id, col)\r | |
126 | self.place(x=x, y=y, w=width)\r | |
127 | self.variable.set(txt)\r | |
128 | self.focus_set()\r | |
129 | self.icursor(0)\r | |
130 | \r | |
131 | def callback(self, *Args):\r | |
132 | cur_val = self.variable.get()\r | |
133 | new_val = self.validate(cur_val)\r | |
134 | if new_val is not None and self.row >= 0:\r | |
135 | self.last_value = new_val\r | |
136 | self.parent.set_cell(self.row, self.col, new_val)\r | |
137 | self.variable.set(self.last_value)\r | |
138 | \r | |
139 | def validate(self, value):\r | |
140 | if len(value) > 0:\r | |
141 | try:\r | |
142 | int(value, 16)\r | |
143 | except Exception:\r | |
144 | return None\r | |
145 | \r | |
146 | # Normalize the cell format\r | |
147 | self.update()\r | |
148 | cell_width = self.winfo_width()\r | |
149 | max_len = custom_table.to_byte_length(cell_width) * 2\r | |
150 | cur_pos = self.index("insert")\r | |
151 | if cur_pos == max_len + 1:\r | |
152 | value = value[-max_len:]\r | |
153 | else:\r | |
154 | value = value[:max_len]\r | |
155 | if value == '':\r | |
156 | value = '0'\r | |
157 | fmt = '%%0%dX' % max_len\r | |
158 | return fmt % int(value, 16)\r | |
159 | \r | |
160 | \r | |
161 | class custom_table(ttk.Treeview):\r | |
162 | _Padding = 20\r | |
163 | _Char_width = 6\r | |
164 | \r | |
165 | def __init__(self, parent, col_hdr, bins):\r | |
166 | cols = len(col_hdr)\r | |
167 | \r | |
168 | col_byte_len = []\r | |
169 | for col in range(cols): # Columns\r | |
170 | col_byte_len.append(int(col_hdr[col].split(':')[1]))\r | |
171 | \r | |
172 | byte_len = sum(col_byte_len)\r | |
173 | rows = (len(bins) + byte_len - 1) // byte_len\r | |
174 | \r | |
175 | self.rows = rows\r | |
176 | self.cols = cols\r | |
177 | self.col_byte_len = col_byte_len\r | |
178 | self.col_hdr = col_hdr\r | |
179 | \r | |
180 | self.size = len(bins)\r | |
181 | self.last_dir = ''\r | |
182 | \r | |
183 | style = ttk.Style()\r | |
184 | style.configure("Custom.Treeview.Heading",\r | |
185 | font=('calibri', 10, 'bold'),\r | |
186 | foreground="blue")\r | |
187 | ttk.Treeview.__init__(self, parent, height=rows,\r | |
188 | columns=[''] + col_hdr, show='headings',\r | |
189 | style="Custom.Treeview",\r | |
190 | selectmode='none')\r | |
191 | self.bind("<Button-1>", self.click)\r | |
192 | self.bind("<FocusOut>", self.focus_out)\r | |
193 | self.entry = validating_entry(self, width=4, justify=tkinter.CENTER)\r | |
194 | \r | |
195 | self.heading(0, text='LOAD')\r | |
196 | self.column(0, width=60, stretch=0, anchor=tkinter.CENTER)\r | |
197 | \r | |
198 | for col in range(cols): # Columns\r | |
199 | text = col_hdr[col].split(':')[0]\r | |
200 | byte_len = int(col_hdr[col].split(':')[1])\r | |
201 | self.heading(col+1, text=text)\r | |
202 | self.column(col+1, width=self.to_cell_width(byte_len),\r | |
203 | stretch=0, anchor=tkinter.CENTER)\r | |
204 | idx = 0\r | |
205 | for row in range(rows): # Rows\r | |
206 | text = '%04X' % (row * len(col_hdr))\r | |
207 | vals = ['%04X:' % (cols * row)]\r | |
208 | for col in range(cols): # Columns\r | |
209 | if idx >= len(bins):\r | |
210 | break\r | |
211 | byte_len = int(col_hdr[col].split(':')[1])\r | |
212 | value = bytes_to_value(bins[idx:idx+byte_len])\r | |
213 | hex = ("%%0%dX" % (byte_len * 2)) % value\r | |
214 | vals.append(hex)\r | |
215 | idx += byte_len\r | |
216 | self.insert('', 'end', values=tuple(vals))\r | |
217 | if idx >= len(bins):\r | |
218 | break\r | |
219 | \r | |
220 | @staticmethod\r | |
221 | def to_cell_width(byte_len):\r | |
222 | return byte_len * 2 * custom_table._Char_width + custom_table._Padding\r | |
223 | \r | |
224 | @staticmethod\r | |
225 | def to_byte_length(cell_width):\r | |
226 | return(cell_width - custom_table._Padding) \\r | |
227 | // (2 * custom_table._Char_width)\r | |
228 | \r | |
229 | def focus_out(self, event):\r | |
230 | self.entry.display(None)\r | |
231 | \r | |
232 | def refresh_bin(self, bins):\r | |
233 | if not bins:\r | |
234 | return\r | |
235 | \r | |
236 | # Reload binary into widget\r | |
237 | bin_len = len(bins)\r | |
238 | for row in range(self.rows):\r | |
239 | iid = self.get_children()[row]\r | |
240 | for col in range(self.cols):\r | |
241 | idx = row * sum(self.col_byte_len) + \\r | |
242 | sum(self.col_byte_len[:col])\r | |
243 | byte_len = self.col_byte_len[col]\r | |
244 | if idx + byte_len <= self.size:\r | |
245 | byte_len = int(self.col_hdr[col].split(':')[1])\r | |
246 | if idx + byte_len > bin_len:\r | |
247 | val = 0\r | |
248 | else:\r | |
249 | val = bytes_to_value(bins[idx:idx+byte_len])\r | |
250 | hex_val = ("%%0%dX" % (byte_len * 2)) % val\r | |
251 | self.set(iid, col + 1, hex_val)\r | |
252 | \r | |
253 | def get_cell(self, row, col):\r | |
254 | iid = self.get_children()[row]\r | |
255 | txt = self.item(iid, 'values')[col]\r | |
256 | return txt\r | |
257 | \r | |
258 | def get_next_cell(self, row, col):\r | |
259 | rows = self.get_children()\r | |
260 | col += 1\r | |
261 | if col > self.cols:\r | |
262 | col = 1\r | |
263 | row += 1\r | |
264 | cnt = row * sum(self.col_byte_len) + sum(self.col_byte_len[:col])\r | |
265 | if cnt > self.size:\r | |
266 | # Reached the last cell, so roll back to beginning\r | |
267 | row = 0\r | |
268 | col = 1\r | |
269 | \r | |
270 | txt = self.get_cell(row, col)\r | |
271 | row_id = rows[row]\r | |
272 | col_id = '#%d' % (col + 1)\r | |
273 | return(txt, row_id, col_id)\r | |
274 | \r | |
275 | def set_cell(self, row, col, val):\r | |
276 | iid = self.get_children()[row]\r | |
277 | self.set(iid, col, val)\r | |
278 | \r | |
279 | def load_bin(self):\r | |
280 | # Load binary from file\r | |
281 | path = filedialog.askopenfilename(\r | |
282 | initialdir=self.last_dir,\r | |
283 | title="Load binary file",\r | |
284 | filetypes=(("Binary files", "*.bin"), (\r | |
285 | "binary files", "*.bin")))\r | |
286 | if path:\r | |
287 | self.last_dir = os.path.dirname(path)\r | |
288 | fd = open(path, 'rb')\r | |
289 | bins = bytearray(fd.read())[:self.size]\r | |
290 | fd.close()\r | |
291 | bins.extend(b'\x00' * (self.size - len(bins)))\r | |
292 | return bins\r | |
293 | \r | |
294 | return None\r | |
295 | \r | |
296 | def click(self, event):\r | |
297 | row_id = self.identify_row(event.y)\r | |
298 | col_id = self.identify_column(event.x)\r | |
299 | if row_id == '' and col_id == '#1':\r | |
300 | # Clicked on "LOAD" cell\r | |
301 | bins = self.load_bin()\r | |
302 | self.refresh_bin(bins)\r | |
303 | return\r | |
304 | \r | |
305 | if col_id == '#1':\r | |
306 | # Clicked on column 1(Offset column)\r | |
307 | return\r | |
308 | \r | |
309 | item = self.identify('item', event.x, event.y)\r | |
310 | if not item or not col_id:\r | |
311 | # Not clicked on valid cell\r | |
312 | return\r | |
313 | \r | |
314 | # Clicked cell\r | |
315 | row = int('0x' + row_id[1:], 0) - 1\r | |
316 | col = int(col_id[1:]) - 1\r | |
317 | if row * self.cols + col > self.size:\r | |
318 | return\r | |
319 | \r | |
320 | vals = self.item(item, 'values')\r | |
321 | if col < len(vals):\r | |
322 | txt = self.item(item, 'values')[col]\r | |
323 | self.entry.display(txt, row_id, col_id)\r | |
324 | \r | |
325 | def get(self):\r | |
326 | bins = bytearray()\r | |
327 | row_ids = self.get_children()\r | |
328 | for row_id in row_ids:\r | |
329 | row = int('0x' + row_id[1:], 0) - 1\r | |
330 | for col in range(self.cols):\r | |
331 | idx = row * sum(self.col_byte_len) + \\r | |
332 | sum(self.col_byte_len[:col])\r | |
333 | byte_len = self.col_byte_len[col]\r | |
334 | if idx + byte_len > self.size:\r | |
335 | break\r | |
336 | hex = self.item(row_id, 'values')[col + 1]\r | |
337 | values = value_to_bytes(int(hex, 16)\r | |
338 | & ((1 << byte_len * 8) - 1), byte_len)\r | |
339 | bins.extend(values)\r | |
340 | return bins\r | |
341 | \r | |
342 | \r | |
343 | class c_uint24(Structure):\r | |
344 | """Little-Endian 24-bit Unsigned Integer"""\r | |
345 | _pack_ = 1\r | |
346 | _fields_ = [('Data', (c_uint8 * 3))]\r | |
347 | \r | |
348 | def __init__(self, val=0):\r | |
349 | self.set_value(val)\r | |
350 | \r | |
351 | def __str__(self, indent=0):\r | |
352 | return '0x%.6x' % self.value\r | |
353 | \r | |
354 | def __int__(self):\r | |
355 | return self.get_value()\r | |
356 | \r | |
357 | def set_value(self, val):\r | |
358 | self.Data[0:3] = Val2Bytes(val, 3)\r | |
359 | \r | |
360 | def get_value(self):\r | |
361 | return Bytes2Val(self.Data[0:3])\r | |
362 | \r | |
363 | value = property(get_value, set_value)\r | |
364 | \r | |
365 | \r | |
366 | class EFI_FIRMWARE_VOLUME_HEADER(Structure):\r | |
367 | _fields_ = [\r | |
368 | ('ZeroVector', ARRAY(c_uint8, 16)),\r | |
369 | ('FileSystemGuid', ARRAY(c_uint8, 16)),\r | |
370 | ('FvLength', c_uint64),\r | |
371 | ('Signature', ARRAY(c_char, 4)),\r | |
372 | ('Attributes', c_uint32),\r | |
373 | ('HeaderLength', c_uint16),\r | |
374 | ('Checksum', c_uint16),\r | |
375 | ('ExtHeaderOffset', c_uint16),\r | |
376 | ('Reserved', c_uint8),\r | |
377 | ('Revision', c_uint8)\r | |
378 | ]\r | |
379 | \r | |
380 | \r | |
381 | class EFI_FIRMWARE_VOLUME_EXT_HEADER(Structure):\r | |
382 | _fields_ = [\r | |
383 | ('FvName', ARRAY(c_uint8, 16)),\r | |
384 | ('ExtHeaderSize', c_uint32)\r | |
385 | ]\r | |
386 | \r | |
387 | \r | |
388 | class EFI_FFS_INTEGRITY_CHECK(Structure):\r | |
389 | _fields_ = [\r | |
390 | ('Header', c_uint8),\r | |
391 | ('File', c_uint8)\r | |
392 | ]\r | |
393 | \r | |
394 | \r | |
395 | class EFI_FFS_FILE_HEADER(Structure):\r | |
396 | _fields_ = [\r | |
397 | ('Name', ARRAY(c_uint8, 16)),\r | |
398 | ('IntegrityCheck', EFI_FFS_INTEGRITY_CHECK),\r | |
399 | ('Type', c_uint8),\r | |
400 | ('Attributes', c_uint8),\r | |
401 | ('Size', c_uint24),\r | |
402 | ('State', c_uint8)\r | |
403 | ]\r | |
404 | \r | |
405 | \r | |
406 | class EFI_COMMON_SECTION_HEADER(Structure):\r | |
407 | _fields_ = [\r | |
408 | ('Size', c_uint24),\r | |
409 | ('Type', c_uint8)\r | |
410 | ]\r | |
411 | \r | |
412 | \r | |
413 | class EFI_SECTION_TYPE:\r | |
414 | """Enumeration of all valid firmware file section types."""\r | |
415 | ALL = 0x00\r | |
416 | COMPRESSION = 0x01\r | |
417 | GUID_DEFINED = 0x02\r | |
418 | DISPOSABLE = 0x03\r | |
419 | PE32 = 0x10\r | |
420 | PIC = 0x11\r | |
421 | TE = 0x12\r | |
422 | DXE_DEPEX = 0x13\r | |
423 | VERSION = 0x14\r | |
424 | USER_INTERFACE = 0x15\r | |
425 | COMPATIBILITY16 = 0x16\r | |
426 | FIRMWARE_VOLUME_IMAGE = 0x17\r | |
427 | FREEFORM_SUBTYPE_GUID = 0x18\r | |
428 | RAW = 0x19\r | |
429 | PEI_DEPEX = 0x1b\r | |
430 | SMM_DEPEX = 0x1c\r | |
431 | \r | |
432 | \r | |
433 | class FSP_COMMON_HEADER(Structure):\r | |
434 | _fields_ = [\r | |
435 | ('Signature', ARRAY(c_char, 4)),\r | |
436 | ('HeaderLength', c_uint32)\r | |
437 | ]\r | |
438 | \r | |
439 | \r | |
440 | class FSP_INFORMATION_HEADER(Structure):\r | |
441 | _fields_ = [\r | |
442 | ('Signature', ARRAY(c_char, 4)),\r | |
443 | ('HeaderLength', c_uint32),\r | |
444 | ('Reserved1', c_uint16),\r | |
445 | ('SpecVersion', c_uint8),\r | |
446 | ('HeaderRevision', c_uint8),\r | |
447 | ('ImageRevision', c_uint32),\r | |
448 | ('ImageId', ARRAY(c_char, 8)),\r | |
449 | ('ImageSize', c_uint32),\r | |
450 | ('ImageBase', c_uint32),\r | |
451 | ('ImageAttribute', c_uint16),\r | |
452 | ('ComponentAttribute', c_uint16),\r | |
453 | ('CfgRegionOffset', c_uint32),\r | |
454 | ('CfgRegionSize', c_uint32),\r | |
455 | ('Reserved2', c_uint32),\r | |
456 | ('TempRamInitEntryOffset', c_uint32),\r | |
457 | ('Reserved3', c_uint32),\r | |
458 | ('NotifyPhaseEntryOffset', c_uint32),\r | |
459 | ('FspMemoryInitEntryOffset', c_uint32),\r | |
460 | ('TempRamExitEntryOffset', c_uint32),\r | |
461 | ('FspSiliconInitEntryOffset', c_uint32)\r | |
462 | ]\r | |
463 | \r | |
464 | \r | |
465 | class FSP_EXTENDED_HEADER(Structure):\r | |
466 | _fields_ = [\r | |
467 | ('Signature', ARRAY(c_char, 4)),\r | |
468 | ('HeaderLength', c_uint32),\r | |
469 | ('Revision', c_uint8),\r | |
470 | ('Reserved', c_uint8),\r | |
471 | ('FspProducerId', ARRAY(c_char, 6)),\r | |
472 | ('FspProducerRevision', c_uint32),\r | |
473 | ('FspProducerDataSize', c_uint32)\r | |
474 | ]\r | |
475 | \r | |
476 | \r | |
477 | class FSP_PATCH_TABLE(Structure):\r | |
478 | _fields_ = [\r | |
479 | ('Signature', ARRAY(c_char, 4)),\r | |
480 | ('HeaderLength', c_uint16),\r | |
481 | ('HeaderRevision', c_uint8),\r | |
482 | ('Reserved', c_uint8),\r | |
483 | ('PatchEntryNum', c_uint32)\r | |
484 | ]\r | |
485 | \r | |
486 | \r | |
487 | class Section:\r | |
488 | def __init__(self, offset, secdata):\r | |
489 | self.SecHdr = EFI_COMMON_SECTION_HEADER.from_buffer(secdata, 0)\r | |
490 | self.SecData = secdata[0:int(self.SecHdr.Size)]\r | |
491 | self.Offset = offset\r | |
492 | \r | |
493 | \r | |
494 | def AlignPtr(offset, alignment=8):\r | |
495 | return (offset + alignment - 1) & ~(alignment - 1)\r | |
496 | \r | |
497 | \r | |
498 | def Bytes2Val(bytes):\r | |
499 | return reduce(lambda x, y: (x << 8) | y, bytes[:: -1])\r | |
500 | \r | |
501 | \r | |
502 | def Val2Bytes(value, blen):\r | |
503 | return [(value >> (i*8) & 0xff) for i in range(blen)]\r | |
504 | \r | |
505 | \r | |
506 | class FirmwareFile:\r | |
507 | def __init__(self, offset, filedata):\r | |
508 | self.FfsHdr = EFI_FFS_FILE_HEADER.from_buffer(filedata, 0)\r | |
509 | self.FfsData = filedata[0:int(self.FfsHdr.Size)]\r | |
510 | self.Offset = offset\r | |
511 | self.SecList = []\r | |
512 | \r | |
513 | def ParseFfs(self):\r | |
514 | ffssize = len(self.FfsData)\r | |
515 | offset = sizeof(self.FfsHdr)\r | |
516 | if self.FfsHdr.Name != '\xff' * 16:\r | |
517 | while offset < (ffssize - sizeof(EFI_COMMON_SECTION_HEADER)):\r | |
518 | sechdr = EFI_COMMON_SECTION_HEADER.from_buffer(\r | |
519 | self.FfsData, offset)\r | |
520 | sec = Section(\r | |
521 | offset, self.FfsData[offset:offset + int(sechdr.Size)])\r | |
522 | self.SecList.append(sec)\r | |
523 | offset += int(sechdr.Size)\r | |
524 | offset = AlignPtr(offset, 4)\r | |
525 | \r | |
526 | \r | |
527 | class FirmwareVolume:\r | |
528 | def __init__(self, offset, fvdata):\r | |
529 | self.FvHdr = EFI_FIRMWARE_VOLUME_HEADER.from_buffer(fvdata, 0)\r | |
530 | self.FvData = fvdata[0: self.FvHdr.FvLength]\r | |
531 | self.Offset = offset\r | |
532 | if self.FvHdr.ExtHeaderOffset > 0:\r | |
533 | self.FvExtHdr = EFI_FIRMWARE_VOLUME_EXT_HEADER.from_buffer(\r | |
534 | self.FvData, self.FvHdr.ExtHeaderOffset)\r | |
535 | else:\r | |
536 | self.FvExtHdr = None\r | |
537 | self.FfsList = []\r | |
538 | \r | |
539 | def ParseFv(self):\r | |
540 | fvsize = len(self.FvData)\r | |
541 | if self.FvExtHdr:\r | |
542 | offset = self.FvHdr.ExtHeaderOffset + self.FvExtHdr.ExtHeaderSize\r | |
543 | else:\r | |
544 | offset = self.FvHdr.HeaderLength\r | |
545 | offset = AlignPtr(offset)\r | |
546 | while offset < (fvsize - sizeof(EFI_FFS_FILE_HEADER)):\r | |
547 | ffshdr = EFI_FFS_FILE_HEADER.from_buffer(self.FvData, offset)\r | |
548 | if (ffshdr.Name == '\xff' * 16) and \\r | |
549 | (int(ffshdr.Size) == 0xFFFFFF):\r | |
550 | offset = fvsize\r | |
551 | else:\r | |
552 | ffs = FirmwareFile(\r | |
553 | offset, self.FvData[offset:offset + int(ffshdr.Size)])\r | |
554 | ffs.ParseFfs()\r | |
555 | self.FfsList.append(ffs)\r | |
556 | offset += int(ffshdr.Size)\r | |
557 | offset = AlignPtr(offset)\r | |
558 | \r | |
559 | \r | |
560 | class FspImage:\r | |
561 | def __init__(self, offset, fih, fihoff, patch):\r | |
562 | self.Fih = fih\r | |
563 | self.FihOffset = fihoff\r | |
564 | self.Offset = offset\r | |
565 | self.FvIdxList = []\r | |
566 | self.Type = "XTMSXXXXOXXXXXXX"[(fih.ComponentAttribute >> 12) & 0x0F]\r | |
567 | self.PatchList = patch\r | |
568 | self.PatchList.append(fihoff + 0x1C)\r | |
569 | \r | |
570 | def AppendFv(self, FvIdx):\r | |
571 | self.FvIdxList.append(FvIdx)\r | |
572 | \r | |
573 | def Patch(self, delta, fdbin):\r | |
574 | count = 0\r | |
575 | applied = 0\r | |
576 | for idx, patch in enumerate(self.PatchList):\r | |
577 | ptype = (patch >> 24) & 0x0F\r | |
578 | if ptype not in [0x00, 0x0F]:\r | |
579 | raise Exception('ERROR: Invalid patch type %d !' % ptype)\r | |
580 | if patch & 0x80000000:\r | |
581 | patch = self.Fih.ImageSize - (0x1000000 - (patch & 0xFFFFFF))\r | |
582 | else:\r | |
583 | patch = patch & 0xFFFFFF\r | |
584 | if (patch < self.Fih.ImageSize) and \\r | |
585 | (patch + sizeof(c_uint32) <= self.Fih.ImageSize):\r | |
586 | offset = patch + self.Offset\r | |
587 | value = Bytes2Val(fdbin[offset:offset+sizeof(c_uint32)])\r | |
588 | value += delta\r | |
589 | fdbin[offset:offset+sizeof(c_uint32)] = Val2Bytes(\r | |
590 | value, sizeof(c_uint32))\r | |
591 | applied += 1\r | |
592 | count += 1\r | |
593 | # Don't count the FSP base address patch entry appended at the end\r | |
594 | if count != 0:\r | |
595 | count -= 1\r | |
596 | applied -= 1\r | |
597 | return (count, applied)\r | |
598 | \r | |
599 | \r | |
600 | class FirmwareDevice:\r | |
601 | def __init__(self, offset, FdData):\r | |
602 | self.FvList = []\r | |
603 | self.FspList = []\r | |
604 | self.FspExtList = []\r | |
605 | self.FihList = []\r | |
606 | self.BuildList = []\r | |
607 | self.OutputText = ""\r | |
608 | self.Offset = 0\r | |
609 | self.FdData = FdData\r | |
610 | \r | |
611 | def ParseFd(self):\r | |
612 | offset = 0\r | |
613 | fdsize = len(self.FdData)\r | |
614 | self.FvList = []\r | |
615 | while offset < (fdsize - sizeof(EFI_FIRMWARE_VOLUME_HEADER)):\r | |
616 | fvh = EFI_FIRMWARE_VOLUME_HEADER.from_buffer(self.FdData, offset)\r | |
617 | if b'_FVH' != fvh.Signature:\r | |
618 | raise Exception("ERROR: Invalid FV header !")\r | |
619 | fv = FirmwareVolume(\r | |
620 | offset, self.FdData[offset:offset + fvh.FvLength])\r | |
621 | fv.ParseFv()\r | |
622 | self.FvList.append(fv)\r | |
623 | offset += fv.FvHdr.FvLength\r | |
624 | \r | |
625 | def CheckFsp(self):\r | |
626 | if len(self.FspList) == 0:\r | |
627 | return\r | |
628 | \r | |
629 | fih = None\r | |
630 | for fsp in self.FspList:\r | |
631 | if not fih:\r | |
632 | fih = fsp.Fih\r | |
633 | else:\r | |
634 | newfih = fsp.Fih\r | |
635 | if (newfih.ImageId != fih.ImageId) or \\r | |
636 | (newfih.ImageRevision != fih.ImageRevision):\r | |
637 | raise Exception(\r | |
638 | "ERROR: Inconsistent FSP ImageId or "\r | |
639 | "ImageRevision detected !")\r | |
640 | \r | |
641 | def ParseFsp(self):\r | |
642 | flen = 0\r | |
643 | for idx, fv in enumerate(self.FvList):\r | |
644 | # Check if this FV contains FSP header\r | |
645 | if flen == 0:\r | |
646 | if len(fv.FfsList) == 0:\r | |
647 | continue\r | |
648 | ffs = fv.FfsList[0]\r | |
649 | if len(ffs.SecList) == 0:\r | |
650 | continue\r | |
651 | sec = ffs.SecList[0]\r | |
652 | if sec.SecHdr.Type != EFI_SECTION_TYPE.RAW:\r | |
653 | continue\r | |
654 | fihoffset = ffs.Offset + sec.Offset + sizeof(sec.SecHdr)\r | |
655 | fspoffset = fv.Offset\r | |
656 | offset = fspoffset + fihoffset\r | |
657 | fih = FSP_INFORMATION_HEADER.from_buffer(self.FdData, offset)\r | |
658 | self.FihList.append(fih)\r | |
659 | if b'FSPH' != fih.Signature:\r | |
660 | continue\r | |
661 | \r | |
662 | offset += fih.HeaderLength\r | |
663 | \r | |
664 | offset = AlignPtr(offset, 2)\r | |
665 | Extfih = FSP_EXTENDED_HEADER.from_buffer(self.FdData, offset)\r | |
666 | self.FspExtList.append(Extfih)\r | |
667 | offset = AlignPtr(offset, 4)\r | |
668 | plist = []\r | |
669 | while True:\r | |
670 | fch = FSP_COMMON_HEADER.from_buffer(self.FdData, offset)\r | |
671 | if b'FSPP' != fch.Signature:\r | |
672 | offset += fch.HeaderLength\r | |
673 | offset = AlignPtr(offset, 4)\r | |
674 | else:\r | |
675 | fspp = FSP_PATCH_TABLE.from_buffer(\r | |
676 | self.FdData, offset)\r | |
677 | offset += sizeof(fspp)\r | |
678 | start_offset = offset + 32\r | |
679 | end_offset = offset + 32\r | |
680 | while True:\r | |
681 | end_offset += 1\r | |
682 | if(self.FdData[\r | |
683 | end_offset: end_offset + 1] == b'\xff'):\r | |
684 | break\r | |
685 | self.BuildList.append(\r | |
686 | self.FdData[start_offset:end_offset])\r | |
687 | pdata = (c_uint32 * fspp.PatchEntryNum).from_buffer(\r | |
688 | self.FdData, offset)\r | |
689 | plist = list(pdata)\r | |
690 | break\r | |
691 | \r | |
692 | fsp = FspImage(fspoffset, fih, fihoffset, plist)\r | |
693 | fsp.AppendFv(idx)\r | |
694 | self.FspList.append(fsp)\r | |
695 | flen = fsp.Fih.ImageSize - fv.FvHdr.FvLength\r | |
696 | else:\r | |
697 | fsp.AppendFv(idx)\r | |
698 | flen -= fv.FvHdr.FvLength\r | |
699 | if flen < 0:\r | |
700 | raise Exception("ERROR: Incorrect FV size in image !")\r | |
701 | self.CheckFsp()\r | |
702 | \r | |
703 | def OutputFsp(self):\r | |
704 | def copy_text_to_clipboard():\r | |
705 | window.clipboard_clear()\r | |
706 | window.clipboard_append(self.OutputText)\r | |
707 | \r | |
708 | window = tkinter.Tk()\r | |
709 | window.title("Fsp Headers")\r | |
710 | window.resizable(0, 0)\r | |
711 | # Window Size\r | |
712 | window.geometry("300x400+350+150")\r | |
713 | frame = tkinter.Frame(window)\r | |
714 | frame.pack(side=tkinter.BOTTOM)\r | |
715 | # Vertical (y) Scroll Bar\r | |
716 | scroll = tkinter.Scrollbar(window)\r | |
717 | scroll.pack(side=tkinter.RIGHT, fill=tkinter.Y)\r | |
718 | text = tkinter.Text(window,\r | |
719 | wrap=tkinter.NONE, yscrollcommand=scroll.set)\r | |
720 | i = 0\r | |
721 | self.OutputText = self.OutputText + "Fsp Header Details \n\n"\r | |
722 | while i < len(self.FihList):\r | |
723 | try:\r | |
724 | self.OutputText += str(self.BuildList[i].decode()) + "\n"\r | |
725 | except Exception:\r | |
726 | self.OutputText += "No description found\n"\r | |
727 | self.OutputText += "FSP Header :\n "\r | |
728 | self.OutputText += "Signature : " + \\r | |
729 | str(self.FihList[i].Signature.decode('utf-8')) + "\n "\r | |
730 | self.OutputText += "Header Length : " + \\r | |
731 | str(hex(self.FihList[i].HeaderLength)) + "\n "\r | |
732 | self.OutputText += "Header Revision : " + \\r | |
733 | str(hex(self.FihList[i].HeaderRevision)) + "\n "\r | |
734 | self.OutputText += "Spec Version : " + \\r | |
735 | str(hex(self.FihList[i].SpecVersion)) + "\n "\r | |
736 | self.OutputText += "Image Revision : " + \\r | |
737 | str(hex(self.FihList[i].ImageRevision)) + "\n "\r | |
738 | self.OutputText += "Image Id : " + \\r | |
739 | str(self.FihList[i].ImageId.decode('utf-8')) + "\n "\r | |
740 | self.OutputText += "Image Size : " + \\r | |
741 | str(hex(self.FihList[i].ImageSize)) + "\n "\r | |
742 | self.OutputText += "Image Base : " + \\r | |
743 | str(hex(self.FihList[i].ImageBase)) + "\n "\r | |
744 | self.OutputText += "Image Attribute : " + \\r | |
745 | str(hex(self.FihList[i].ImageAttribute)) + "\n "\r | |
746 | self.OutputText += "Cfg Region Offset : " + \\r | |
747 | str(hex(self.FihList[i].CfgRegionOffset)) + "\n "\r | |
748 | self.OutputText += "Cfg Region Size : " + \\r | |
749 | str(hex(self.FihList[i].CfgRegionSize)) + "\n "\r | |
750 | self.OutputText += "API Entry Num : " + \\r | |
751 | str(hex(self.FihList[i].Reserved2)) + "\n "\r | |
752 | self.OutputText += "Temp Ram Init Entry : " + \\r | |
753 | str(hex(self.FihList[i].TempRamInitEntryOffset)) + "\n "\r | |
754 | self.OutputText += "FSP Init Entry : " + \\r | |
755 | str(hex(self.FihList[i].Reserved3)) + "\n "\r | |
756 | self.OutputText += "Notify Phase Entry : " + \\r | |
757 | str(hex(self.FihList[i].NotifyPhaseEntryOffset)) + "\n "\r | |
758 | self.OutputText += "Fsp Memory Init Entry : " + \\r | |
759 | str(hex(self.FihList[i].FspMemoryInitEntryOffset)) + "\n "\r | |
760 | self.OutputText += "Temp Ram Exit Entry : " + \\r | |
761 | str(hex(self.FihList[i].TempRamExitEntryOffset)) + "\n "\r | |
762 | self.OutputText += "Fsp Silicon Init Entry : " + \\r | |
763 | str(hex(self.FihList[i].FspSiliconInitEntryOffset)) + "\n\n"\r | |
764 | self.OutputText += "FSP Extended Header:\n "\r | |
765 | self.OutputText += "Signature : " + \\r | |
766 | str(self.FspExtList[i].Signature.decode('utf-8')) + "\n "\r | |
767 | self.OutputText += "Header Length : " + \\r | |
768 | str(hex(self.FspExtList[i].HeaderLength)) + "\n "\r | |
769 | self.OutputText += "Header Revision : " + \\r | |
770 | str(hex(self.FspExtList[i].Revision)) + "\n "\r | |
771 | self.OutputText += "Fsp Producer Id : " + \\r | |
772 | str(self.FspExtList[i].FspProducerId.decode('utf-8')) + "\n "\r | |
773 | self.OutputText += "FspProducerRevision : " + \\r | |
774 | str(hex(self.FspExtList[i].FspProducerRevision)) + "\n\n"\r | |
775 | i += 1\r | |
776 | text.insert(tkinter.INSERT, self.OutputText)\r | |
777 | text.pack()\r | |
778 | # Configure the scrollbars\r | |
779 | scroll.config(command=text.yview)\r | |
780 | copy_button = tkinter.Button(\r | |
781 | window, text="Copy to Clipboard", command=copy_text_to_clipboard)\r | |
782 | copy_button.pack(in_=frame, side=tkinter.LEFT, padx=20, pady=10)\r | |
783 | exit_button = tkinter.Button(\r | |
784 | window, text="Close", command=window.destroy)\r | |
785 | exit_button.pack(in_=frame, side=tkinter.RIGHT, padx=20, pady=10)\r | |
786 | window.mainloop()\r | |
787 | \r | |
788 | \r | |
789 | class state:\r | |
790 | def __init__(self):\r | |
791 | self.state = False\r | |
792 | \r | |
793 | def set(self, value):\r | |
794 | self.state = value\r | |
795 | \r | |
796 | def get(self):\r | |
797 | return self.state\r | |
798 | \r | |
799 | \r | |
800 | class application(tkinter.Frame):\r | |
801 | def __init__(self, master=None):\r | |
802 | root = master\r | |
803 | \r | |
804 | self.debug = True\r | |
805 | self.mode = 'FSP'\r | |
806 | self.last_dir = '.'\r | |
807 | self.page_id = ''\r | |
808 | self.page_list = {}\r | |
809 | self.conf_list = {}\r | |
810 | self.cfg_data_obj = None\r | |
811 | self.org_cfg_data_bin = None\r | |
812 | self.in_left = state()\r | |
813 | self.in_right = state()\r | |
cac83b6f LTL |
814 | self.search_text = ''\r |
815 | self.binseg_dict = {}\r | |
580b1120 LTL |
816 | \r |
817 | # Check if current directory contains a file with a .yaml extension\r | |
818 | # if not default self.last_dir to a Platform directory where it is\r | |
819 | # easier to locate *BoardPkg\CfgData\*Def.yaml files\r | |
820 | self.last_dir = '.'\r | |
821 | if not any(fname.endswith('.yaml') for fname in os.listdir('.')):\r | |
822 | platform_path = Path(os.path.realpath(__file__)).parents[2].\\r | |
823 | joinpath('Platform')\r | |
824 | if platform_path.exists():\r | |
825 | self.last_dir = platform_path\r | |
826 | \r | |
827 | tkinter.Frame.__init__(self, master, borderwidth=2)\r | |
828 | \r | |
829 | self.menu_string = [\r | |
830 | 'Save Config Data to Binary', 'Load Config Data from Binary',\r | |
831 | 'Show Binary Information',\r | |
832 | 'Load Config Changes from Delta File',\r | |
833 | 'Save Config Changes to Delta File',\r | |
834 | 'Save Full Config Data to Delta File',\r | |
835 | 'Open Config BSF file'\r | |
836 | ]\r | |
837 | \r | |
838 | root.geometry("1200x800")\r | |
839 | \r | |
cac83b6f LTL |
840 | # Search string\r |
841 | fram = tkinter.Frame(root)\r | |
842 | # adding label to search box\r | |
843 | tkinter.Label(fram, text='Text to find:').pack(side=tkinter.LEFT)\r | |
844 | # adding of single line text box\r | |
845 | self.edit = tkinter.Entry(fram, width=30)\r | |
846 | # positioning of text box\r | |
847 | self.edit.pack(\r | |
848 | side=tkinter.LEFT, fill=tkinter.BOTH, expand=1, padx=(4, 4))\r | |
849 | # setting focus\r | |
850 | self.edit.focus_set()\r | |
851 | # adding of search button\r | |
852 | butt = tkinter.Button(fram, text='Search', relief=tkinter.GROOVE,\r | |
853 | command=self.search_bar)\r | |
854 | butt.pack(side=tkinter.RIGHT, padx=(4, 4))\r | |
855 | fram.pack(side=tkinter.TOP, anchor=tkinter.SE)\r | |
856 | \r | |
580b1120 LTL |
857 | paned = ttk.Panedwindow(root, orient=tkinter.HORIZONTAL)\r |
858 | paned.pack(fill=tkinter.BOTH, expand=True, padx=(4, 4))\r | |
859 | \r | |
860 | status = tkinter.Label(master, text="", bd=1, relief=tkinter.SUNKEN,\r | |
861 | anchor=tkinter.W)\r | |
862 | status.pack(side=tkinter.BOTTOM, fill=tkinter.X)\r | |
863 | \r | |
864 | frame_left = ttk.Frame(paned, height=800, relief="groove")\r | |
865 | \r | |
866 | self.left = ttk.Treeview(frame_left, show="tree")\r | |
867 | \r | |
868 | # Set up tree HScroller\r | |
869 | pady = (10, 10)\r | |
870 | self.tree_scroll = ttk.Scrollbar(frame_left,\r | |
871 | orient="vertical",\r | |
872 | command=self.left.yview)\r | |
873 | self.left.configure(yscrollcommand=self.tree_scroll.set)\r | |
874 | self.left.bind("<<TreeviewSelect>>", self.on_config_page_select_change)\r | |
875 | self.left.bind("<Enter>", lambda e: self.in_left.set(True))\r | |
876 | self.left.bind("<Leave>", lambda e: self.in_left.set(False))\r | |
877 | self.left.bind("<MouseWheel>", self.on_tree_scroll)\r | |
878 | \r | |
879 | self.left.pack(side='left',\r | |
880 | fill=tkinter.BOTH,\r | |
881 | expand=True,\r | |
882 | padx=(5, 0),\r | |
883 | pady=pady)\r | |
884 | self.tree_scroll.pack(side='right', fill=tkinter.Y,\r | |
885 | pady=pady, padx=(0, 5))\r | |
886 | \r | |
887 | frame_right = ttk.Frame(paned, relief="groove")\r | |
888 | self.frame_right = frame_right\r | |
889 | \r | |
890 | self.conf_canvas = tkinter.Canvas(frame_right, highlightthickness=0)\r | |
891 | self.page_scroll = ttk.Scrollbar(frame_right,\r | |
892 | orient="vertical",\r | |
893 | command=self.conf_canvas.yview)\r | |
894 | self.right_grid = ttk.Frame(self.conf_canvas)\r | |
895 | self.conf_canvas.configure(yscrollcommand=self.page_scroll.set)\r | |
896 | self.conf_canvas.pack(side='left',\r | |
897 | fill=tkinter.BOTH,\r | |
898 | expand=True,\r | |
899 | pady=pady,\r | |
900 | padx=(5, 0))\r | |
901 | self.page_scroll.pack(side='right', fill=tkinter.Y,\r | |
902 | pady=pady, padx=(0, 5))\r | |
903 | self.conf_canvas.create_window(0, 0, window=self.right_grid,\r | |
904 | anchor='nw')\r | |
905 | self.conf_canvas.bind('<Enter>', lambda e: self.in_right.set(True))\r | |
906 | self.conf_canvas.bind('<Leave>', lambda e: self.in_right.set(False))\r | |
907 | self.conf_canvas.bind("<Configure>", self.on_canvas_configure)\r | |
908 | self.conf_canvas.bind_all("<MouseWheel>", self.on_page_scroll)\r | |
909 | \r | |
910 | paned.add(frame_left, weight=2)\r | |
911 | paned.add(frame_right, weight=10)\r | |
912 | \r | |
913 | style = ttk.Style()\r | |
914 | style.layout("Treeview", [('Treeview.treearea', {'sticky': 'nswe'})])\r | |
915 | \r | |
916 | menubar = tkinter.Menu(root)\r | |
917 | file_menu = tkinter.Menu(menubar, tearoff=0)\r | |
918 | file_menu.add_command(label="Open Config YAML file",\r | |
919 | command=self.load_from_yaml)\r | |
920 | file_menu.add_command(label=self.menu_string[6],\r | |
921 | command=self.load_from_bsf_file)\r | |
922 | file_menu.add_command(label=self.menu_string[2],\r | |
923 | command=self.load_from_fd)\r | |
924 | file_menu.add_command(label=self.menu_string[0],\r | |
925 | command=self.save_to_bin,\r | |
926 | state='disabled')\r | |
927 | file_menu.add_command(label=self.menu_string[1],\r | |
928 | command=self.load_from_bin,\r | |
929 | state='disabled')\r | |
930 | file_menu.add_command(label=self.menu_string[3],\r | |
931 | command=self.load_from_delta,\r | |
932 | state='disabled')\r | |
933 | file_menu.add_command(label=self.menu_string[4],\r | |
934 | command=self.save_to_delta,\r | |
935 | state='disabled')\r | |
936 | file_menu.add_command(label=self.menu_string[5],\r | |
937 | command=self.save_full_to_delta,\r | |
938 | state='disabled')\r | |
939 | file_menu.add_command(label="About", command=self.about)\r | |
940 | menubar.add_cascade(label="File", menu=file_menu)\r | |
941 | self.file_menu = file_menu\r | |
942 | \r | |
943 | root.config(menu=menubar)\r | |
944 | \r | |
945 | if len(sys.argv) > 1:\r | |
946 | path = sys.argv[1]\r | |
947 | if not path.endswith('.yaml') and not path.endswith('.pkl'):\r | |
948 | messagebox.showerror('LOADING ERROR',\r | |
949 | "Unsupported file '%s' !" % path)\r | |
950 | return\r | |
951 | else:\r | |
952 | self.load_cfg_file(path)\r | |
953 | \r | |
954 | if len(sys.argv) > 2:\r | |
955 | path = sys.argv[2]\r | |
956 | if path.endswith('.dlt'):\r | |
957 | self.load_delta_file(path)\r | |
958 | elif path.endswith('.bin'):\r | |
959 | self.load_bin_file(path)\r | |
960 | else:\r | |
961 | messagebox.showerror('LOADING ERROR',\r | |
962 | "Unsupported file '%s' !" % path)\r | |
963 | return\r | |
964 | \r | |
cac83b6f LTL |
965 | def search_bar(self):\r |
966 | # get data from text box\r | |
967 | self.search_text = self.edit.get()\r | |
968 | # Clear the page and update it according to search value\r | |
969 | self.refresh_config_data_page()\r | |
970 | \r | |
580b1120 LTL |
971 | def set_object_name(self, widget, name):\r |
972 | self.conf_list[id(widget)] = name\r | |
973 | \r | |
974 | def get_object_name(self, widget):\r | |
975 | if id(widget) in self.conf_list:\r | |
976 | return self.conf_list[id(widget)]\r | |
977 | else:\r | |
978 | return None\r | |
979 | \r | |
980 | def limit_entry_size(self, variable, limit):\r | |
981 | value = variable.get()\r | |
982 | if len(value) > limit:\r | |
983 | variable.set(value[:limit])\r | |
984 | \r | |
985 | def on_canvas_configure(self, event):\r | |
986 | self.right_grid.grid_columnconfigure(0, minsize=event.width)\r | |
987 | \r | |
988 | def on_tree_scroll(self, event):\r | |
989 | if not self.in_left.get() and self.in_right.get():\r | |
990 | # This prevents scroll event from being handled by both left and\r | |
991 | # right frame at the same time.\r | |
992 | self.on_page_scroll(event)\r | |
993 | return 'break'\r | |
994 | \r | |
995 | def on_page_scroll(self, event):\r | |
996 | if self.in_right.get():\r | |
997 | # Only scroll when it is in active area\r | |
998 | min, max = self.page_scroll.get()\r | |
999 | if not((min == 0.0) and (max == 1.0)):\r | |
1000 | self.conf_canvas.yview_scroll(-1 * int(event.delta / 120),\r | |
1001 | 'units')\r | |
1002 | \r | |
1003 | def update_visibility_for_widget(self, widget, args):\r | |
580b1120 LTL |
1004 | visible = True\r |
1005 | item = self.get_config_data_item_from_widget(widget, True)\r | |
1006 | if item is None:\r | |
1007 | return visible\r | |
1008 | elif not item:\r | |
1009 | return visible\r | |
cac83b6f LTL |
1010 | if self.cfg_data_obj.binseg_dict:\r |
1011 | str_split = item['path'].split('.')\r | |
1012 | if self.cfg_data_obj.binseg_dict[str_split[-2]] == -1:\r | |
1013 | visible = False\r | |
1014 | widget.grid_remove()\r | |
1015 | return visible\r | |
580b1120 LTL |
1016 | result = 1\r |
1017 | if item['condition']:\r | |
1018 | result = self.evaluate_condition(item)\r | |
1019 | if result == 2:\r | |
1020 | # Gray\r | |
1021 | widget.configure(state='disabled')\r | |
1022 | elif result == 0:\r | |
1023 | # Hide\r | |
1024 | visible = False\r | |
1025 | widget.grid_remove()\r | |
1026 | else:\r | |
1027 | # Show\r | |
1028 | widget.grid()\r | |
1029 | widget.configure(state='normal')\r | |
1030 | \r | |
cac83b6f LTL |
1031 | if visible and self.search_text != '':\r |
1032 | name = item['name']\r | |
1033 | if name.lower().find(self.search_text.lower()) == -1:\r | |
1034 | visible = False\r | |
1035 | widget.grid_remove()\r | |
1036 | \r | |
580b1120 LTL |
1037 | return visible\r |
1038 | \r | |
1039 | def update_widgets_visibility_on_page(self):\r | |
1040 | self.walk_widgets_in_layout(self.right_grid,\r | |
1041 | self.update_visibility_for_widget)\r | |
1042 | \r | |
1043 | def combo_select_changed(self, event):\r | |
1044 | self.update_config_data_from_widget(event.widget, None)\r | |
1045 | self.update_widgets_visibility_on_page()\r | |
1046 | \r | |
1047 | def edit_num_finished(self, event):\r | |
1048 | widget = event.widget\r | |
1049 | item = self.get_config_data_item_from_widget(widget)\r | |
1050 | if not item:\r | |
1051 | return\r | |
1052 | parts = item['type'].split(',')\r | |
1053 | if len(parts) > 3:\r | |
1054 | min = parts[2].lstrip()[1:]\r | |
1055 | max = parts[3].rstrip()[:-1]\r | |
1056 | min_val = array_str_to_value(min)\r | |
1057 | max_val = array_str_to_value(max)\r | |
1058 | text = widget.get()\r | |
1059 | if ',' in text:\r | |
1060 | text = '{ %s }' % text\r | |
1061 | try:\r | |
1062 | value = array_str_to_value(text)\r | |
1063 | if value < min_val or value > max_val:\r | |
1064 | raise Exception('Invalid input!')\r | |
1065 | self.set_config_item_value(item, text)\r | |
1066 | except Exception:\r | |
1067 | pass\r | |
1068 | \r | |
1069 | text = item['value'].strip('{').strip('}').strip()\r | |
1070 | widget.delete(0, tkinter.END)\r | |
1071 | widget.insert(0, text)\r | |
1072 | \r | |
1073 | self.update_widgets_visibility_on_page()\r | |
1074 | \r | |
1075 | def update_page_scroll_bar(self):\r | |
1076 | # Update scrollbar\r | |
1077 | self.frame_right.update()\r | |
1078 | self.conf_canvas.config(scrollregion=self.conf_canvas.bbox("all"))\r | |
1079 | \r | |
1080 | def on_config_page_select_change(self, event):\r | |
1081 | self.update_config_data_on_page()\r | |
1082 | sel = self.left.selection()\r | |
1083 | if len(sel) > 0:\r | |
1084 | page_id = sel[0]\r | |
1085 | self.build_config_data_page(page_id)\r | |
1086 | self.update_widgets_visibility_on_page()\r | |
1087 | self.update_page_scroll_bar()\r | |
1088 | \r | |
1089 | def walk_widgets_in_layout(self, parent, callback_function, args=None):\r | |
1090 | for widget in parent.winfo_children():\r | |
1091 | callback_function(widget, args)\r | |
1092 | \r | |
1093 | def clear_widgets_inLayout(self, parent=None):\r | |
1094 | if parent is None:\r | |
1095 | parent = self.right_grid\r | |
1096 | \r | |
1097 | for widget in parent.winfo_children():\r | |
1098 | widget.destroy()\r | |
1099 | \r | |
1100 | parent.grid_forget()\r | |
1101 | self.conf_list.clear()\r | |
1102 | \r | |
1103 | def build_config_page_tree(self, cfg_page, parent):\r | |
1104 | for page in cfg_page['child']:\r | |
1105 | page_id = next(iter(page))\r | |
1106 | # Put CFG items into related page list\r | |
1107 | self.page_list[page_id] = self.cfg_data_obj.get_cfg_list(page_id)\r | |
1108 | self.page_list[page_id].sort(key=lambda x: x['order'])\r | |
1109 | page_name = self.cfg_data_obj.get_page_title(page_id)\r | |
1110 | child = self.left.insert(\r | |
1111 | parent, 'end',\r | |
1112 | iid=page_id, text=page_name,\r | |
1113 | value=0)\r | |
1114 | if len(page[page_id]) > 0:\r | |
1115 | self.build_config_page_tree(page[page_id], child)\r | |
1116 | \r | |
1117 | def is_config_data_loaded(self):\r | |
1118 | return True if len(self.page_list) else False\r | |
1119 | \r | |
1120 | def set_current_config_page(self, page_id):\r | |
1121 | self.page_id = page_id\r | |
1122 | \r | |
1123 | def get_current_config_page(self):\r | |
1124 | return self.page_id\r | |
1125 | \r | |
1126 | def get_current_config_data(self):\r | |
1127 | page_id = self.get_current_config_page()\r | |
1128 | if page_id in self.page_list:\r | |
1129 | return self.page_list[page_id]\r | |
1130 | else:\r | |
1131 | return []\r | |
1132 | \r | |
1133 | invalid_values = {}\r | |
1134 | \r | |
1135 | def build_config_data_page(self, page_id):\r | |
1136 | self.clear_widgets_inLayout()\r | |
1137 | self.set_current_config_page(page_id)\r | |
1138 | disp_list = []\r | |
1139 | for item in self.get_current_config_data():\r | |
1140 | disp_list.append(item)\r | |
1141 | row = 0\r | |
1142 | disp_list.sort(key=lambda x: x['order'])\r | |
1143 | for item in disp_list:\r | |
1144 | self.add_config_item(item, row)\r | |
1145 | row += 2\r | |
1146 | if self.invalid_values:\r | |
1147 | string = 'The following contails invalid options/values \n\n'\r | |
1148 | for i in self.invalid_values:\r | |
1149 | string += i + ": " + str(self.invalid_values[i]) + "\n"\r | |
1150 | reply = messagebox.showwarning('Warning!', string)\r | |
1151 | if reply == 'ok':\r | |
1152 | self.invalid_values.clear()\r | |
1153 | \r | |
1154 | fsp_version = ''\r | |
1155 | \r | |
1156 | def load_config_data(self, file_name):\r | |
1157 | gen_cfg_data = CGenYamlCfg()\r | |
1158 | if file_name.endswith('.pkl'):\r | |
1159 | with open(file_name, "rb") as pkl_file:\r | |
1160 | gen_cfg_data.__dict__ = marshal.load(pkl_file)\r | |
1161 | gen_cfg_data.prepare_marshal(False)\r | |
1162 | elif file_name.endswith('.yaml'):\r | |
1163 | if gen_cfg_data.load_yaml(file_name) != 0:\r | |
1164 | raise Exception(gen_cfg_data.get_last_error())\r | |
1165 | else:\r | |
1166 | raise Exception('Unsupported file "%s" !' % file_name)\r | |
1167 | # checking fsp version\r | |
1168 | if gen_cfg_data.detect_fsp():\r | |
1169 | self.fsp_version = '2.X'\r | |
1170 | else:\r | |
1171 | self.fsp_version = '1.X'\r | |
cac83b6f | 1172 | \r |
580b1120 LTL |
1173 | return gen_cfg_data\r |
1174 | \r | |
1175 | def about(self):\r | |
1176 | msg = 'Configuration Editor\n--------------------------------\n \\r | |
1177 | Version 0.8\n2021'\r | |
1178 | lines = msg.split('\n')\r | |
1179 | width = 30\r | |
1180 | text = []\r | |
1181 | for line in lines:\r | |
1182 | text.append(line.center(width, ' '))\r | |
1183 | messagebox.showinfo('Config Editor', '\n'.join(text))\r | |
1184 | \r | |
1185 | def update_last_dir(self, path):\r | |
1186 | self.last_dir = os.path.dirname(path)\r | |
1187 | \r | |
1188 | def get_open_file_name(self, ftype):\r | |
1189 | if self.is_config_data_loaded():\r | |
1190 | if ftype == 'dlt':\r | |
1191 | question = ''\r | |
1192 | elif ftype == 'bin':\r | |
1193 | question = 'All configuration will be reloaded from BIN file, \\r | |
1194 | continue ?'\r | |
1195 | elif ftype == 'yaml':\r | |
1196 | question = ''\r | |
1197 | elif ftype == 'bsf':\r | |
1198 | question = ''\r | |
1199 | else:\r | |
1200 | raise Exception('Unsupported file type !')\r | |
1201 | if question:\r | |
1202 | reply = messagebox.askquestion('', question, icon='warning')\r | |
1203 | if reply == 'no':\r | |
1204 | return None\r | |
1205 | \r | |
1206 | if ftype == 'yaml':\r | |
1207 | if self.mode == 'FSP':\r | |
1208 | file_type = 'YAML'\r | |
1209 | file_ext = 'yaml'\r | |
1210 | else:\r | |
1211 | file_type = 'YAML or PKL'\r | |
1212 | file_ext = 'pkl *.yaml'\r | |
1213 | else:\r | |
1214 | file_type = ftype.upper()\r | |
1215 | file_ext = ftype\r | |
1216 | \r | |
1217 | path = filedialog.askopenfilename(\r | |
1218 | initialdir=self.last_dir,\r | |
1219 | title="Load file",\r | |
1220 | filetypes=(("%s files" % file_type, "*.%s" % file_ext), (\r | |
1221 | "all files", "*.*")))\r | |
1222 | if path:\r | |
1223 | self.update_last_dir(path)\r | |
1224 | return path\r | |
1225 | else:\r | |
1226 | return None\r | |
1227 | \r | |
1228 | def load_from_delta(self):\r | |
1229 | path = self.get_open_file_name('dlt')\r | |
1230 | if not path:\r | |
1231 | return\r | |
1232 | self.load_delta_file(path)\r | |
1233 | \r | |
1234 | def load_delta_file(self, path):\r | |
1235 | self.reload_config_data_from_bin(self.org_cfg_data_bin)\r | |
1236 | try:\r | |
1237 | self.cfg_data_obj.override_default_value(path)\r | |
1238 | except Exception as e:\r | |
1239 | messagebox.showerror('LOADING ERROR', str(e))\r | |
1240 | return\r | |
1241 | self.update_last_dir(path)\r | |
1242 | self.refresh_config_data_page()\r | |
1243 | \r | |
1244 | def load_from_bin(self):\r | |
1245 | path = filedialog.askopenfilename(\r | |
1246 | initialdir=self.last_dir,\r | |
1247 | title="Load file",\r | |
1248 | filetypes={("Binaries", "*.fv *.fd *.bin *.rom")})\r | |
1249 | if not path:\r | |
1250 | return\r | |
1251 | self.load_bin_file(path)\r | |
1252 | \r | |
1253 | def load_bin_file(self, path):\r | |
1254 | with open(path, 'rb') as fd:\r | |
1255 | bin_data = bytearray(fd.read())\r | |
1256 | if len(bin_data) < len(self.org_cfg_data_bin):\r | |
1257 | messagebox.showerror('Binary file size is smaller than what \\r | |
1258 | YAML requires !')\r | |
1259 | return\r | |
1260 | \r | |
1261 | try:\r | |
1262 | self.reload_config_data_from_bin(bin_data)\r | |
1263 | except Exception as e:\r | |
1264 | messagebox.showerror('LOADING ERROR', str(e))\r | |
1265 | return\r | |
1266 | \r | |
1267 | def load_from_bsf_file(self):\r | |
1268 | path = self.get_open_file_name('bsf')\r | |
1269 | if not path:\r | |
1270 | return\r | |
1271 | self.load_bsf_file(path)\r | |
1272 | \r | |
1273 | def load_bsf_file(self, path):\r | |
1274 | bsf_file = path\r | |
1275 | dsc_file = os.path.splitext(bsf_file)[0] + '.dsc'\r | |
1276 | yaml_file = os.path.splitext(bsf_file)[0] + '.yaml'\r | |
1277 | bsf_to_dsc(bsf_file, dsc_file)\r | |
1278 | dsc_to_yaml(dsc_file, yaml_file)\r | |
1279 | \r | |
1280 | self.load_cfg_file(yaml_file)\r | |
1281 | return\r | |
1282 | \r | |
1283 | def load_from_fd(self):\r | |
1284 | path = filedialog.askopenfilename(\r | |
1285 | initialdir=self.last_dir,\r | |
1286 | title="Load file",\r | |
1287 | filetypes={("Binaries", "*.fv *.fd *.bin *.rom")})\r | |
1288 | if not path:\r | |
1289 | return\r | |
1290 | self.load_fd_file(path)\r | |
1291 | \r | |
1292 | def load_fd_file(self, path):\r | |
1293 | with open(path, 'rb') as fd:\r | |
1294 | bin_data = bytearray(fd.read())\r | |
1295 | \r | |
1296 | fd = FirmwareDevice(0, bin_data)\r | |
1297 | fd.ParseFd()\r | |
1298 | fd.ParseFsp()\r | |
1299 | fd.OutputFsp()\r | |
1300 | \r | |
1301 | def load_cfg_file(self, path):\r | |
1302 | # Save current values in widget and clear database\r | |
1303 | self.clear_widgets_inLayout()\r | |
1304 | self.left.delete(*self.left.get_children())\r | |
1305 | \r | |
1306 | self.cfg_data_obj = self.load_config_data(path)\r | |
1307 | \r | |
1308 | self.update_last_dir(path)\r | |
1309 | self.org_cfg_data_bin = self.cfg_data_obj.generate_binary_array()\r | |
1310 | self.build_config_page_tree(self.cfg_data_obj.get_cfg_page()['root'],\r | |
1311 | '')\r | |
1312 | \r | |
1313 | msg_string = 'Click YES if it is FULL FSP '\\r | |
1314 | + self.fsp_version + ' Binary'\r | |
1315 | reply = messagebox.askquestion('Form', msg_string)\r | |
1316 | if reply == 'yes':\r | |
1317 | self.load_from_bin()\r | |
1318 | \r | |
1319 | for menu in self.menu_string:\r | |
1320 | self.file_menu.entryconfig(menu, state="normal")\r | |
1321 | \r | |
1322 | return 0\r | |
1323 | \r | |
1324 | def load_from_yaml(self):\r | |
1325 | path = self.get_open_file_name('yaml')\r | |
1326 | if not path:\r | |
1327 | return\r | |
1328 | \r | |
1329 | self.load_cfg_file(path)\r | |
1330 | \r | |
1331 | def get_save_file_name(self, extension):\r | |
1332 | path = filedialog.asksaveasfilename(\r | |
1333 | initialdir=self.last_dir,\r | |
1334 | title="Save file",\r | |
1335 | defaultextension=extension)\r | |
1336 | if path:\r | |
1337 | self.last_dir = os.path.dirname(path)\r | |
1338 | return path\r | |
1339 | else:\r | |
1340 | return None\r | |
1341 | \r | |
1342 | def save_delta_file(self, full=False):\r | |
1343 | path = self.get_save_file_name(".dlt")\r | |
1344 | if not path:\r | |
1345 | return\r | |
1346 | \r | |
1347 | self.update_config_data_on_page()\r | |
1348 | new_data = self.cfg_data_obj.generate_binary_array()\r | |
1349 | self.cfg_data_obj.generate_delta_file_from_bin(path,\r | |
1350 | self.org_cfg_data_bin,\r | |
1351 | new_data, full)\r | |
1352 | \r | |
1353 | def save_to_delta(self):\r | |
1354 | self.save_delta_file()\r | |
1355 | \r | |
1356 | def save_full_to_delta(self):\r | |
1357 | self.save_delta_file(True)\r | |
1358 | \r | |
1359 | def save_to_bin(self):\r | |
1360 | path = self.get_save_file_name(".bin")\r | |
1361 | if not path:\r | |
1362 | return\r | |
1363 | \r | |
1364 | self.update_config_data_on_page()\r | |
1365 | bins = self.cfg_data_obj.save_current_to_bin()\r | |
1366 | \r | |
1367 | with open(path, 'wb') as fd:\r | |
1368 | fd.write(bins)\r | |
1369 | \r | |
1370 | def refresh_config_data_page(self):\r | |
1371 | self.clear_widgets_inLayout()\r | |
1372 | self.on_config_page_select_change(None)\r | |
1373 | \r | |
1374 | def reload_config_data_from_bin(self, bin_dat):\r | |
1375 | self.cfg_data_obj.load_default_from_bin(bin_dat)\r | |
1376 | self.refresh_config_data_page()\r | |
1377 | \r | |
1378 | def set_config_item_value(self, item, value_str):\r | |
1379 | itype = item['type'].split(',')[0]\r | |
1380 | if itype == "Table":\r | |
1381 | new_value = value_str\r | |
1382 | elif itype == "EditText":\r | |
1383 | length = (self.cfg_data_obj.get_cfg_item_length(item) + 7) // 8\r | |
1384 | new_value = value_str[:length]\r | |
1385 | if item['value'].startswith("'"):\r | |
1386 | new_value = "'%s'" % new_value\r | |
1387 | else:\r | |
1388 | try:\r | |
1389 | new_value = self.cfg_data_obj.reformat_value_str(\r | |
1390 | value_str,\r | |
1391 | self.cfg_data_obj.get_cfg_item_length(item),\r | |
1392 | item['value'])\r | |
1393 | except Exception:\r | |
1394 | print("WARNING: Failed to format value string '%s' for '%s' !"\r | |
1395 | % (value_str, item['path']))\r | |
1396 | new_value = item['value']\r | |
1397 | \r | |
1398 | if item['value'] != new_value:\r | |
1399 | if self.debug:\r | |
1400 | print('Update %s from %s to %s !'\r | |
1401 | % (item['cname'], item['value'], new_value))\r | |
1402 | item['value'] = new_value\r | |
1403 | \r | |
1404 | def get_config_data_item_from_widget(self, widget, label=False):\r | |
1405 | name = self.get_object_name(widget)\r | |
1406 | if not name or not len(self.page_list):\r | |
1407 | return None\r | |
1408 | \r | |
1409 | if name.startswith('LABEL_'):\r | |
1410 | if label:\r | |
1411 | path = name[6:]\r | |
1412 | else:\r | |
1413 | return None\r | |
1414 | else:\r | |
1415 | path = name\r | |
cac83b6f | 1416 | \r |
580b1120 LTL |
1417 | item = self.cfg_data_obj.get_item_by_path(path)\r |
1418 | return item\r | |
1419 | \r | |
1420 | def update_config_data_from_widget(self, widget, args):\r | |
1421 | item = self.get_config_data_item_from_widget(widget)\r | |
1422 | if item is None:\r | |
1423 | return\r | |
1424 | elif not item:\r | |
1425 | if isinstance(widget, tkinter.Label):\r | |
1426 | return\r | |
1427 | raise Exception('Failed to find "%s" !' %\r | |
1428 | self.get_object_name(widget))\r | |
1429 | \r | |
1430 | itype = item['type'].split(',')[0]\r | |
1431 | if itype == "Combo":\r | |
1432 | opt_list = self.cfg_data_obj.get_cfg_item_options(item)\r | |
1433 | tmp_list = [opt[0] for opt in opt_list]\r | |
1434 | idx = widget.current()\r | |
1435 | if idx != -1:\r | |
1436 | self.set_config_item_value(item, tmp_list[idx])\r | |
1437 | elif itype in ["EditNum", "EditText"]:\r | |
1438 | self.set_config_item_value(item, widget.get())\r | |
1439 | elif itype in ["Table"]:\r | |
1440 | new_value = bytes_to_bracket_str(widget.get())\r | |
1441 | self.set_config_item_value(item, new_value)\r | |
1442 | \r | |
1443 | def evaluate_condition(self, item):\r | |
1444 | try:\r | |
1445 | result = self.cfg_data_obj.evaluate_condition(item)\r | |
1446 | except Exception:\r | |
1447 | print("WARNING: Condition '%s' is invalid for '%s' !"\r | |
1448 | % (item['condition'], item['path']))\r | |
1449 | result = 1\r | |
1450 | return result\r | |
1451 | \r | |
1452 | def add_config_item(self, item, row):\r | |
1453 | parent = self.right_grid\r | |
1454 | \r | |
1455 | name = tkinter.Label(parent, text=item['name'], anchor="w")\r | |
1456 | \r | |
1457 | parts = item['type'].split(',')\r | |
1458 | itype = parts[0]\r | |
1459 | widget = None\r | |
1460 | \r | |
1461 | if itype == "Combo":\r | |
1462 | # Build\r | |
1463 | opt_list = self.cfg_data_obj.get_cfg_item_options(item)\r | |
1464 | current_value = self.cfg_data_obj.get_cfg_item_value(item, False)\r | |
1465 | option_list = []\r | |
1466 | current = None\r | |
1467 | \r | |
1468 | for idx, option in enumerate(opt_list):\r | |
1469 | option_str = option[0]\r | |
1470 | try:\r | |
1471 | option_value = self.cfg_data_obj.get_value(\r | |
1472 | option_str,\r | |
1473 | len(option_str), False)\r | |
1474 | except Exception:\r | |
1475 | option_value = 0\r | |
1476 | print('WARNING: Option "%s" has invalid format for "%s" !'\r | |
1477 | % (option_str, item['path']))\r | |
1478 | if option_value == current_value:\r | |
1479 | current = idx\r | |
1480 | option_list.append(option[1])\r | |
1481 | \r | |
1482 | widget = ttk.Combobox(parent, value=option_list, state="readonly")\r | |
1483 | widget.bind("<<ComboboxSelected>>", self.combo_select_changed)\r | |
1484 | widget.unbind_class("TCombobox", "<MouseWheel>")\r | |
1485 | \r | |
1486 | if current is None:\r | |
1487 | print('WARNING: Value "%s" is an invalid option for "%s" !' %\r | |
1488 | (current_value, item['path']))\r | |
1489 | self.invalid_values[item['path']] = current_value\r | |
1490 | else:\r | |
1491 | widget.current(current)\r | |
1492 | \r | |
1493 | elif itype in ["EditNum", "EditText"]:\r | |
1494 | txt_val = tkinter.StringVar()\r | |
1495 | widget = tkinter.Entry(parent, textvariable=txt_val)\r | |
1496 | value = item['value'].strip("'")\r | |
1497 | if itype in ["EditText"]:\r | |
1498 | txt_val.trace(\r | |
1499 | 'w',\r | |
1500 | lambda *args: self.limit_entry_size\r | |
1501 | (txt_val, (self.cfg_data_obj.get_cfg_item_length(item)\r | |
1502 | + 7) // 8))\r | |
1503 | elif itype in ["EditNum"]:\r | |
1504 | value = item['value'].strip("{").strip("}").strip()\r | |
1505 | widget.bind("<FocusOut>", self.edit_num_finished)\r | |
1506 | txt_val.set(value)\r | |
1507 | \r | |
1508 | elif itype in ["Table"]:\r | |
1509 | bins = self.cfg_data_obj.get_cfg_item_value(item, True)\r | |
1510 | col_hdr = item['option'].split(',')\r | |
1511 | widget = custom_table(parent, col_hdr, bins)\r | |
1512 | \r | |
1513 | else:\r | |
1514 | if itype and itype not in ["Reserved"]:\r | |
1515 | print("WARNING: Type '%s' is invalid for '%s' !" %\r | |
1516 | (itype, item['path']))\r | |
1517 | self.invalid_values[item['path']] = itype\r | |
1518 | \r | |
1519 | if widget:\r | |
1520 | create_tool_tip(widget, item['help'])\r | |
1521 | self.set_object_name(name, 'LABEL_' + item['path'])\r | |
1522 | self.set_object_name(widget, item['path'])\r | |
1523 | name.grid(row=row, column=0, padx=10, pady=5, sticky="nsew")\r | |
1524 | widget.grid(row=row + 1, rowspan=1, column=0,\r | |
1525 | padx=10, pady=5, sticky="nsew")\r | |
1526 | \r | |
1527 | def update_config_data_on_page(self):\r | |
1528 | self.walk_widgets_in_layout(self.right_grid,\r | |
1529 | self.update_config_data_from_widget)\r | |
1530 | \r | |
1531 | \r | |
1532 | if __name__ == '__main__':\r | |
1533 | root = tkinter.Tk()\r | |
1534 | app = application(master=root)\r | |
1535 | root.title("Config Editor")\r | |
1536 | root.mainloop()\r |