#!/usr/bin/env python3
"""
Reusable UI widgets for the VorLap GUI.
This module contains custom widgets that are used across multiple tabs.
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
import csv
[docs]
class PathEntry(ttk.Frame):
"""Entry + Browse button (file or directory)."""
[docs]
def __init__(self, master, kind="file", title="Select...", must_exist=False, **kwargs):
"""
Initialize PathEntry widget.
Args:
master: Parent widget.
kind: Type of path selection ("file", "dir", or "savefile").
title: Dialog title.
must_exist: Whether the path must exist.
**kwargs: Additional keyword arguments.
"""
super().__init__(master, **kwargs)
self.kind = kind # "file" | "dir" | "savefile"
self.title = title
self.must_exist = must_exist
self.var = tk.StringVar()
self.entry = ttk.Entry(self, textvariable=self.var)
self.entry.grid(row=0, column=0, sticky="ew", padx=(0, 4))
self.btn = ttk.Button(self, text="Browse", command=self.browse)
self.btn.grid(row=0, column=1)
self.columnconfigure(0, weight=1)
[docs]
def browse(self):
"""Open file/directory browser dialog."""
if self.kind == "file":
path = filedialog.askopenfilename(title=self.title)
elif self.kind == "savefile":
path = filedialog.asksaveasfilename(title=self.title)
else:
path = filedialog.askdirectory(title=self.title)
if path:
if self.must_exist and not Path(path).exists():
messagebox.showerror("Path not found", f"{path}\n\ndoes not exist.")
return
self.var.set(path)
[docs]
def get(self) -> str:
"""Get the current path value."""
return self.var.get()
[docs]
def set(self, value: str):
"""Set the path value."""
self.var.set(value or "")
[docs]
class ScrollText(ttk.Frame):
"""A Text widget with a vertical scrollbar."""
[docs]
def __init__(self, master, height=10, **kwargs):
"""
Initialize ScrollText widget.
Args:
master: Parent widget.
height: Height of the text widget.
**kwargs: Additional keyword arguments.
"""
super().__init__(master, **kwargs)
self.text = tk.Text(self, wrap="word", height=height,
font=('Segoe UI', 10),
bg='#ffffff',
fg='#2d3748',
selectbackground='#4299e1',
selectforeground='#ffffff',
insertbackground='#2d3748',
borderwidth=1,
relief='solid',
padx=8,
pady=6)
sb = ttk.Scrollbar(self, command=self.text.yview)
self.text.configure(yscrollcommand=sb.set)
self.text.grid(row=0, column=0, sticky="nsew")
sb.grid(row=0, column=1, sticky="ns")
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
[docs]
def write(self, s: str):
"""Write text to the widget."""
self.text.insert("end", s)
self.text.see("end")
[docs]
def clear(self):
"""Clear all text from the widget."""
self.text.delete("1.0", "end")
[docs]
class EditableTreeview(ttk.Frame):
"""Spreadsheet-like table with CSV load/save and inline cell editing (double-click)."""
[docs]
def __init__(self, master, columns, show_headings=True, height=8, non_editable_columns=None, **kwargs):
"""
Initialize EditableTreeview widget.
Args:
master: Parent widget.
columns: List of column names.
show_headings: Whether to show column headings.
height: Height of the treeview.
non_editable_columns: List of columns that cannot be edited.
**kwargs: Additional keyword arguments.
"""
super().__init__(master, **kwargs)
self.columns = columns
self.non_editable_columns = non_editable_columns or []
self.tree = ttk.Treeview(self, columns=columns, show=("headings" if show_headings else ""))
for col in columns:
self.tree.heading(col, text=col)
self.tree.column(col, width=100, anchor="center")
vsb = ttk.Scrollbar(self, command=self.tree.yview)
hsb = ttk.Scrollbar(self, command=self.tree.xview, orient="horizontal")
self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
self.tree.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
hsb.grid(row=1, column=0, sticky="ew")
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self._editor = None
self.tree.bind("<Double-1>", self._begin_edit)
# ---- data helpers ----
[docs]
def clear(self):
"""Clear all rows from the treeview."""
for i in self.tree.get_children():
self.tree.delete(i)
[docs]
def append_row(self, values):
"""Append a row to the treeview."""
# pad/truncate to number of columns
vals = list(values) + [""] * (len(self.columns) - len(values))
vals = vals[:len(self.columns)]
self.tree.insert("", "end", values=vals)
[docs]
def get_all(self):
"""Get all rows from the treeview."""
return [self.tree.item(i, "values") for i in self.tree.get_children()]
# ---- CSV I/O ----
[docs]
def load_csv(self, path):
"""Load data from a CSV file."""
self.clear()
with open(path, newline="") as f:
reader = csv.reader(f)
for row in reader:
# Convert string values to floats if possible
float_row = []
for val in row:
try:
float_row.append(float(val.strip()))
except ValueError:
float_row.append(val) # Keep original value if not a float
self.append_row(float_row)
[docs]
def save_csv(self, path):
"""Save data to a CSV file."""
with open(path, "w", newline="") as f:
writer = csv.writer(f)
for row in self.get_all():
writer.writerow(row)
# ---- inline editing ----
def _begin_edit(self, event):
region = self.tree.identify("region", event.x, event.y)
if region != "cell":
return
row_id = self.tree.identify_row(event.y)
col_id = self.tree.identify_column(event.x)
if not row_id or not col_id:
return
col = int(col_id.replace("#", "")) - 1
# Check if this column is non-editable
if self.columns[col] in self.non_editable_columns:
return
bbox = self.tree.bbox(row_id, col_id)
if not bbox:
return
x, y, w, h = bbox
value = self.tree.set(row_id, self.columns[col])
self._editor = tk.Entry(self.tree,
font=('Segoe UI', 10),
bg='#ffffff',
fg='#2d3748',
selectbackground='#4299e1',
selectforeground='#ffffff',
insertbackground='#2d3748',
borderwidth=1,
relief='solid')
self._editor.insert(0, value)
self._editor.select_range(0, "end")
self._editor.focus()
self._editor.place(x=x, y=y, width=w, height=h)
def _finish(e=None):
new_val = self._editor.get()
self.tree.set(row_id, self.columns[col], new_val)
self._editor.destroy()
self._editor = None
self._editor.bind("<Return>", _finish)
self._editor.bind("<Escape>", lambda e: (self._editor.destroy(), setattr(self, "_editor", None)))
self._editor.bind("<FocusOut>", _finish)