kspl 1.1.1__py3-none-any.whl → 1.3.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 +88 -85
- kspl/edit.py +71 -84
- kspl/generate.py +22 -43
- kspl/gui.py +416 -83
- kspl/kconfig.py +228 -242
- kspl/main.py +35 -39
- {kspl-1.1.1.dist-info → kspl-1.3.0.dist-info}/METADATA +27 -39
- kspl-1.3.0.dist-info/RECORD +14 -0
- {kspl-1.1.1.dist-info → kspl-1.3.0.dist-info}/WHEEL +1 -1
- kspl-1.1.1.dist-info/RECORD +0 -14
- {kspl-1.1.1.dist-info → kspl-1.3.0.dist-info}/LICENSE +0 -0
- {kspl-1.1.1.dist-info → kspl-1.3.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
|
@@ -21,6 +21,7 @@ from kspl.kconfig import ConfigElementType, EditableConfigElement, TriState
|
|
21
21
|
|
22
22
|
class KSplEvents(EventID):
|
23
23
|
EDIT = auto()
|
24
|
+
REFRESH = auto()
|
24
25
|
|
25
26
|
|
26
27
|
class CTkView(View):
|
@@ -40,8 +41,8 @@ class MainView(CTkView):
|
|
40
41
|
def __init__(
|
41
42
|
self,
|
42
43
|
event_manager: EventManager,
|
43
|
-
elements:
|
44
|
-
variants:
|
44
|
+
elements: list[EditableConfigElement],
|
45
|
+
variants: list[VariantViewData],
|
45
46
|
) -> None:
|
46
47
|
self.event_manager = event_manager
|
47
48
|
self.elements = elements
|
@@ -49,47 +50,90 @@ class MainView(CTkView):
|
|
49
50
|
self.variants = variants
|
50
51
|
|
51
52
|
self.logger = logger.bind()
|
52
|
-
self.edit_event_data:
|
53
|
-
self.trigger_edit_event = self.event_manager.create_event_trigger(
|
54
|
-
|
55
|
-
)
|
53
|
+
self.edit_event_data: EditEventData | None = None
|
54
|
+
self.trigger_edit_event = self.event_manager.create_event_trigger(KSplEvents.EDIT)
|
55
|
+
self.trigger_refresh_event = self.event_manager.create_event_trigger(KSplEvents.REFRESH)
|
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
|
+
|
73
|
+
# Tree expansion controls with segmented button
|
74
|
+
tree_label = customtkinter.CTkLabel(control_frame, text="Tree:", font=("Arial", 12))
|
75
|
+
tree_label.pack(side="left", padx=(10, 5))
|
76
|
+
|
77
|
+
self.tree_control_segment = customtkinter.CTkSegmentedButton(
|
78
|
+
master=control_frame,
|
79
|
+
values=["⊞", "⊟", "🔄"],
|
80
|
+
command=self.on_tree_control_segment_click,
|
81
|
+
height=30,
|
82
|
+
)
|
83
|
+
self.tree_control_segment.pack(side="left", padx=2)
|
84
|
+
# Note: Tooltip doesn't work with segmented buttons due to CTk limitations
|
85
|
+
|
86
|
+
# Reserve space for future refresh button
|
87
|
+
# self.refresh_button = customtkinter.CTkButton(
|
88
|
+
# master=control_frame,
|
89
|
+
# text="🔄",
|
90
|
+
# command=self.refresh_data,
|
91
|
+
# width=30,
|
92
|
+
# height=30,
|
93
|
+
# )
|
94
|
+
# self.refresh_button.pack(side="left", padx=2)
|
95
|
+
# self.create_tooltip(self.refresh_button, "Refresh")
|
96
|
+
|
62
97
|
# ========================================================
|
63
|
-
# create
|
64
|
-
|
65
|
-
self.tree = self.create_tree_view(
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
98
|
+
# create main content frame
|
99
|
+
main_frame = customtkinter.CTkFrame(self.root)
|
100
|
+
self.tree = self.create_tree_view(main_frame)
|
101
|
+
|
102
|
+
# Initialize column manager after tree is created
|
103
|
+
self.column_manager = ColumnManager(self.tree)
|
104
|
+
self.column_manager.update_columns(self.variants)
|
105
|
+
|
70
106
|
# Keep track of the mapping between the tree view items and the config elements
|
71
107
|
self.tree_view_items_mapping = self.populate_tree_view()
|
72
|
-
self.
|
108
|
+
self.adjust_column_width()
|
109
|
+
self.tree.bind("<Button-1>", self.on_tree_click)
|
73
110
|
# TODO: make the tree view editable
|
74
111
|
# self.tree.bind("<Double-1>", self.double_click_handler)
|
75
112
|
|
76
113
|
# ========================================================
|
77
114
|
# put all together
|
78
115
|
self.root.grid_columnconfigure(0, weight=1)
|
79
|
-
self.root.grid_rowconfigure(0, weight=
|
80
|
-
|
116
|
+
self.root.grid_rowconfigure(0, weight=0)
|
117
|
+
self.root.grid_rowconfigure(1, weight=1)
|
118
|
+
main_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 10))
|
81
119
|
|
82
120
|
def mainloop(self) -> None:
|
83
121
|
self.root.mainloop()
|
84
122
|
|
85
123
|
def create_tree_view(self, frame: customtkinter.CTkFrame) -> ttk.Treeview:
|
86
|
-
frame.grid_rowconfigure(0, weight=
|
87
|
-
frame.grid_rowconfigure(1, weight=1)
|
124
|
+
frame.grid_rowconfigure(0, weight=1)
|
88
125
|
frame.grid_columnconfigure(0, weight=1)
|
89
126
|
|
90
127
|
columns = [var.name for var in self.variants]
|
91
128
|
|
92
129
|
style = ttk.Style()
|
130
|
+
# From: https://stackoverflow.com/a/56684731
|
131
|
+
# This gives the selection a transparent look
|
132
|
+
style.map(
|
133
|
+
"mystyle.Treeview",
|
134
|
+
background=[("selected", "#a6d5f7")],
|
135
|
+
foreground=[("selected", "black")],
|
136
|
+
)
|
93
137
|
style.configure(
|
94
138
|
"mystyle.Treeview",
|
95
139
|
highlightthickness=0,
|
@@ -97,9 +141,26 @@ class MainView(CTkView):
|
|
97
141
|
font=("Calibri", 14),
|
98
142
|
rowheight=30,
|
99
143
|
) # Modify the font of the body
|
100
|
-
style.configure(
|
101
|
-
|
102
|
-
|
144
|
+
style.configure("mystyle.Treeview.Heading", font=("Calibri", 14, "bold")) # Modify the font of the headings
|
145
|
+
|
146
|
+
# Add a separator to the right of the heading
|
147
|
+
MainView.vline_img = tkinter.PhotoImage("vline", data="R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=")
|
148
|
+
style.element_create("vline", "image", "vline")
|
149
|
+
style.layout(
|
150
|
+
"mystyle.Treeview.Heading",
|
151
|
+
[
|
152
|
+
(
|
153
|
+
"mystyle.Treeview.heading.cell",
|
154
|
+
{
|
155
|
+
"sticky": "nswe",
|
156
|
+
"children": [
|
157
|
+
("mystyle.Treeview.heading.text", {"sticky": "we"}),
|
158
|
+
("vline", {"side": "right", "sticky": "ns"}),
|
159
|
+
],
|
160
|
+
},
|
161
|
+
)
|
162
|
+
],
|
163
|
+
)
|
103
164
|
|
104
165
|
# create a Treeview widget
|
105
166
|
config_treeview = ttk.Treeview(
|
@@ -108,17 +169,25 @@ class MainView(CTkView):
|
|
108
169
|
show="tree headings",
|
109
170
|
style="mystyle.Treeview",
|
110
171
|
)
|
111
|
-
|
172
|
+
|
173
|
+
scrollbar_y = ttk.Scrollbar(frame, command=config_treeview.yview)
|
174
|
+
scrollbar_x = ttk.Scrollbar(frame, command=config_treeview.xview, orient=tkinter.HORIZONTAL)
|
175
|
+
config_treeview.config(xscrollcommand=scrollbar_x.set, yscrollcommand=scrollbar_y.set)
|
176
|
+
scrollbar_y.pack(fill=tkinter.Y, side=tkinter.RIGHT)
|
177
|
+
scrollbar_x.pack(fill=tkinter.X, side=tkinter.BOTTOM)
|
178
|
+
config_treeview.pack(fill=tkinter.BOTH, expand=True)
|
179
|
+
|
112
180
|
return config_treeview
|
113
181
|
|
114
|
-
def populate_tree_view(self) ->
|
182
|
+
def populate_tree_view(self) -> dict[str, str]:
|
115
183
|
"""
|
116
184
|
Populates the tree view with the configuration elements.
|
185
|
+
|
117
186
|
:return: a mapping between the tree view items and the configuration elements
|
118
187
|
"""
|
119
188
|
stack = [] # To keep track of the parent items
|
120
189
|
last_level = -1
|
121
|
-
mapping:
|
190
|
+
mapping: dict[str, str] = {}
|
122
191
|
|
123
192
|
for element in self.elements:
|
124
193
|
values = self.collect_values_for_element(element)
|
@@ -128,45 +197,28 @@ class MainView(CTkView):
|
|
128
197
|
stack = [item_id] # Reset the stack with the root item
|
129
198
|
elif element.level > last_level:
|
130
199
|
# 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
|
-
)
|
200
|
+
item_id = self.tree.insert(stack[-1], "end", text=element.name, values=values)
|
134
201
|
stack.append(item_id)
|
135
202
|
elif element.level == last_level:
|
136
203
|
# 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
|
-
)
|
204
|
+
item_id = self.tree.insert(stack[-2], "end", text=element.name, values=values)
|
140
205
|
stack[-1] = item_id # Replace the top item in the stack
|
141
206
|
else:
|
142
207
|
# 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]
|
208
|
+
item_id = self.tree.insert(stack[element.level - 1], "end", text=element.name, values=values)
|
209
|
+
stack = [*stack[: element.level], item_id]
|
147
210
|
|
148
211
|
last_level = element.level
|
149
212
|
mapping[item_id] = element.name
|
150
213
|
return mapping
|
151
214
|
|
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
|
-
)
|
215
|
+
def collect_values_for_element(self, element: EditableConfigElement) -> list[int | str]:
|
216
|
+
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
217
|
|
166
|
-
def prepare_value_to_be_displayed(
|
167
|
-
self, element_type: ConfigElementType, value: Any
|
168
|
-
) -> str:
|
218
|
+
def prepare_value_to_be_displayed(self, element_type: ConfigElementType, value: Any) -> str:
|
169
219
|
"""
|
220
|
+
Prepare the value to be displayed in the tree view based on the element type.
|
221
|
+
|
170
222
|
UNKNOWN - N/A
|
171
223
|
BOOL - ✅ ⛔
|
172
224
|
TRISTATE - str
|
@@ -182,7 +234,54 @@ class MainView(CTkView):
|
|
182
234
|
else:
|
183
235
|
return str(value)
|
184
236
|
|
185
|
-
def
|
237
|
+
def adjust_column_width(self) -> None:
|
238
|
+
"""Adjust the column widths to fit the header text, preserving manual resizing."""
|
239
|
+
heading_font = font.Font(font=("Calibri", 14, "bold"))
|
240
|
+
padding = 60
|
241
|
+
|
242
|
+
# Only adjust columns that actually exist in the current configuration
|
243
|
+
current_columns = self.tree["columns"]
|
244
|
+
for col in current_columns:
|
245
|
+
try:
|
246
|
+
text = self.tree.heading(col, "text")
|
247
|
+
min_width = heading_font.measure(text) + padding
|
248
|
+
# Get current width to preserve manual resizing
|
249
|
+
current_width = self.tree.column(col, "width")
|
250
|
+
# Use the larger of current width or minimum required width
|
251
|
+
final_width = max(current_width, min_width)
|
252
|
+
self.tree.column(col, minwidth=min_width, width=final_width, stretch=False)
|
253
|
+
except tkinter.TclError:
|
254
|
+
# Column might not exist anymore, skip it
|
255
|
+
self.logger.warning(f"Skipping column '{col}' as it no longer exists")
|
256
|
+
continue
|
257
|
+
|
258
|
+
# First column (#0)
|
259
|
+
try:
|
260
|
+
text = self.tree.heading("#0", "text")
|
261
|
+
min_width = heading_font.measure(text) + padding
|
262
|
+
current_width = self.tree.column("#0", "width")
|
263
|
+
final_width = max(current_width, min_width)
|
264
|
+
self.tree.column("#0", minwidth=min_width, width=final_width, stretch=False)
|
265
|
+
except tkinter.TclError:
|
266
|
+
self.logger.warning("Skipping column '#0' as it no longer exists")
|
267
|
+
|
268
|
+
def on_tree_click(self, event: Any) -> None:
|
269
|
+
"""Handle click events on the treeview to highlight the column header."""
|
270
|
+
column_name = self.column_manager.get_column_from_click_position(event.x)
|
271
|
+
|
272
|
+
if column_name is None:
|
273
|
+
# Click was on the tree part or outside columns, clear selection
|
274
|
+
self.column_manager.clear_selection()
|
275
|
+
return
|
276
|
+
|
277
|
+
if column_name == self.column_manager.selected_column_id:
|
278
|
+
# Already selected, do nothing
|
279
|
+
return
|
280
|
+
|
281
|
+
# Set the new selected column
|
282
|
+
self.column_manager.set_selected_column(column_name)
|
283
|
+
|
284
|
+
def double_click_handler(self, event: Any) -> None:
|
186
285
|
current_selection = self.tree.selection()
|
187
286
|
if not current_selection:
|
188
287
|
return
|
@@ -191,9 +290,7 @@ class MainView(CTkView):
|
|
191
290
|
selected_element_name = self.tree_view_items_mapping[selected_item]
|
192
291
|
|
193
292
|
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
|
293
|
+
variant_idx = int(variant_idx_str.split("#")[-1]) - 1 # Convert to 0-based index
|
197
294
|
|
198
295
|
if variant_idx < 0 or variant_idx >= len(self.variants):
|
199
296
|
return
|
@@ -207,9 +304,7 @@ class MainView(CTkView):
|
|
207
304
|
new_value: Any = None
|
208
305
|
if selected_element.type == ConfigElementType.BOOL:
|
209
306
|
# Toggle the boolean value
|
210
|
-
new_value =
|
211
|
-
TriState.N if selected_element_value == TriState.Y else TriState.Y
|
212
|
-
)
|
307
|
+
new_value = TriState.N if selected_element_value == TriState.Y else TriState.Y
|
213
308
|
elif selected_element.type == ConfigElementType.INT:
|
214
309
|
tmp_int_value = simpledialog.askinteger(
|
215
310
|
"Enter new value",
|
@@ -231,26 +326,142 @@ class MainView(CTkView):
|
|
231
326
|
# Check if the value has changed
|
232
327
|
if new_value:
|
233
328
|
# Trigger the EDIT event
|
234
|
-
self.create_edit_event_trigger(
|
235
|
-
selected_variant, selected_element_name, new_value
|
236
|
-
)
|
329
|
+
self.create_edit_event_trigger(selected_variant, selected_element_name, new_value)
|
237
330
|
|
238
|
-
def create_edit_event_trigger(
|
239
|
-
self, variant: VariantViewData, element_name: str, new_value: Any
|
240
|
-
) -> None:
|
331
|
+
def create_edit_event_trigger(self, variant: VariantViewData, element_name: str, new_value: Any) -> None:
|
241
332
|
self.edit_event_data = EditEventData(variant, element_name, new_value)
|
242
333
|
self.trigger_edit_event()
|
243
334
|
|
244
|
-
def pop_edit_event_data(self) ->
|
335
|
+
def pop_edit_event_data(self) -> EditEventData | None:
|
245
336
|
result = self.edit_event_data
|
246
337
|
self.edit_event_data = None
|
247
338
|
return result
|
248
339
|
|
340
|
+
def expand_all_items(self) -> None:
|
341
|
+
"""Expand all items in the tree view."""
|
342
|
+
|
343
|
+
def expand_recursive(item: str) -> None:
|
344
|
+
self.tree.item(item, open=True)
|
345
|
+
children = self.tree.get_children(item)
|
346
|
+
for child in children:
|
347
|
+
expand_recursive(child)
|
348
|
+
|
349
|
+
# Start with root items
|
350
|
+
root_items = self.tree.get_children()
|
351
|
+
for item in root_items:
|
352
|
+
expand_recursive(item)
|
353
|
+
|
354
|
+
def collapse_all_items(self) -> None:
|
355
|
+
"""Collapse all items in the tree view."""
|
356
|
+
|
357
|
+
def collapse_recursive(item: str) -> None:
|
358
|
+
children = self.tree.get_children(item)
|
359
|
+
for child in children:
|
360
|
+
collapse_recursive(child)
|
361
|
+
self.tree.item(item, open=False)
|
362
|
+
|
363
|
+
# Start with root items
|
364
|
+
root_items = self.tree.get_children()
|
365
|
+
for item in root_items:
|
366
|
+
collapse_recursive(item)
|
367
|
+
|
368
|
+
def on_tree_control_segment_click(self, value: str) -> None:
|
369
|
+
"""Handle clicks on the tree control segmented button."""
|
370
|
+
if value == "⊞":
|
371
|
+
self.expand_all_items()
|
372
|
+
elif value == "⊟":
|
373
|
+
self.collapse_all_items()
|
374
|
+
elif value == "🔄":
|
375
|
+
self.trigger_refresh_event()
|
376
|
+
# Reset selection to avoid button staying selected
|
377
|
+
self.tree_control_segment.set("")
|
378
|
+
|
379
|
+
def open_column_selection_dialog(self) -> None:
|
380
|
+
"""Open a dialog to select which columns to display."""
|
381
|
+
# Create a new top-level window
|
382
|
+
dialog = customtkinter.CTkToplevel(self.root)
|
383
|
+
dialog.title("Select variants")
|
384
|
+
dialog.geometry("400x300")
|
385
|
+
|
386
|
+
# Create a frame for the checkboxes
|
387
|
+
checkbox_frame = customtkinter.CTkFrame(dialog)
|
388
|
+
checkbox_frame.pack(padx=10, pady=10, fill="both", expand=True)
|
389
|
+
|
390
|
+
# Create a variable for each column using ColumnManager
|
391
|
+
for column_name in self.column_manager.all_columns:
|
392
|
+
# Set the initial value based on whether the column is currently visible
|
393
|
+
is_visible = column_name in self.column_manager.visible_columns
|
394
|
+
|
395
|
+
# Get or create variable
|
396
|
+
if column_name not in self.column_manager.column_vars:
|
397
|
+
self.column_manager.column_vars[column_name] = tkinter.BooleanVar(value=is_visible)
|
398
|
+
else:
|
399
|
+
self.column_manager.column_vars[column_name].set(is_visible)
|
400
|
+
|
401
|
+
checkbox = customtkinter.CTkCheckBox(
|
402
|
+
master=checkbox_frame,
|
403
|
+
text=column_name,
|
404
|
+
command=self.update_visible_columns,
|
405
|
+
variable=self.column_manager.column_vars[column_name],
|
406
|
+
)
|
407
|
+
checkbox.pack(anchor="w", padx=5, pady=2)
|
408
|
+
|
409
|
+
# Add OK and Cancel buttons
|
410
|
+
button_frame = customtkinter.CTkFrame(dialog)
|
411
|
+
button_frame.pack(padx=10, pady=10)
|
412
|
+
|
413
|
+
ok_button = customtkinter.CTkButton(
|
414
|
+
master=button_frame,
|
415
|
+
text="OK",
|
416
|
+
command=dialog.destroy,
|
417
|
+
)
|
418
|
+
ok_button.pack(side="right", padx=5)
|
419
|
+
|
420
|
+
cancel_button = customtkinter.CTkButton(
|
421
|
+
master=button_frame,
|
422
|
+
text="Cancel",
|
423
|
+
command=dialog.destroy,
|
424
|
+
)
|
425
|
+
cancel_button.pack(side="right", padx=5)
|
426
|
+
|
427
|
+
# Center the dialog on the screen
|
428
|
+
dialog.update_idletasks()
|
429
|
+
x = (self.root.winfo_width() - dialog.winfo_width()) // 2
|
430
|
+
y = (self.root.winfo_height() - dialog.winfo_height()) // 2
|
431
|
+
dialog.geometry(f"+{self.root.winfo_x() + x}+{self.root.winfo_y() + y}")
|
432
|
+
|
433
|
+
dialog.transient(self.root) # Keep the dialog above the main window
|
434
|
+
dialog.grab_set() # Make the dialog modal
|
435
|
+
|
436
|
+
def update_visible_columns(self) -> None:
|
437
|
+
"""Wrapper method to update visible columns via ColumnManager."""
|
438
|
+
self.column_manager.update_visible_columns()
|
439
|
+
|
440
|
+
def update_data(self, elements: list[EditableConfigElement], variants: list[VariantViewData]) -> None:
|
441
|
+
"""Update the view with refreshed data."""
|
442
|
+
self.elements = elements
|
443
|
+
self.elements_dict = {elem.name: elem for elem in elements}
|
444
|
+
self.variants = variants
|
445
|
+
|
446
|
+
# Clear the tree first
|
447
|
+
for item in self.tree.get_children():
|
448
|
+
self.tree.delete(item)
|
449
|
+
|
450
|
+
# Use ColumnManager to handle all column-related updates
|
451
|
+
self.column_manager.update_columns(variants)
|
452
|
+
|
453
|
+
# Repopulate the tree view
|
454
|
+
self.tree_view_items_mapping = self.populate_tree_view()
|
455
|
+
self.adjust_column_width()
|
456
|
+
|
457
|
+
# ...existing code...
|
458
|
+
|
249
459
|
|
250
460
|
class KSPL(Presenter):
|
251
461
|
def __init__(self, event_manager: EventManager, project_dir: Path) -> None:
|
252
462
|
self.event_manager = event_manager
|
253
463
|
self.event_manager.subscribe(KSplEvents.EDIT, self.edit)
|
464
|
+
self.event_manager.subscribe(KSplEvents.REFRESH, self.refresh)
|
254
465
|
self.logger = logger.bind()
|
255
466
|
self.kconfig_data = SPLKConfigData(project_dir)
|
256
467
|
self.view = MainView(
|
@@ -264,25 +475,41 @@ class KSPL(Presenter):
|
|
264
475
|
if edit_event_data is None:
|
265
476
|
self.logger.error("Edit event received but event data is missing!")
|
266
477
|
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
|
-
)
|
478
|
+
self.logger.debug(f"Edit event received: '{edit_event_data.variant.name}:{edit_event_data.config_element_name} = {edit_event_data.new_value}'")
|
271
479
|
# Update the variant configuration data with the new value
|
272
|
-
variant = self.kconfig_data.find_variant_config(
|
273
|
-
edit_event_data.variant.name
|
274
|
-
)
|
480
|
+
variant = self.kconfig_data.find_variant_config(edit_event_data.variant.name)
|
275
481
|
if variant is None:
|
276
|
-
raise ValueError(
|
277
|
-
f"Could not find variant '{edit_event_data.variant.name}'"
|
278
|
-
)
|
482
|
+
raise ValueError(f"Could not find variant '{edit_event_data.variant.name}'")
|
279
483
|
config_element = variant.find_element(edit_event_data.config_element_name)
|
280
484
|
if config_element is None:
|
281
|
-
raise ValueError(
|
282
|
-
f"Could not find config element '{edit_event_data.config_element_name}'"
|
283
|
-
)
|
485
|
+
raise ValueError(f"Could not find config element '{edit_event_data.config_element_name}'")
|
284
486
|
config_element.value = edit_event_data.new_value
|
285
487
|
|
488
|
+
def refresh(self) -> None:
|
489
|
+
"""Handle refresh event by reloading data and updating the view."""
|
490
|
+
self.logger.info("Refreshing KConfig data...")
|
491
|
+
try:
|
492
|
+
# Store old state for debugging
|
493
|
+
old_variants = [v.name for v in self.kconfig_data.get_variants()]
|
494
|
+
self.logger.debug(f"Before refresh: {len(old_variants)} variants: {old_variants}")
|
495
|
+
|
496
|
+
self.kconfig_data.refresh_data()
|
497
|
+
|
498
|
+
# Log new state
|
499
|
+
new_variants = [v.name for v in self.kconfig_data.get_variants()]
|
500
|
+
self.logger.debug(f"After refresh: {len(new_variants)} variants: {new_variants}")
|
501
|
+
|
502
|
+
# Update the view with new data
|
503
|
+
self.view.update_data(
|
504
|
+
self.kconfig_data.get_elements(),
|
505
|
+
self.kconfig_data.get_variants(),
|
506
|
+
)
|
507
|
+
self.logger.info("Data refreshed successfully")
|
508
|
+
except Exception as e:
|
509
|
+
self.logger.error(f"Failed to refresh data: {e}")
|
510
|
+
# Don't re-raise the exception to prevent the GUI from crashing
|
511
|
+
# Instead, keep the current data state
|
512
|
+
|
286
513
|
def run(self) -> None:
|
287
514
|
self.view.mainloop()
|
288
515
|
|
@@ -291,10 +518,7 @@ class KSPL(Presenter):
|
|
291
518
|
class GuiCommandConfig(DataClassDictMixin):
|
292
519
|
project_dir: Path = field(
|
293
520
|
default=Path(".").absolute(),
|
294
|
-
metadata={
|
295
|
-
"help": "Project root directory. "
|
296
|
-
"Defaults to the current directory if not specified."
|
297
|
-
},
|
521
|
+
metadata={"help": "Project root directory. Defaults to the current directory if not specified."},
|
298
522
|
)
|
299
523
|
|
300
524
|
@classmethod
|
@@ -317,3 +541,112 @@ class GuiCommand(Command):
|
|
317
541
|
|
318
542
|
def _register_arguments(self, parser: ArgumentParser) -> None:
|
319
543
|
register_arguments_for_config_dataclass(parser, GuiCommandConfig)
|
544
|
+
|
545
|
+
|
546
|
+
class ColumnManager:
|
547
|
+
"""Manages column state, visibility, selection, and headings for the treeview."""
|
548
|
+
|
549
|
+
def __init__(self, tree: ttk.Treeview) -> None:
|
550
|
+
self.tree = tree
|
551
|
+
self.all_columns: list[str] = []
|
552
|
+
self.visible_columns: list[str] = []
|
553
|
+
self.header_texts: dict[str, str] = {}
|
554
|
+
self.selected_column_id: str | None = None
|
555
|
+
self.column_vars: dict[str, tkinter.BooleanVar] = {}
|
556
|
+
|
557
|
+
def update_columns(self, variants: list[VariantViewData]) -> None:
|
558
|
+
"""Update column configuration with new variants."""
|
559
|
+
# Clear any existing selection FIRST, before any tree operations
|
560
|
+
self.selected_column_id = None
|
561
|
+
|
562
|
+
# Update column lists
|
563
|
+
new_all_columns = [v.name for v in variants]
|
564
|
+
|
565
|
+
# Preserve visible columns that still exist, add new ones
|
566
|
+
if self.visible_columns:
|
567
|
+
existing_visible = [col for col in self.visible_columns if col in new_all_columns]
|
568
|
+
new_columns = [col for col in new_all_columns if col not in existing_visible]
|
569
|
+
self.visible_columns = existing_visible + new_columns
|
570
|
+
else:
|
571
|
+
self.visible_columns = list(new_all_columns)
|
572
|
+
|
573
|
+
# Update all_columns after determining visible columns
|
574
|
+
self.all_columns = new_all_columns
|
575
|
+
|
576
|
+
# Update tree configuration with error handling
|
577
|
+
try:
|
578
|
+
# First completely clear the tree configuration
|
579
|
+
self.tree.configure(columns=(), displaycolumns=())
|
580
|
+
# Then set new configuration
|
581
|
+
self.tree["columns"] = tuple(self.all_columns)
|
582
|
+
self.tree["displaycolumns"] = self.visible_columns
|
583
|
+
except tkinter.TclError:
|
584
|
+
# If there's still an error, log it but continue
|
585
|
+
pass
|
586
|
+
|
587
|
+
# Update header texts and tree headings
|
588
|
+
self.header_texts = {}
|
589
|
+
for variant in variants:
|
590
|
+
self.header_texts[variant.name] = variant.name
|
591
|
+
try:
|
592
|
+
self.tree.heading(variant.name, text=variant.name)
|
593
|
+
except tkinter.TclError:
|
594
|
+
# Column might not exist yet, will be handled in next update
|
595
|
+
pass
|
596
|
+
|
597
|
+
# Clean up column variables for dialog
|
598
|
+
self.column_vars = {k: v for k, v in self.column_vars.items() if k in self.all_columns}
|
599
|
+
|
600
|
+
def set_selected_column(self, column_name: str) -> bool:
|
601
|
+
"""Set the selected column and update its visual state. Returns True if successful."""
|
602
|
+
if column_name not in self.all_columns:
|
603
|
+
return False
|
604
|
+
|
605
|
+
# Clear previous selection
|
606
|
+
self._clear_selection()
|
607
|
+
|
608
|
+
# Set new selection
|
609
|
+
self.selected_column_id = column_name
|
610
|
+
original_text = self.header_texts.get(column_name)
|
611
|
+
if original_text:
|
612
|
+
try:
|
613
|
+
self.tree.heading(column_name, text=f"✅{original_text}")
|
614
|
+
return True
|
615
|
+
except tkinter.TclError:
|
616
|
+
self.selected_column_id = None
|
617
|
+
return False
|
618
|
+
return False
|
619
|
+
|
620
|
+
def clear_selection(self) -> None:
|
621
|
+
"""Clear the column selection."""
|
622
|
+
self._clear_selection()
|
623
|
+
|
624
|
+
def _clear_selection(self) -> None:
|
625
|
+
"""Internal method to clear selection without public access."""
|
626
|
+
if self.selected_column_id and self.selected_column_id in self.header_texts:
|
627
|
+
# Only try to clear if the column still exists in the tree
|
628
|
+
if self.selected_column_id in self.all_columns:
|
629
|
+
original_text = self.header_texts[self.selected_column_id]
|
630
|
+
try:
|
631
|
+
self.tree.heading(self.selected_column_id, text=original_text)
|
632
|
+
except tkinter.TclError:
|
633
|
+
# Column no longer exists in tree, that's fine
|
634
|
+
pass
|
635
|
+
self.selected_column_id = None
|
636
|
+
|
637
|
+
def get_column_from_click_position(self, x: int) -> str | None:
|
638
|
+
"""Get column name from click position, returns None if invalid."""
|
639
|
+
column_id_str = self.tree.identify_column(x)
|
640
|
+
if not column_id_str or column_id_str == "#0":
|
641
|
+
return None
|
642
|
+
|
643
|
+
col_idx = int(column_id_str.replace("#", "")) - 1
|
644
|
+
if col_idx < 0 or col_idx >= len(self.visible_columns):
|
645
|
+
return None
|
646
|
+
|
647
|
+
return self.visible_columns[col_idx]
|
648
|
+
|
649
|
+
def update_visible_columns(self) -> None:
|
650
|
+
"""Update visible columns based on column_vars state."""
|
651
|
+
self.visible_columns = [col_name for col_name, var in self.column_vars.items() if var.get()]
|
652
|
+
self.tree["displaycolumns"] = self.visible_columns
|