224 lines
8.8 KiB
Python
224 lines
8.8 KiB
Python
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()
|