kspl 1.1.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kspl/__init__.py +1 -1
- kspl/_run.py +13 -9
- kspl/config_slurper.py +71 -85
- kspl/edit.py +71 -84
- kspl/generate.py +22 -43
- kspl/gui.py +204 -79
- kspl/kconfig.py +228 -242
- kspl/main.py +35 -39
- kspl-1.2.0.dist-info/METADATA +99 -0
- kspl-1.2.0.dist-info/RECORD +14 -0
- {kspl-1.1.0.dist-info → kspl-1.2.0.dist-info}/WHEEL +1 -1
- kspl-1.1.0.dist-info/METADATA +0 -127
- kspl-1.1.0.dist-info/RECORD +0 -14
- {kspl-1.1.0.dist-info → kspl-1.2.0.dist-info}/LICENSE +0 -0
- {kspl-1.1.0.dist-info → kspl-1.2.0.dist-info}/entry_points.txt +0 -0
kspl/gui.py
CHANGED
@@ -4,8 +4,8 @@ from argparse import ArgumentParser, Namespace
|
|
4
4
|
from dataclasses import dataclass, field
|
5
5
|
from enum import auto
|
6
6
|
from pathlib import Path
|
7
|
-
from tkinter import simpledialog, ttk
|
8
|
-
from typing import Any
|
7
|
+
from tkinter import font, simpledialog, ttk
|
8
|
+
from typing import Any
|
9
9
|
|
10
10
|
import customtkinter
|
11
11
|
from mashumaro import DataClassDictMixin
|
@@ -40,56 +40,79 @@ class MainView(CTkView):
|
|
40
40
|
def __init__(
|
41
41
|
self,
|
42
42
|
event_manager: EventManager,
|
43
|
-
elements:
|
44
|
-
variants:
|
43
|
+
elements: list[EditableConfigElement],
|
44
|
+
variants: list[VariantViewData],
|
45
45
|
) -> None:
|
46
46
|
self.event_manager = event_manager
|
47
47
|
self.elements = elements
|
48
48
|
self.elements_dict = {elem.name: elem for elem in elements}
|
49
49
|
self.variants = variants
|
50
|
+
self.all_columns = [v.name for v in self.variants]
|
51
|
+
self.visible_columns = list(self.all_columns)
|
50
52
|
|
51
53
|
self.logger = logger.bind()
|
52
|
-
self.edit_event_data:
|
53
|
-
self.trigger_edit_event = self.event_manager.create_event_trigger(
|
54
|
-
KSplEvents.EDIT
|
55
|
-
)
|
54
|
+
self.edit_event_data: EditEventData | None = None
|
55
|
+
self.trigger_edit_event = self.event_manager.create_event_trigger(KSplEvents.EDIT)
|
56
56
|
self.root = customtkinter.CTk()
|
57
57
|
|
58
58
|
# Configure the main window
|
59
59
|
self.root.title("K-SPL")
|
60
60
|
self.root.geometry(f"{1080}x{580}")
|
61
61
|
|
62
|
+
# Frame for controls
|
63
|
+
control_frame = customtkinter.CTkFrame(self.root)
|
64
|
+
control_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
|
65
|
+
|
66
|
+
self.column_select_button = customtkinter.CTkButton(
|
67
|
+
master=control_frame,
|
68
|
+
text="Select variants",
|
69
|
+
command=self.open_column_selection_dialog,
|
70
|
+
)
|
71
|
+
self.column_select_button.pack(side="left", padx=5)
|
72
|
+
|
62
73
|
# ========================================================
|
63
|
-
# create
|
64
|
-
|
65
|
-
self.tree = self.create_tree_view(
|
74
|
+
# create main content frame
|
75
|
+
main_frame = customtkinter.CTkFrame(self.root)
|
76
|
+
self.tree = self.create_tree_view(main_frame)
|
66
77
|
self.tree["columns"] = tuple(variant.name for variant in self.variants)
|
78
|
+
self.tree["displaycolumns"] = self.visible_columns
|
67
79
|
self.tree.heading("#0", text="Configuration")
|
80
|
+
self.header_texts: dict[str, str] = {}
|
68
81
|
for variant in self.variants:
|
69
82
|
self.tree.heading(variant.name, text=variant.name)
|
83
|
+
self.header_texts[variant.name] = variant.name
|
70
84
|
# Keep track of the mapping between the tree view items and the config elements
|
71
85
|
self.tree_view_items_mapping = self.populate_tree_view()
|
72
|
-
self.
|
86
|
+
self.adjust_column_width()
|
87
|
+
self.selected_column_id: str | None = None
|
88
|
+
self.tree.bind("<Button-1>", self.on_tree_click)
|
73
89
|
# TODO: make the tree view editable
|
74
90
|
# self.tree.bind("<Double-1>", self.double_click_handler)
|
75
91
|
|
76
92
|
# ========================================================
|
77
93
|
# put all together
|
78
94
|
self.root.grid_columnconfigure(0, weight=1)
|
79
|
-
self.root.grid_rowconfigure(0, weight=
|
80
|
-
|
95
|
+
self.root.grid_rowconfigure(0, weight=0)
|
96
|
+
self.root.grid_rowconfigure(1, weight=1)
|
97
|
+
main_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 10))
|
81
98
|
|
82
99
|
def mainloop(self) -> None:
|
83
100
|
self.root.mainloop()
|
84
101
|
|
85
102
|
def create_tree_view(self, frame: customtkinter.CTkFrame) -> ttk.Treeview:
|
86
|
-
frame.grid_rowconfigure(0, weight=
|
87
|
-
frame.grid_rowconfigure(1, weight=1)
|
103
|
+
frame.grid_rowconfigure(0, weight=1)
|
88
104
|
frame.grid_columnconfigure(0, weight=1)
|
89
105
|
|
90
106
|
columns = [var.name for var in self.variants]
|
91
107
|
|
92
108
|
style = ttk.Style()
|
109
|
+
# From: https://stackoverflow.com/a/56684731
|
110
|
+
# This gives the selection a transparent look
|
111
|
+
style.map(
|
112
|
+
"mystyle.Treeview",
|
113
|
+
background=[("selected", "#a6d5f7")],
|
114
|
+
foreground=[("selected", "black")],
|
115
|
+
)
|
93
116
|
style.configure(
|
94
117
|
"mystyle.Treeview",
|
95
118
|
highlightthickness=0,
|
@@ -97,9 +120,26 @@ class MainView(CTkView):
|
|
97
120
|
font=("Calibri", 14),
|
98
121
|
rowheight=30,
|
99
122
|
) # Modify the font of the body
|
100
|
-
style.configure(
|
101
|
-
|
102
|
-
|
123
|
+
style.configure("mystyle.Treeview.Heading", font=("Calibri", 14, "bold")) # Modify the font of the headings
|
124
|
+
|
125
|
+
# Add a separator to the right of the heading
|
126
|
+
MainView.vline_img = tkinter.PhotoImage("vline", data="R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=")
|
127
|
+
style.element_create("vline", "image", "vline")
|
128
|
+
style.layout(
|
129
|
+
"mystyle.Treeview.Heading",
|
130
|
+
[
|
131
|
+
(
|
132
|
+
"mystyle.Treeview.heading.cell",
|
133
|
+
{
|
134
|
+
"sticky": "nswe",
|
135
|
+
"children": [
|
136
|
+
("mystyle.Treeview.heading.text", {"sticky": "we"}),
|
137
|
+
("vline", {"side": "right", "sticky": "ns"}),
|
138
|
+
],
|
139
|
+
},
|
140
|
+
)
|
141
|
+
],
|
142
|
+
)
|
103
143
|
|
104
144
|
# create a Treeview widget
|
105
145
|
config_treeview = ttk.Treeview(
|
@@ -108,17 +148,25 @@ class MainView(CTkView):
|
|
108
148
|
show="tree headings",
|
109
149
|
style="mystyle.Treeview",
|
110
150
|
)
|
111
|
-
|
151
|
+
|
152
|
+
scrollbar_y = ttk.Scrollbar(frame, command=config_treeview.yview)
|
153
|
+
scrollbar_x = ttk.Scrollbar(frame, command=config_treeview.xview, orient=tkinter.HORIZONTAL)
|
154
|
+
config_treeview.config(xscrollcommand=scrollbar_x.set, yscrollcommand=scrollbar_y.set)
|
155
|
+
scrollbar_y.pack(fill=tkinter.Y, side=tkinter.RIGHT)
|
156
|
+
scrollbar_x.pack(fill=tkinter.X, side=tkinter.BOTTOM)
|
157
|
+
config_treeview.pack(fill=tkinter.BOTH, expand=True)
|
158
|
+
|
112
159
|
return config_treeview
|
113
160
|
|
114
|
-
def populate_tree_view(self) ->
|
161
|
+
def populate_tree_view(self) -> dict[str, str]:
|
115
162
|
"""
|
116
163
|
Populates the tree view with the configuration elements.
|
164
|
+
|
117
165
|
:return: a mapping between the tree view items and the configuration elements
|
118
166
|
"""
|
119
167
|
stack = [] # To keep track of the parent items
|
120
168
|
last_level = -1
|
121
|
-
mapping:
|
169
|
+
mapping: dict[str, str] = {}
|
122
170
|
|
123
171
|
for element in self.elements:
|
124
172
|
values = self.collect_values_for_element(element)
|
@@ -128,45 +176,28 @@ class MainView(CTkView):
|
|
128
176
|
stack = [item_id] # Reset the stack with the root item
|
129
177
|
elif element.level > last_level:
|
130
178
|
# Insert as a child of the last inserted item
|
131
|
-
item_id = self.tree.insert(
|
132
|
-
stack[-1], "end", text=element.name, values=values
|
133
|
-
)
|
179
|
+
item_id = self.tree.insert(stack[-1], "end", text=element.name, values=values)
|
134
180
|
stack.append(item_id)
|
135
181
|
elif element.level == last_level:
|
136
182
|
# Insert at the same level as the last item
|
137
|
-
item_id = self.tree.insert(
|
138
|
-
stack[-2], "end", text=element.name, values=values
|
139
|
-
)
|
183
|
+
item_id = self.tree.insert(stack[-2], "end", text=element.name, values=values)
|
140
184
|
stack[-1] = item_id # Replace the top item in the stack
|
141
185
|
else:
|
142
186
|
# Go up in the hierarchy and insert at the appropriate level
|
143
|
-
item_id = self.tree.insert(
|
144
|
-
|
145
|
-
)
|
146
|
-
stack = stack[: element.level] + [item_id]
|
187
|
+
item_id = self.tree.insert(stack[element.level - 1], "end", text=element.name, values=values)
|
188
|
+
stack = [*stack[: element.level], item_id]
|
147
189
|
|
148
190
|
last_level = element.level
|
149
191
|
mapping[item_id] = element.name
|
150
192
|
return mapping
|
151
193
|
|
152
|
-
def collect_values_for_element(
|
153
|
-
self, element
|
154
|
-
) -> List[int | str]:
|
155
|
-
return (
|
156
|
-
[
|
157
|
-
self.prepare_value_to_be_displayed(
|
158
|
-
element.type, variant.config_dict.get(element.name, None)
|
159
|
-
)
|
160
|
-
for variant in self.variants
|
161
|
-
]
|
162
|
-
if not element.is_menu
|
163
|
-
else []
|
164
|
-
)
|
194
|
+
def collect_values_for_element(self, element: EditableConfigElement) -> list[int | str]:
|
195
|
+
return [self.prepare_value_to_be_displayed(element.type, variant.config_dict.get(element.name, None)) for variant in self.variants] if not element.is_menu else []
|
165
196
|
|
166
|
-
def prepare_value_to_be_displayed(
|
167
|
-
self, element_type: ConfigElementType, value: Any
|
168
|
-
) -> str:
|
197
|
+
def prepare_value_to_be_displayed(self, element_type: ConfigElementType, value: Any) -> str:
|
169
198
|
"""
|
199
|
+
Prepare the value to be displayed in the tree view based on the element type.
|
200
|
+
|
170
201
|
UNKNOWN - N/A
|
171
202
|
BOOL - ✅ ⛔
|
172
203
|
TRISTATE - str
|
@@ -182,7 +213,62 @@ class MainView(CTkView):
|
|
182
213
|
else:
|
183
214
|
return str(value)
|
184
215
|
|
185
|
-
def
|
216
|
+
def adjust_column_width(self) -> None:
|
217
|
+
"""Adjust the column widths to fit the header text, preserving manual resizing."""
|
218
|
+
heading_font = font.Font(font=("Calibri", 14, "bold"))
|
219
|
+
padding = 60
|
220
|
+
for col in self.tree["columns"]:
|
221
|
+
text = self.tree.heading(col, "text")
|
222
|
+
min_width = heading_font.measure(text) + padding
|
223
|
+
# Get current width to preserve manual resizing
|
224
|
+
current_width = self.tree.column(col, "width")
|
225
|
+
# Use the larger of current width or minimum required width
|
226
|
+
final_width = max(current_width, min_width)
|
227
|
+
self.tree.column(col, minwidth=min_width, width=final_width, stretch=False)
|
228
|
+
# First column (#0)
|
229
|
+
text = self.tree.heading("#0", "text")
|
230
|
+
min_width = heading_font.measure(text) + padding
|
231
|
+
current_width = self.tree.column("#0", "width")
|
232
|
+
final_width = max(current_width, min_width)
|
233
|
+
self.tree.column("#0", minwidth=min_width, width=final_width, stretch=False)
|
234
|
+
|
235
|
+
def on_tree_click(self, event: Any) -> None:
|
236
|
+
"""Handle click events on the treeview to highlight the column header."""
|
237
|
+
column_id_str = self.tree.identify_column(event.x)
|
238
|
+
if not column_id_str or column_id_str == "#0":
|
239
|
+
# Click was on the tree part or outside columns, so reset if anything was selected
|
240
|
+
if self.selected_column_id:
|
241
|
+
original_text = self.header_texts.get(self.selected_column_id)
|
242
|
+
if original_text:
|
243
|
+
self.tree.heading(self.selected_column_id, text=original_text)
|
244
|
+
self.selected_column_id = None
|
245
|
+
return
|
246
|
+
|
247
|
+
col_idx = int(column_id_str.replace("#", "")) - 1
|
248
|
+
if col_idx < 0:
|
249
|
+
return
|
250
|
+
# Use displaycolumns instead of columns to account for hidden columns
|
251
|
+
visible_columns = self.tree["displaycolumns"]
|
252
|
+
if col_idx >= len(visible_columns):
|
253
|
+
return
|
254
|
+
column_name = visible_columns[col_idx]
|
255
|
+
|
256
|
+
if column_name == self.selected_column_id:
|
257
|
+
return
|
258
|
+
|
259
|
+
# Reset the previously selected column header
|
260
|
+
if self.selected_column_id:
|
261
|
+
original_text = self.header_texts.get(self.selected_column_id)
|
262
|
+
if original_text:
|
263
|
+
self.tree.heading(self.selected_column_id, text=original_text)
|
264
|
+
|
265
|
+
# Highlight the new column header
|
266
|
+
original_text = self.header_texts.get(column_name)
|
267
|
+
if original_text:
|
268
|
+
self.tree.heading(column_name, text=f"✅{original_text}")
|
269
|
+
self.selected_column_id = column_name
|
270
|
+
|
271
|
+
def double_click_handler(self, event: Any) -> None:
|
186
272
|
current_selection = self.tree.selection()
|
187
273
|
if not current_selection:
|
188
274
|
return
|
@@ -191,9 +277,7 @@ class MainView(CTkView):
|
|
191
277
|
selected_element_name = self.tree_view_items_mapping[selected_item]
|
192
278
|
|
193
279
|
variant_idx_str = self.tree.identify_column(event.x) # Get the clicked column
|
194
|
-
variant_idx = (
|
195
|
-
int(variant_idx_str.split("#")[-1]) - 1
|
196
|
-
) # Convert to 0-based index
|
280
|
+
variant_idx = int(variant_idx_str.split("#")[-1]) - 1 # Convert to 0-based index
|
197
281
|
|
198
282
|
if variant_idx < 0 or variant_idx >= len(self.variants):
|
199
283
|
return
|
@@ -207,9 +291,7 @@ class MainView(CTkView):
|
|
207
291
|
new_value: Any = None
|
208
292
|
if selected_element.type == ConfigElementType.BOOL:
|
209
293
|
# Toggle the boolean value
|
210
|
-
new_value =
|
211
|
-
TriState.N if selected_element_value == TriState.Y else TriState.Y
|
212
|
-
)
|
294
|
+
new_value = TriState.N if selected_element_value == TriState.Y else TriState.Y
|
213
295
|
elif selected_element.type == ConfigElementType.INT:
|
214
296
|
tmp_int_value = simpledialog.askinteger(
|
215
297
|
"Enter new value",
|
@@ -231,21 +313,76 @@ class MainView(CTkView):
|
|
231
313
|
# Check if the value has changed
|
232
314
|
if new_value:
|
233
315
|
# Trigger the EDIT event
|
234
|
-
self.create_edit_event_trigger(
|
235
|
-
selected_variant, selected_element_name, new_value
|
236
|
-
)
|
316
|
+
self.create_edit_event_trigger(selected_variant, selected_element_name, new_value)
|
237
317
|
|
238
|
-
def create_edit_event_trigger(
|
239
|
-
self, variant: VariantViewData, element_name: str, new_value: Any
|
240
|
-
) -> None:
|
318
|
+
def create_edit_event_trigger(self, variant: VariantViewData, element_name: str, new_value: Any) -> None:
|
241
319
|
self.edit_event_data = EditEventData(variant, element_name, new_value)
|
242
320
|
self.trigger_edit_event()
|
243
321
|
|
244
|
-
def pop_edit_event_data(self) ->
|
322
|
+
def pop_edit_event_data(self) -> EditEventData | None:
|
245
323
|
result = self.edit_event_data
|
246
324
|
self.edit_event_data = None
|
247
325
|
return result
|
248
326
|
|
327
|
+
def update_visible_columns(self) -> None:
|
328
|
+
"""Update the visible columns based on the state of the checkboxes."""
|
329
|
+
self.visible_columns = [col_name for col_name, var in self.column_vars.items() if var.get()]
|
330
|
+
self.tree["displaycolumns"] = self.visible_columns
|
331
|
+
self.adjust_column_width()
|
332
|
+
|
333
|
+
def open_column_selection_dialog(self) -> None:
|
334
|
+
"""Open a dialog to select which columns to display."""
|
335
|
+
# Create a new top-level window
|
336
|
+
dialog = customtkinter.CTkToplevel(self.root)
|
337
|
+
dialog.title("Select variants")
|
338
|
+
dialog.geometry("400x300")
|
339
|
+
|
340
|
+
# Create a frame for the checkboxes
|
341
|
+
checkbox_frame = customtkinter.CTkFrame(dialog)
|
342
|
+
checkbox_frame.pack(padx=10, pady=10, fill="both", expand=True)
|
343
|
+
|
344
|
+
# Create a variable for each column
|
345
|
+
self.column_vars = {}
|
346
|
+
for column_name in self.all_columns:
|
347
|
+
# Set the initial value based on whether the column is currently visible
|
348
|
+
is_visible = column_name in self.visible_columns
|
349
|
+
var = tkinter.BooleanVar(value=is_visible)
|
350
|
+
checkbox = customtkinter.CTkCheckBox(
|
351
|
+
master=checkbox_frame,
|
352
|
+
text=column_name,
|
353
|
+
command=self.update_visible_columns,
|
354
|
+
variable=var,
|
355
|
+
)
|
356
|
+
checkbox.pack(anchor="w", padx=5, pady=2)
|
357
|
+
self.column_vars[column_name] = var
|
358
|
+
|
359
|
+
# Add OK and Cancel buttons
|
360
|
+
button_frame = customtkinter.CTkFrame(dialog)
|
361
|
+
button_frame.pack(padx=10, pady=10)
|
362
|
+
|
363
|
+
ok_button = customtkinter.CTkButton(
|
364
|
+
master=button_frame,
|
365
|
+
text="OK",
|
366
|
+
command=dialog.destroy,
|
367
|
+
)
|
368
|
+
ok_button.pack(side="right", padx=5)
|
369
|
+
|
370
|
+
cancel_button = customtkinter.CTkButton(
|
371
|
+
master=button_frame,
|
372
|
+
text="Cancel",
|
373
|
+
command=dialog.destroy,
|
374
|
+
)
|
375
|
+
cancel_button.pack(side="right", padx=5)
|
376
|
+
|
377
|
+
# Center the dialog on the screen
|
378
|
+
dialog.update_idletasks()
|
379
|
+
x = (self.root.winfo_width() - dialog.winfo_width()) // 2
|
380
|
+
y = (self.root.winfo_height() - dialog.winfo_height()) // 2
|
381
|
+
dialog.geometry(f"+{self.root.winfo_x() + x}+{self.root.winfo_y() + y}")
|
382
|
+
|
383
|
+
dialog.transient(self.root) # Keep the dialog above the main window
|
384
|
+
dialog.grab_set() # Make the dialog modal
|
385
|
+
|
249
386
|
|
250
387
|
class KSPL(Presenter):
|
251
388
|
def __init__(self, event_manager: EventManager, project_dir: Path) -> None:
|
@@ -264,23 +401,14 @@ class KSPL(Presenter):
|
|
264
401
|
if edit_event_data is None:
|
265
402
|
self.logger.error("Edit event received but event data is missing!")
|
266
403
|
else:
|
267
|
-
self.logger.debug(
|
268
|
-
"Edit event received: "
|
269
|
-
f"'{edit_event_data.variant.name}:{edit_event_data.config_element_name} = {edit_event_data.new_value}'"
|
270
|
-
)
|
404
|
+
self.logger.debug(f"Edit event received: '{edit_event_data.variant.name}:{edit_event_data.config_element_name} = {edit_event_data.new_value}'")
|
271
405
|
# Update the variant configuration data with the new value
|
272
|
-
variant = self.kconfig_data.find_variant_config(
|
273
|
-
edit_event_data.variant.name
|
274
|
-
)
|
406
|
+
variant = self.kconfig_data.find_variant_config(edit_event_data.variant.name)
|
275
407
|
if variant is None:
|
276
|
-
raise ValueError(
|
277
|
-
f"Could not find variant '{edit_event_data.variant.name}'"
|
278
|
-
)
|
408
|
+
raise ValueError(f"Could not find variant '{edit_event_data.variant.name}'")
|
279
409
|
config_element = variant.find_element(edit_event_data.config_element_name)
|
280
410
|
if config_element is None:
|
281
|
-
raise ValueError(
|
282
|
-
f"Could not find config element '{edit_event_data.config_element_name}'"
|
283
|
-
)
|
411
|
+
raise ValueError(f"Could not find config element '{edit_event_data.config_element_name}'")
|
284
412
|
config_element.value = edit_event_data.new_value
|
285
413
|
|
286
414
|
def run(self) -> None:
|
@@ -291,10 +419,7 @@ class KSPL(Presenter):
|
|
291
419
|
class GuiCommandConfig(DataClassDictMixin):
|
292
420
|
project_dir: Path = field(
|
293
421
|
default=Path(".").absolute(),
|
294
|
-
metadata={
|
295
|
-
"help": "Project root directory. "
|
296
|
-
"Defaults to the current directory if not specified."
|
297
|
-
},
|
422
|
+
metadata={"help": "Project root directory. Defaults to the current directory if not specified."},
|
298
423
|
)
|
299
424
|
|
300
425
|
@classmethod
|