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/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, Dict, List, Optional
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: List[EditableConfigElement],
44
- variants: List[VariantViewData],
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: Optional[EditEventData] = None
53
- self.trigger_edit_event = self.event_manager.create_event_trigger(
54
- KSplEvents.EDIT
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 tabview and populate with frames
64
- tabview = customtkinter.CTkTabview(self.root)
65
- self.tree = self.create_tree_view(tabview.add("Configuration"))
66
- self.tree["columns"] = tuple(variant.name for variant in self.variants)
67
- self.tree.heading("#0", text="Configuration")
68
- for variant in self.variants:
69
- self.tree.heading(variant.name, text=variant.name)
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.tree.pack(fill="both", expand=True)
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=1)
80
- tabview.grid(row=0, column=0, sticky="nsew")
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=10)
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
- "mystyle.Treeview.Heading", font=("Calibri", 14, "bold")
102
- ) # Modify the font of the headings
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
- config_treeview.grid(row=0, column=0, sticky="nsew")
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) -> Dict[str, str]:
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: Dict[str, str] = {}
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
- stack[element.level - 1], "end", text=element.name, values=values
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: EditableConfigElement
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 double_click_handler(self, event: tkinter.Event) -> None: # type: ignore
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) -> Optional[EditEventData]:
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