SXXXXXXX_CodeBridge/codebridge/gui/profile_dialog.py
2025-12-23 10:32:22 +01:00

224 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import json
from typing import Dict, Any
class ProfileManagerFrame(ttk.Frame):
"""Embeddable frame to create/edit/delete folder profiles.
Can be placed inside the main window (centered) or inside a Toplevel.
"""
def __init__(self, parent, profiles_path: str, profiles: Dict[str, Any]):
super().__init__(parent, padding=8, relief="raised")
self.parent = parent
self.profiles_path = profiles_path
self.profiles = profiles.copy()
self._setup_ui()
self._populate_list()
def _setup_ui(self):
left = ttk.Frame(self, padding=8)
left.grid(row=0, column=0, sticky="ns")
ttk.Label(left, text="Profiles").pack(anchor="w")
self.listbox = tk.Listbox(left, width=30, height=20)
self.listbox.pack(fill="y", expand=True)
self.listbox.bind("<<ListboxSelect>>", self._on_select)
right = ttk.Frame(self, padding=8)
right.grid(row=0, column=1, sticky="nsew")
# Name (required)
ttk.Label(right, text="Name (required)").grid(row=0, column=0, sticky="w")
self.name_var = tk.StringVar()
self.name_entry = ttk.Entry(right, textvariable=self.name_var)
self.name_entry.grid(row=0, column=1, sticky="ew", pady=2)
# Description (optional)
ttk.Label(right, text="Description").grid(row=1, column=0, sticky="w")
self.desc_var = tk.StringVar()
self.desc_entry = ttk.Entry(right, textvariable=self.desc_var)
self.desc_entry.grid(row=1, column=1, sticky="ew", pady=2)
# Source folder
ttk.Label(right, text="Source folder").grid(row=2, column=0, sticky="w")
self.src_var = tk.StringVar()
src_row = ttk.Frame(right)
src_row.grid(row=2, column=1, sticky="ew", pady=2)
self.src_entry = ttk.Entry(src_row, textvariable=self.src_var)
self.src_entry.pack(side="left", fill="x", expand=True)
ttk.Button(src_row, text="📁 Browse", command=self._browse_src).pack(side="left", padx=4)
# Destination folder
ttk.Label(right, text="Destination folder").grid(row=3, column=0, sticky="w")
self.dst_var = tk.StringVar()
dst_row = ttk.Frame(right)
dst_row.grid(row=3, column=1, sticky="ew", pady=2)
self.dst_entry = ttk.Entry(dst_row, textvariable=self.dst_var)
self.dst_entry.pack(side="left", fill="x", expand=True)
ttk.Button(dst_row, text="📁 Browse", command=self._browse_dst).pack(side="left", padx=4)
# Ignore extensions (multi-line text for more space)
ttk.Label(right, text="Ignore extensions (comma-separated)").grid(row=4, column=0, sticky="nw")
ignore_row = ttk.Frame(right)
ignore_row.grid(row=4, column=1, sticky="ew", pady=2)
self.ignore_text = tk.Text(ignore_row, height=3, width=60)
self.ignore_text.pack(side="left", fill="both", expand=True)
# Buttons
btn_row = ttk.Frame(right)
btn_row.grid(row=5, column=0, columnspan=2, pady=12, sticky="ew")
ttk.Button(btn_row, text=" New", command=self._new).pack(side="left", padx=4)
ttk.Button(btn_row, text="💾 Save", command=self._save).pack(side="left", padx=4)
ttk.Button(btn_row, text="🗑️ Delete", command=self._delete).pack(side="left", padx=4)
ttk.Button(btn_row, text="Insert common", command=self._insert_common_extensions).pack(side="left", padx=4)
ttk.Button(btn_row, text="✖ Close", command=self._on_close_requested).pack(side="right", padx=4)
right.columnconfigure(1, weight=1)
self.columnconfigure(1, weight=1)
# --- Internal helpers ---
def _populate_list(self):
self.listbox.delete(0, "end")
for name in sorted(self.profiles.keys()):
self.listbox.insert("end", name)
def _on_select(self, _evt=None):
sel = self.listbox.curselection()
if not sel:
return
name = self.listbox.get(sel[0])
p = self.profiles.get(name, {})
self.name_var.set(name)
self.desc_var.set(p.get("description", ""))
self.src_var.set(p.get("source", ""))
self.dst_var.set(p.get("destination", ""))
# load ignore extensions
ign = p.get("ignore_extensions")
if isinstance(ign, (list, tuple)):
self.ignore_text.delete("1.0", "end")
self.ignore_text.insert("1.0", ",".join([e.lstrip('.') for e in ign]))
else:
self.ignore_text.delete("1.0", "end")
self.ignore_text.insert("1.0", ign or "")
def _browse_src(self):
parent = self.winfo_toplevel()
d = filedialog.askdirectory(parent=parent)
if d:
self.src_var.set(d)
def _browse_dst(self):
parent = self.winfo_toplevel()
d = filedialog.askdirectory(parent=parent)
if d:
self.dst_var.set(d)
def _new(self):
self.listbox.selection_clear(0, "end")
self.name_var.set("")
self.desc_var.set("")
self.src_var.set("")
self.dst_var.set("")
self.ignore_text.delete("1.0", "end")
self.name_entry.focus()
def _save(self):
name = self.name_var.get().strip()
if not name:
messagebox.showwarning("Validation", "Profile name is required.", parent=self.winfo_toplevel())
return
profile = {
"description": self.desc_var.get().strip(),
"source": self.src_var.get().strip(),
"destination": self.dst_var.get().strip()
}
# parse ignore extensions from comma-separated input
raw = self.ignore_text.get("1.0", "end").strip()
if raw:
parts = [p.strip() for p in raw.split(",") if p.strip()]
# normalize to start with dot
norm = []
for p in parts:
if not p.startswith("."):
p = "." + p
norm.append(p)
profile["ignore_extensions"] = norm
else:
profile["ignore_extensions"] = []
self.profiles[name] = profile
try:
with open(self.profiles_path, "w", encoding="utf-8") as f:
json.dump(self.profiles, f, indent=2)
except Exception as e:
messagebox.showerror("Error", f"Failed to save profiles: {e}", parent=self.winfo_toplevel())
return
self._populate_list()
messagebox.showinfo("Saved", "Profile saved.", parent=self.winfo_toplevel())
def _delete(self):
name = self.name_var.get().strip()
if not name:
return
if name not in self.profiles:
messagebox.showwarning("Not found", "Profile not found.", parent=self.winfo_toplevel())
return
if not messagebox.askyesno("Confirm", f"Delete profile '{name}'?", parent=self.winfo_toplevel()):
return
del self.profiles[name]
try:
with open(self.profiles_path, "w", encoding="utf-8") as f:
json.dump(self.profiles, f, indent=2)
except Exception as e:
messagebox.showerror("Error", f"Failed to save profiles: {e}", parent=self.winfo_toplevel())
return
self._populate_list()
self._new()
def _insert_common_extensions(self):
# common list without dots (entry convenience)
common = ",".join([
"o", "d", "obj", "class", "pyc", "pyo", "log", "tmp", "swp", "DS_Store",
"exe", "a", "mk", "bak"
])
self.ignore_text.delete("1.0", "end")
self.ignore_text.insert("1.0", common)
def _on_close_requested(self):
# emit a virtual event so parent can handle closing/hiding
try:
self.event_generate("<<ProfileManagerClose>>")
except Exception:
pass
class ProfileManagerDialog(tk.Toplevel):
"""Backward-compatible Toplevel wrapper using the embeddable frame."""
def __init__(self, parent, profiles_path: str, profiles: Dict[str, Any]):
super().__init__(parent)
self.parent = parent
self.title("Manage Profiles")
desired_w, desired_h = 760, 420
self.geometry(f"{desired_w}x{desired_h}")
frame = ProfileManagerFrame(self, profiles_path, profiles)
frame.pack(fill="both", expand=True)
frame.bind("<<ProfileManagerClose>>", lambda e: self.destroy())
self.transient(parent)
# center over parent window
try:
self.update_idletasks()
px = parent.winfo_rootx()
py = parent.winfo_rooty()
pw = parent.winfo_width()
ph = parent.winfo_height()
x = px + max(0, (pw - desired_w) // 2)
y = py + max(0, (ph - desired_h) // 2)
self.geometry(f"{desired_w}x{desired_h}+{x}+{y}")
except Exception:
# fallback: keep default geometry
pass
self.grab_set()