import os
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import pandas as pd
from datetime import datetime
import re
from openpyxl.styles import Border, Side, Alignment
class NameValidatorApp:
def __init__(self, root):
self.root = root
self.root.title("Name Validator")
w, h = 900, 550
self.root.geometry(f"{w}x{h}+{int((root.winfo_screenwidth()-w)/2)}+{int((root.winfo_screenheight()-h)/2)}")
# UI Elements
frame_top = tk.Frame(root)
frame_top.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)
# Template selection
self.template_path = tk.StringVar()
tk.Label(frame_top, text="Default Template:").grid(row=0, column=0, sticky="w")
tk.Entry(frame_top, textvariable=self.template_path, width=75).grid(row=0, column=1, padx=5)
tk.Button(frame_top, text="Browse", command=self.browse_template).grid(row=0, column=2, padx=2)
# Buttons (Unified size and alignment)
btn_width = 18
self.exec_btn = tk.Button(frame_top, text="Execute", command=self.start_execution, width=btn_width, pady=2, bg="#d4edda")
self.exec_btn.grid(row=0, column=3, padx=10, sticky="e")
# Study folder selection
self.study_folder = tk.StringVar()
tk.Label(frame_top, text="Study Folder:").grid(row=1, column=0, sticky="w", pady=5)
tk.Entry(frame_top, textvariable=self.study_folder, width=75).grid(row=1, column=1, padx=5, pady=5)
tk.Button(frame_top, text="Browse", command=self.browse_study_folder).grid(row=1, column=2, pady=5, padx=2)
# Cleanup button (same size as Execute)
tk.Button(frame_top, text="Cleanup Old Results", command=self.cleanup_old, width=btn_width, pady=2, bg="#f8d7da").grid(row=1, column=3, padx=10, sticky="e")
self.status_var = tk.StringVar(value="Ready")
tk.Label(frame_top, textvariable=self.status_var, fg="blue").grid(row=2, column=0, columnspan=4, pady=5)
# Table for results
frame_mid = tk.Frame(root)
frame_mid.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=5)
# UI Styling for Treeview Borders/Lines
style = ttk.Style()
# Ensure grid lines are visible if the theme supports it
style.theme_use('clam') # 'clam' usually supports grid lines better than others
style.configure("Treeview",
background="#ffffff",
foreground="black",
rowheight=25,
fieldbackground="#ffffff",
borderwidth=1)
style.map("Treeview", background=[('selected', '#347083')])
# Configure the headings
style.configure("Treeview.Heading", font=('Calibri', 10, 'bold'))
self.cols = ("Parent_Folder", "Folder path", "page_Name", "Page Order")
self.tree = ttk.Treeview(frame_mid, columns=self.cols, show="headings", selectmode="browse")
for c in self.cols:
display_name = c.replace("_", " ").title()
self.tree.heading(c, text=display_name)
# Default widths
if c == "Page Order":
self.tree.column(c, width=70, stretch=False, anchor="center")
elif c == "Folder path":
self.tree.column(c, width=350, stretch=True)
else:
self.tree.column(c, width=150, stretch=False)
sb = ttk.Scrollbar(frame_mid, orient="vertical", command=self.tree.yview)
self.tree.configure(yscrollcommand=sb.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
sb.pack(side=tk.RIGHT, fill=tk.Y)
# Activity Log
frame_bot = tk.Frame(root)
frame_bot.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=5)
tk.Label(frame_bot, text="Activity Log:").pack(anchor="w")
self.log_text = tk.Text(frame_bot, height=8, state='disabled')
self.log_text.pack(fill=tk.X)
def log(self, msg):
self.log_text.config(state='normal')
self.log_text.insert(tk.END, f"[{datetime.now().strftime('%H:%M:%S')}] {msg}\n")
self.log_text.see(tk.END)
self.log_text.config(state='disabled')
def browse_template(self):
filename = filedialog.askopenfilename(filetypes=[("Text files", "*Pages.txt"), ("All files", "*.*")])
if filename:
self.template_path.set(filename)
def browse_study_folder(self):
folder = filedialog.askdirectory()
if folder:
self.study_folder.set(folder)
def cleanup_old(self):
count = 0
try:
curr_dir = os.path.dirname(os.path.abspath(__file__))
for f in os.listdir(curr_dir):
if f.startswith("Validation_Results_") and f.endswith(".xlsx"):
os.remove(os.path.join(curr_dir, f))
count += 1
self.log(f"Deleted {count} old result files.")
messagebox.showinfo("Cleanup", f"Deleted {count} old result files.")
except Exception as e:
self.log(f"Cleanup Error: {e}")
messagebox.showerror("Error", str(e))
def start_execution(self):
if not self.template_path.get() or not self.study_folder.get():
messagebox.showwarning("Warning", "Please select both template file and study folder.")
return
[self.tree.delete(i) for i in self.tree.get_children()]
threading.Thread(target=self.execute_logic, daemon=True).start()
def extract_page_info(self, line):
# Example: Page: Core Setup Order: 0 ...
try:
if "Page:" in line and "Order:" in line:
page_name = line.split("Page:")[1].split("Order:")[0].strip()
# Order can be followed by spaces and numbers, and then "Visible:"
order_part = line.split("Order:")[1].strip()
order_match = re.search(r'^(\d+)', order_part)
page_order = order_match.group(1) if order_match else "N/A"
return page_name, page_order
except Exception:
pass
return None, None
def execute_logic(self):
self.exec_btn.config(state=tk.DISABLED)
self.status_var.set("Processing...")
self.log("Starting validation...")
standard_Names = set()
results = []
try:
# 1. Read Template
self.log(f"Reading template: {os.path.basename(self.template_path.get())}")
with open(self.template_path.get(), 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
p_name, _ = self.extract_page_info(line)
if p_name:
standard_Names.add(p_name)
self.log(f"Found {len(standard_Names)} standard page names.")
# 2. Search Study Folder
study_root = self.study_folder.get()
for root, _, files in os.walk(study_root):
for file in files:
if file.lower().endswith("pages.txt"):
fpath = os.path.join(root, file)
parent_folder = os.path.basename(root)
self.log(f"Processing study file: {fpath}")
try:
with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
p_name, p_order = self.extract_page_info(line)
if p_name and p_name not in standard_Names:
row = (parent_folder, fpath, p_name, p_order)
results.append(row)
self.root.after(0, lambda r=row: self.tree.insert("", tk.END, values=r))
except Exception as e:
self.log(f"Error reading {file}: {e}")
if results:
self.root.after(0, self.auto_resize_columns)
self.save_excel(results)
self.status_var.set("Validation Complete - Results Saved")
else:
self.log("No validation mismatches found.")
self.status_var.set("Validation Complete - All names valid")
messagebox.showinfo("Complete", "Validation finished. No mismatches found.")
except Exception as e:
self.log(f"Execution Error: {e}")
messagebox.showerror("Error", str(e))
self.status_var.set("Error")
finally:
self.exec_btn.config(state=tk.NORMAL)
def auto_resize_columns(self):
# Dictionary to store max length for each column
# Initialize with header lengths
max_lens = {c: len(c.replace("_", " ")) for c in self.cols}
# Iterate over all rows in the treeview
for child in self.tree.get_children():
values = self.tree.item(child)["values"]
for i, c in enumerate(self.cols):
val_len = len(str(values[i]))
if val_len > max_lens[c]:
max_lens[c] = val_len
# Adjust column widths
for c in self.cols:
if c == "Page Order":
# Fixed 80 px for ~8 digits
self.tree.column(c, width=80, stretch=False)
elif c == "Folder path":
# Let Folder Path expand to fill space
self.tree.column(c, width=200, stretch=True)
else:
# Calculate width in pixels roughly (len * 8px + safety margin)
new_width = max_lens[c] * 8 + 20
self.tree.column(c, width=new_width, stretch=False)
def save_excel(self, data):
try:
self.log("Exporting to Excel...")
df = pd.DataFrame(data, columns=["Parent_Folder", "Folder path", "page_Name", "Page Order"])
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
out_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"Validation_Results_{timestamp}.xlsx")
with pd.ExcelWriter(out_file, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Validation Results', index=False)
ws = writer.sheets['Validation Results']
thin = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin'))
# Formatting
for col in ws.columns:
# Calculate max length for column width
max_len = 0
column = col[0].column_letter
for cell in col:
try:
if cell.value:
max_len = max(max_len, len(str(cell.value)))
except:
pass
ws.column_dimensions[column].width = min(max_len + 5, 100) # Cap width at 100
for cell in col:
cell.border = thin
if cell.row == 1:
cell.alignment = Alignment(horizontal='center', vertical='center')
# Make header bold (optional, let's keep it consistent with VersionScanner)
self.log(f"Results saved to: {out_file}")
messagebox.showinfo("Success", f"Validation complete. Results exported to:\n{out_file}")
except Exception as e:
self.log(f"Export Error: {e}")
messagebox.showerror("Export Error", str(e))
if __name__ == "__main__":
root = tk.Tk()
app = NameValidatorApp(root)
root.mainloop()