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("<>", 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("<>") 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("<>", 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()