461 lines
24 KiB
Python
461 lines
24 KiB
Python
import tkinter as tk
|
||
from tkinter import ttk
|
||
import time # For debouncing
|
||
|
||
DEFAULT_MAPPING = {
|
||
'svg_type': "rect", 'label_prefix': "", 'element_type': "ia.display.view",
|
||
'props_path': "Symbol-Views/Equipment-Views/Status", 'width': "14", 'height': "14",
|
||
'x_offset': "0", 'y_offset': "0", 'final_prefix': "", 'final_suffix': ""
|
||
}
|
||
|
||
class ElementMappingFrame(ttk.Frame):
|
||
"""Frame for managing SVG element mappings, including add/remove and saving."""
|
||
|
||
def __init__(self, parent, save_config_callback=None, default_mappings=None, **kwargs):
|
||
"""
|
||
Initialize the Element Mapping Frame.
|
||
|
||
Args:
|
||
parent: The parent widget.
|
||
save_config_callback (callable, optional): Function to call (debounced) when mappings change.
|
||
default_mappings (list, optional): Default mappings to load if none exist.
|
||
**kwargs: Additional arguments for ttk.Frame.
|
||
"""
|
||
super().__init__(parent, **kwargs)
|
||
self._save_config_callback = save_config_callback
|
||
self._default_mappings = default_mappings if default_mappings is not None else [DEFAULT_MAPPING]
|
||
|
||
# Internal state
|
||
self.mapping_rows = [] # List storing dicts: {'vars': {}, 'widgets': {}}
|
||
self._initialized = False # Flag to prevent saving during initial load
|
||
self._save_timer_id = None
|
||
self._canvas = None
|
||
self._scrollable_frame = None
|
||
|
||
self._create_widgets()
|
||
self._setup_bindings()
|
||
|
||
def _create_widgets(self):
|
||
"""Create the main widgets: Canvas, Scrollbar, inner frame, header, add button."""
|
||
# --- Container for Add Button + Scrollable Area ---
|
||
main_container = ttk.Frame(self)
|
||
main_container.pack(fill=tk.BOTH, expand=True)
|
||
main_container.rowconfigure(1, weight=1)
|
||
main_container.columnconfigure(0, weight=1)
|
||
|
||
# --- Add Button ---
|
||
add_button_frame = ttk.Frame(main_container, padding=(0, 5))
|
||
add_button_frame.grid(row=0, column=0, sticky=tk.EW)
|
||
add_button = ttk.Button(add_button_frame, text="Add New Mapping", command=self._handle_add_new_mapping_click)
|
||
add_button.pack(side=tk.LEFT)
|
||
# Add more buttons here later if needed (e.g., Load Defaults)
|
||
|
||
# --- Scrollable Area ---
|
||
scroll_container = ttk.Frame(main_container)
|
||
scroll_container.grid(row=1, column=0, sticky="nsew")
|
||
scroll_container.rowconfigure(0, weight=1)
|
||
scroll_container.columnconfigure(0, weight=1)
|
||
|
||
self._canvas = tk.Canvas(scroll_container, borderwidth=0, highlightthickness=0)
|
||
scrollbar = ttk.Scrollbar(scroll_container, orient="vertical", command=self._canvas.yview)
|
||
self._scrollable_frame = ttk.Frame(self._canvas) # The frame holding the mapping grid
|
||
|
||
self._scrollable_frame.bind("<Configure>", self._on_frame_configure)
|
||
self._canvas.create_window((0, 0), window=self._scrollable_frame, anchor="nw", tags="self.frame")
|
||
self._canvas.configure(yscrollcommand=scrollbar.set)
|
||
|
||
self._canvas.grid(row=0, column=0, sticky="nsew")
|
||
scrollbar.grid(row=0, column=1, sticky="ns")
|
||
|
||
# --- Header Row (in scrollable frame) ---
|
||
self._create_header_row(self._scrollable_frame)
|
||
|
||
def _on_frame_configure(self, event=None):
|
||
"""Reset the scroll region to encompass the inner frame."""
|
||
if self._canvas:
|
||
self._canvas.configure(scrollregion=self._canvas.bbox("all"))
|
||
|
||
def _create_header_row(self, parent):
|
||
"""Create the header row with column labels."""
|
||
headers = [
|
||
("SVG Type", 10), ("Label Prefix", 10), ("Element Type", 20), ("Props Path", 40),
|
||
("Size (WxH)", 10), ("Offset (X,Y)", 10), ("Final Prefix", 15), ("Final Suffix", 15), ("", 3) # Action
|
||
]
|
||
|
||
for col, (header, width) in enumerate(headers):
|
||
lbl = ttk.Label(parent, text=header, font=('TkDefaultFont', 9, 'bold'))
|
||
lbl.grid(row=0, column=col, padx=5, pady=(2,5), sticky="w")
|
||
parent.columnconfigure(col, weight=0, minsize=width*5) # Basic width estimate
|
||
parent.columnconfigure(3, weight=2) # Allow Props Path to expand most (increased weight)
|
||
parent.columnconfigure(2, weight=1)
|
||
|
||
def _setup_bindings(self):
|
||
"""Set up mouse wheel scrolling for the canvas."""
|
||
# Bind to canvas and scrollable frame to catch events inside
|
||
for widget in [self._canvas, self._scrollable_frame]:
|
||
# Unix-like
|
||
widget.bind("<Button-4>", self._on_mousewheel, add='+')
|
||
widget.bind("<Button-5>", self._on_mousewheel, add='+')
|
||
# Windows
|
||
widget.bind("<MouseWheel>", self._on_mousewheel, add='+')
|
||
|
||
def _on_mousewheel(self, event):
|
||
"""Handle mouse wheel scrolling only when mouse is over the canvas."""
|
||
# Check if the mouse is within the canvas bounds
|
||
canvas_x = self._canvas.winfo_rootx()
|
||
canvas_y = self._canvas.winfo_rooty()
|
||
canvas_w = self._canvas.winfo_width()
|
||
canvas_h = self._canvas.winfo_height()
|
||
if not (canvas_x <= event.x_root < canvas_x + canvas_w and
|
||
canvas_y <= event.y_root < canvas_y + canvas_h):
|
||
return # Mouse not over canvas
|
||
|
||
if event.num == 5 or event.delta < 0:
|
||
self._canvas.yview_scroll(1, "units")
|
||
elif event.num == 4 or event.delta > 0:
|
||
self._canvas.yview_scroll(-1, "units")
|
||
# Optional: return "break" to prevent event propagation further?
|
||
# return "break"
|
||
|
||
def _add_mapping_row(self, data=None):
|
||
"""Adds a new row for element mapping to the UI and internal list."""
|
||
if data is None: data = {}
|
||
|
||
row_index_in_grid = len(self.mapping_rows) + 1 # Grid row (1-based below header)
|
||
list_index = len(self.mapping_rows) # Index in self.mapping_rows list
|
||
|
||
row_vars = {
|
||
'svg_type': tk.StringVar(value=data.get('svg_type', '')),
|
||
'label_prefix': tk.StringVar(value=data.get('label_prefix', '')),
|
||
'element_type': tk.StringVar(value=data.get('element_type', '')),
|
||
'props_path': tk.StringVar(value=data.get('props_path', '')),
|
||
'width': tk.StringVar(value=str(data.get('width', ''))),
|
||
'height': tk.StringVar(value=str(data.get('height', ''))),
|
||
'x_offset': tk.StringVar(value=str(data.get('x_offset', ''))),
|
||
'y_offset': tk.StringVar(value=str(data.get('y_offset', ''))),
|
||
'final_prefix': tk.StringVar(value=data.get('final_prefix', '')),
|
||
'final_suffix': tk.StringVar(value=data.get('final_suffix', '')),
|
||
}
|
||
|
||
widgets = {}
|
||
pad_options = {'pady': 1, 'padx': 5}
|
||
entry_width = 10 # Default, will be overridden by column config mostly
|
||
|
||
# Create Entry Widgets
|
||
widgets['svg_entry'] = ttk.Entry(self._scrollable_frame, textvariable=row_vars['svg_type'], width=entry_width)
|
||
widgets['svg_entry'].grid(row=row_index_in_grid, column=0, sticky=tk.EW, **pad_options)
|
||
widgets['label_prefix_entry'] = ttk.Entry(self._scrollable_frame, textvariable=row_vars['label_prefix'], width=entry_width)
|
||
widgets['label_prefix_entry'].grid(row=row_index_in_grid, column=1, sticky=tk.EW, **pad_options)
|
||
widgets['element_entry'] = ttk.Entry(self._scrollable_frame, textvariable=row_vars['element_type'], width=entry_width*2)
|
||
widgets['element_entry'].grid(row=row_index_in_grid, column=2, sticky=tk.EW, **pad_options)
|
||
widgets['props_entry'] = ttk.Entry(self._scrollable_frame, textvariable=row_vars['props_path'], width=entry_width*6)
|
||
widgets['props_entry'].grid(row=row_index_in_grid, column=3, sticky=tk.EW, **pad_options)
|
||
|
||
# --- Size Frame (WxH) ---
|
||
size_frame = ttk.Frame(self._scrollable_frame)
|
||
size_frame.grid(row=row_index_in_grid, column=4, sticky=tk.EW, **pad_options)
|
||
widgets['width_entry'] = ttk.Entry(size_frame, textvariable=row_vars['width'], width=5)
|
||
widgets['width_entry'].pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0,1))
|
||
ttk.Label(size_frame, text="×").pack(side=tk.LEFT)
|
||
widgets['height_entry'] = ttk.Entry(size_frame, textvariable=row_vars['height'], width=5)
|
||
widgets['height_entry'].pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(1,0))
|
||
widgets['size_frame'] = size_frame
|
||
|
||
# --- Offset Frame (X,Y) ---
|
||
offset_frame = ttk.Frame(self._scrollable_frame)
|
||
offset_frame.grid(row=row_index_in_grid, column=5, sticky=tk.EW, **pad_options)
|
||
widgets['x_offset_entry'] = ttk.Entry(offset_frame, textvariable=row_vars['x_offset'], width=5)
|
||
widgets['x_offset_entry'].pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0,1))
|
||
ttk.Label(offset_frame, text=",").pack(side=tk.LEFT)
|
||
widgets['y_offset_entry'] = ttk.Entry(offset_frame, textvariable=row_vars['y_offset'], width=5)
|
||
widgets['y_offset_entry'].pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(1,0))
|
||
widgets['offset_frame'] = offset_frame
|
||
|
||
# --- Final Prefix/Suffix ---
|
||
widgets['final_prefix_entry'] = ttk.Entry(self._scrollable_frame, textvariable=row_vars['final_prefix'], width=entry_width+5)
|
||
widgets['final_prefix_entry'].grid(row=row_index_in_grid, column=6, sticky=tk.EW, **pad_options)
|
||
widgets['final_suffix_entry'] = ttk.Entry(self._scrollable_frame, textvariable=row_vars['final_suffix'], width=entry_width+5)
|
||
widgets['final_suffix_entry'].grid(row=row_index_in_grid, column=7, sticky=tk.EW, **pad_options)
|
||
|
||
# --- Remove Button ---
|
||
remove_button = ttk.Button(self._scrollable_frame, text="X", width=3, style="Danger.TButton",
|
||
command=lambda idx=list_index: self._handle_remove_mapping_click(idx))
|
||
remove_button.grid(row=row_index_in_grid, column=8, sticky=tk.W, pady=1, padx=(5, 15))
|
||
widgets['remove_button'] = remove_button
|
||
|
||
# Store row information
|
||
self.mapping_rows.append({'vars': row_vars, 'widgets': widgets})
|
||
|
||
# Add trace to trigger save when any value changes (AFTER adding to list)
|
||
for var in row_vars.values():
|
||
var.trace_add("write", self._schedule_config_save)
|
||
|
||
# Update scroll region after adding row
|
||
self._on_frame_configure()
|
||
# Scroll to bottom if added by button click (heuristic)
|
||
# if self._initialized: self._scroll_to_bottom()
|
||
|
||
return list_index # Return the list index
|
||
|
||
def _handle_add_new_mapping_click(self):
|
||
"""Callback for the 'Add New Mapping' button."""
|
||
new_index = self._add_mapping_row() # Add blank row
|
||
|
||
# Focus on the first entry of the new row and scroll
|
||
if new_index is not None and new_index < len(self.mapping_rows):
|
||
try:
|
||
self.mapping_rows[new_index]['widgets']['svg_entry'].focus_set()
|
||
self._scroll_to_show_row(new_index)
|
||
except tk.TclError: pass # Ignore if widget destroyed
|
||
|
||
self._schedule_config_save() # Trigger save after adding
|
||
|
||
def _scroll_to_show_row(self, list_index):
|
||
"""Scrolls the canvas so the specified row (by list index) is visible."""
|
||
try:
|
||
widget_to_show = self.mapping_rows[list_index]['widgets']['svg_entry']
|
||
self.update_idletasks() # Ensure layout calculated
|
||
self._canvas.yview_scroll(0, "pages") # Go to top first for better bbox calc?
|
||
bbox = self._canvas.bbox(widget_to_show) # Get bbox relative to canvas
|
||
if bbox:
|
||
canvas_height = self._canvas.winfo_height()
|
||
# Check if bottom of widget is below the visible area
|
||
if bbox[3] > self._canvas.canvasy(0) + canvas_height:
|
||
self._canvas.yview_moveto(bbox[1] / self._canvas.bbox("all")[3]) # Scroll based on top edge
|
||
# Check if top of widget is above the visible area
|
||
elif bbox[1] < self._canvas.canvasy(0):
|
||
self._canvas.yview_moveto(bbox[1] / self._canvas.bbox("all")[3])
|
||
except (IndexError, KeyError, tk.TclError) as e:
|
||
print(f"Error scrolling to row {list_index}: {e}")
|
||
|
||
def _handle_remove_mapping_click(self, list_index):
|
||
"""Removes the row and schedules a save."""
|
||
self._remove_mapping_row(list_index)
|
||
self._schedule_config_save() # Trigger save after removal
|
||
|
||
def _remove_mapping_row(self, list_index):
|
||
"""Removes a mapping row from the list and destroys its widgets."""
|
||
if not (0 <= list_index < len(self.mapping_rows)):
|
||
print(f"Error: Invalid index {list_index} for removing mapping row.")
|
||
return
|
||
|
||
row_to_remove = self.mapping_rows.pop(list_index)
|
||
|
||
# Destroy Widgets
|
||
for widget in row_to_remove['widgets'].values():
|
||
if widget and isinstance(widget, tk.Widget):
|
||
try:
|
||
widget.destroy()
|
||
except tk.TclError: pass # Ignore if already destroyed
|
||
|
||
# Re-grid the remaining rows below the removed one
|
||
self._re_grid_rows(start_list_index=list_index)
|
||
self._on_frame_configure() # Update scroll region
|
||
|
||
# If list is now empty, add a default row (but don't save it yet)
|
||
if not self.mapping_rows:
|
||
# Temporarily disable save callback during this add
|
||
original_callback = self._save_config_callback
|
||
self._save_config_callback = None
|
||
self._add_mapping_row(DEFAULT_MAPPING)
|
||
self._save_config_callback = original_callback
|
||
|
||
def _re_grid_rows(self, start_list_index=0):
|
||
"""Re-grids rows in the UI from the given list index onwards."""
|
||
for idx in range(start_list_index, len(self.mapping_rows)):
|
||
row_data = self.mapping_rows[idx]
|
||
new_grid_row = idx + 1 # Grid row is 1-based
|
||
widgets = row_data['widgets']
|
||
pad_options = {'pady': 1, 'padx': 5}
|
||
|
||
# --- Re-Grid all widgets ---
|
||
widgets['svg_entry'].grid(row=new_grid_row, column=0, sticky=tk.EW, **pad_options)
|
||
widgets['label_prefix_entry'].grid(row=new_grid_row, column=1, sticky=tk.EW, **pad_options)
|
||
widgets['element_entry'].grid(row=new_grid_row, column=2, sticky=tk.EW, **pad_options)
|
||
widgets['props_entry'].grid(row=new_grid_row, column=3, sticky=tk.EW, **pad_options)
|
||
widgets['size_frame'].grid(row=new_grid_row, column=4, sticky=tk.EW, **pad_options)
|
||
widgets['offset_frame'].grid(row=new_grid_row, column=5, sticky=tk.EW, **pad_options)
|
||
widgets['final_prefix_entry'].grid(row=new_grid_row, column=6, sticky=tk.EW, **pad_options)
|
||
widgets['final_suffix_entry'].grid(row=new_grid_row, column=7, sticky=tk.EW, **pad_options)
|
||
|
||
# --- Update Remove Button Command ---
|
||
remove_button = widgets['remove_button']
|
||
remove_button.grid(row=new_grid_row, column=8, sticky=tk.W, pady=1, padx=(5, 15))
|
||
remove_button.configure(command=lambda current_idx=idx: self._handle_remove_mapping_click(current_idx))
|
||
|
||
def _schedule_config_save(self, *args, delay=1500):
|
||
"""Debounces calls to the save configuration callback."""
|
||
if not self._initialized or not self._save_config_callback:
|
||
return # Don't save during init or if no callback provided
|
||
|
||
if self._save_timer_id is not None:
|
||
self.after_cancel(self._save_timer_id)
|
||
|
||
# print(f"DEBUG: Scheduling save in {delay}ms") # Debug
|
||
self._save_timer_id = self.after(delay, self._save_config_callback)
|
||
|
||
def get_mappings(self, include_incomplete=False):
|
||
"""Get all current mappings, optionally filtering incomplete ones."""
|
||
mappings = []
|
||
for row_data in self.mapping_rows:
|
||
try:
|
||
mapping = {
|
||
'svg_type': row_data['vars']['svg_type'].get().strip(),
|
||
'label_prefix': row_data['vars']['label_prefix'].get().strip(),
|
||
'element_type': row_data['vars']['element_type'].get().strip(),
|
||
'props_path': row_data['vars']['props_path'].get().strip(),
|
||
'width': row_data['vars']['width'].get().strip(),
|
||
'height': row_data['vars']['height'].get().strip(),
|
||
'x_offset': row_data['vars']['x_offset'].get().strip(),
|
||
'y_offset': row_data['vars']['y_offset'].get().strip(),
|
||
'final_prefix': row_data['vars']['final_prefix'].get().strip(),
|
||
'final_suffix': row_data['vars']['final_suffix'].get().strip()
|
||
}
|
||
# Basic Validation: Only include if essential info exists, unless forced
|
||
if include_incomplete or (mapping['svg_type'] and mapping['element_type']):
|
||
# Convert numeric fields safely
|
||
for key in ['width', 'height', 'x_offset', 'y_offset']:
|
||
# Try converting to int, keep as string if error or empty
|
||
try:
|
||
if mapping[key]: mapping[key] = int(mapping[key])
|
||
# else: keep empty string '' or handle as 0? Decide based on need.
|
||
# For saving, empty might mean use default. Keeping string allows flexibility.
|
||
except ValueError: pass # Keep original string if not valid int
|
||
|
||
mappings.append(mapping)
|
||
|
||
except (KeyError, tk.TclError) as e:
|
||
print(f"Warning: Skipping row during get_mappings due to error: {e}")
|
||
continue
|
||
|
||
return mappings
|
||
|
||
def load_mappings(self, mappings_data):
|
||
"""Load mappings into the frame, replacing existing ones."""
|
||
# --- Clear Existing Rows ---
|
||
# Cancel pending saves first
|
||
if self._save_timer_id is not None:
|
||
self.after_cancel(self._save_timer_id)
|
||
self._save_timer_id = None
|
||
|
||
# Iterate backwards through the list to remove rows
|
||
for i in range(len(self.mapping_rows) - 1, -1, -1):
|
||
self._remove_mapping_row(i)
|
||
# At this point, self.mapping_rows should be empty,
|
||
# and _remove_mapping_row should have added back one default row if it became empty.
|
||
# If the default row add logic is robust, we might not need to explicitly clear again.
|
||
# Let's ensure it's empty before loading.
|
||
if self.mapping_rows:
|
||
print("Warning: Rows still exist after clearing loop in load_mappings.")
|
||
# Force clear again?
|
||
for i in range(len(self.mapping_rows) - 1, -1, -1):
|
||
self._remove_mapping_row(i)
|
||
|
||
# --- Load New Mappings ---
|
||
self._initialized = False # Disable saving during load
|
||
if not mappings_data: # If input is empty list
|
||
mappings_to_load = self._default_mappings
|
||
else:
|
||
mappings_to_load = mappings_data
|
||
|
||
for mapping in mappings_to_load:
|
||
if isinstance(mapping, dict): # Basic check
|
||
self._add_mapping_row(data=mapping)
|
||
|
||
# --- Finalize ---
|
||
# Ensure at least one row exists (should be handled by _remove_mapping_row)
|
||
if not self.mapping_rows:
|
||
print("Warning: No rows after loading, adding default.")
|
||
self._add_mapping_row(DEFAULT_MAPPING)
|
||
|
||
self._initialized = True # Enable saving now
|
||
self.update_idletasks() # Update layout
|
||
self._on_frame_configure() # Recalculate scroll region
|
||
# print(f"Loaded {len(self.mapping_rows)} mappings.")
|
||
|
||
def cleanup_empty_rows(self):
|
||
"""Removes rows where both SVG Type and Element Type are empty, except if it's the only row."""
|
||
indices_to_remove = []
|
||
for i in range(len(self.mapping_rows) - 1, -1, -1):
|
||
# Don't remove the *only* row, even if empty
|
||
if len(self.mapping_rows) <= 1:
|
||
break
|
||
|
||
row_data = self.mapping_rows[i]
|
||
try:
|
||
svg_type = row_data['vars']['svg_type'].get().strip()
|
||
element_type = row_data['vars']['element_type'].get().strip()
|
||
if not svg_type and not element_type:
|
||
indices_to_remove.append(i)
|
||
except (KeyError, tk.TclError):
|
||
indices_to_remove.append(i) # Remove if vars/widgets are broken
|
||
|
||
if indices_to_remove:
|
||
print(f"Cleaning up {len(indices_to_remove)} empty/incomplete mapping rows.")
|
||
# Cancel any pending save before modifying rows
|
||
if self._save_timer_id is not None:
|
||
self.after_cancel(self._save_timer_id)
|
||
self._save_timer_id = None
|
||
|
||
for index in indices_to_remove:
|
||
self._remove_mapping_row(index) # This handles re-gridding
|
||
# Schedule a save after cleanup is done
|
||
self._schedule_config_save(delay=100)
|
||
|
||
# Example usage (for testing)
|
||
if __name__ == '__main__':
|
||
root = tk.Tk()
|
||
root.title("Element Mapping Frame Test")
|
||
root.geometry("950x400")
|
||
|
||
# --- Style ---
|
||
style = ttk.Style()
|
||
style.theme_use('clam')
|
||
# Add a danger style for the remove button
|
||
style.configure("Danger.TButton", foreground='white', background='#a83232')
|
||
style.map("Danger.TButton", background=[('active', '#c44'), ('pressed', '#d55')])
|
||
|
||
# --- Save Callback ---
|
||
def mock_save_config():
|
||
print(f"[{time.strftime('%H:%M:%S')}] MOCK SAVE triggered!")
|
||
current_mappings = mapping_frame.get_mappings()
|
||
print(f"Current Mappings ({len(current_mappings)}):")
|
||
import json
|
||
print(json.dumps(current_mappings, indent=2))
|
||
|
||
# --- Frame Creation ---
|
||
mapping_frame = ElementMappingFrame(root, save_config_callback=mock_save_config)
|
||
mapping_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
# --- Test Data ---
|
||
test_mappings = [
|
||
{'svg_type': 'rect', 'label_prefix': 'EQ', 'element_type': 'ia.display.view', 'props_path': 'Symbols/Valves/ValveSO', 'width': '20', 'height': '20', 'x_offset': '0', 'y_offset': '0', 'final_prefix': 'valves/', 'final_suffix': ''},
|
||
{'svg_type': 'circle', 'label_prefix': 'PMP', 'element_type': 'ia.display.view', 'props_path': 'Symbols/Pumps/PumpBasic', 'width': '30', 'height': '30', 'x_offset': '-15', 'y_offset': '-15', 'final_prefix': 'pumps/', 'final_suffix': '_status'},
|
||
{'svg_type': 'path', 'label_prefix': '', 'element_type': 'ia.display.label', 'props_path': '', 'width': '50', 'height': '15', 'x_offset': '0', 'y_offset': '0', 'final_prefix': 'labels/', 'final_suffix': ''},
|
||
# Add more diverse examples if needed
|
||
]
|
||
|
||
# --- Test Controls ---
|
||
controls_frame = ttk.Frame(root, padding=5)
|
||
controls_frame.pack()
|
||
|
||
load_button = ttk.Button(controls_frame, text="Load Test Data",
|
||
command=lambda: mapping_frame.load_mappings(test_mappings))
|
||
load_button.pack(side=tk.LEFT, padx=5)
|
||
|
||
clear_button = ttk.Button(controls_frame, text="Load Empty ([])",
|
||
command=lambda: mapping_frame.load_mappings([]))
|
||
clear_button.pack(side=tk.LEFT, padx=5)
|
||
|
||
cleanup_button = ttk.Button(controls_frame, text="Cleanup Empty Rows",
|
||
command=mapping_frame.cleanup_empty_rows)
|
||
cleanup_button.pack(side=tk.LEFT, padx=5)
|
||
|
||
get_button = ttk.Button(controls_frame, text="Get Mappings (Print)", command=mock_save_config)
|
||
get_button.pack(side=tk.LEFT, padx=5)
|
||
|
||
# --- Initial Load ---
|
||
# mapping_frame.load_mappings([]) # Start empty
|
||
mapping_frame.load_mappings(test_mappings) # Start with test data
|
||
|
||
root.mainloop() |