SXXXXXXX_PyUCC/pyucc/gui/profile_manager.py
VALLONGOL 4fdd646d60 Chore: Stop tracking files based on .gitignore update.
Untracked files matching the following rules:
- Rule "*.zip": 1 file
2025-11-24 10:15:59 +01:00

218 lines
8.9 KiB
Python

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from pathlib import Path
from typing import List
from ..config import profiles as profiles_cfg
from ..config.languages import LANGUAGE_EXTENSIONS
class ProfileManager(tk.Toplevel):
"""Dialog window to create/edit/delete scanning profiles.
Usage: pm = ProfileManager(root); pm.wait_window()
"""
COMMON_LANGS = ["Python", "C", "C++", "Java", "JavaScript", "HTML", "Shell"]
# sensible defaults for ignore patterns covering common build/artifact dirs and files
DEFAULT_IGNORES = [
"__pycache__",
"*.pyc",
"*.pyo",
"*.pyd",
".Python",
"env",
"venv",
".venv",
"build",
"dist",
"*.egg-info",
".eggs",
"node_modules",
".git",
".hg",
".svn",
".idea",
".vscode",
".DS_Store",
"*.class",
"*.o",
"*.so",
"*.dylib",
".pytest_cache",
".mypy_cache",
".cache",
"coverage",
".tox",
"pip-wheel-metadata",
]
def __init__(self, parent, on_change=None):
super().__init__(parent)
self.title("Profile Manager")
self.geometry("900x600")
self.on_change = on_change
self.profiles = profiles_cfg.load_profiles()
# Left: list of profiles
left = ttk.Frame(self)
left.pack(side="left", fill="y", padx=8, pady=8)
# Make listbox taller so names are easily visible
self.listbox = tk.Listbox(left, width=30, height=20)
self.listbox.pack(fill="y", expand=True)
for p in self.profiles:
self.listbox.insert("end", p.get("name"))
self.listbox.bind("<<ListboxSelect>>", self._on_select)
# Right: edit form
right = ttk.Frame(self)
right.pack(side="left", fill="both", expand=True, padx=8, pady=8)
ttk.Label(right, text="Profile name:").grid(row=0, column=0, sticky="w", pady=4)
self.name_var = tk.StringVar()
ttk.Entry(right, textvariable=self.name_var, width=48).grid(row=0, column=1, columnspan=2, sticky="ew", pady=4)
# Path: label above large entry spanning full right area
ttk.Label(right, text="Path:").grid(row=1, column=0, sticky="w", pady=(8, 2))
self.path_var = tk.StringVar()
ttk.Entry(right, textvariable=self.path_var, width=80).grid(row=2, column=0, columnspan=2, sticky="ew", pady=2)
ttk.Button(right, text="Browse...", command=self._browse_path).grid(row=2, column=2, sticky="w", padx=4)
ttk.Label(right, text="Languages:").grid(row=3, column=0, sticky="nw", pady=(8, 2))
# Scrollable frame for languages so checkboxes remain readable
langs_container = ttk.Frame(right)
langs_container.grid(row=3, column=1, columnspan=2, sticky="ew", pady=4)
canvas = tk.Canvas(langs_container, height=140)
vsb = ttk.Scrollbar(langs_container, orient="vertical", command=canvas.yview)
inner = ttk.Frame(canvas)
inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=inner, anchor="nw")
canvas.configure(yscrollcommand=vsb.set)
canvas.grid(row=0, column=0, sticky="nsew")
vsb.grid(row=0, column=1, sticky="ns")
langs_container.columnconfigure(0, weight=1)
self.lang_vars = {}
for i, ln in enumerate(self.COMMON_LANGS):
var = tk.BooleanVar()
exts = LANGUAGE_EXTENSIONS.get(ln, [])
exts_label = f" ({', '.join(exts)})" if exts else ""
cb = ttk.Checkbutton(inner, text=ln + exts_label, variable=var)
cb.grid(row=i, column=0, sticky="w", padx=2, pady=2)
self.lang_vars[ln] = var
# Custom languages: label above large text box spanning full width
ttk.Label(right, text="Custom languages (comma-separated):").grid(row=4, column=0, sticky="w", pady=(8, 2))
self.custom_text = tk.Text(right, height=3, wrap="none")
self.custom_text.grid(row=5, column=0, columnspan=3, sticky="nsew", pady=2)
# Ignore patterns: label above large text box with scrollbars
ttk.Label(right, text="Ignore patterns (comma-separated):").grid(row=6, column=0, sticky="w", pady=(8, 2))
ignore_container = ttk.Frame(right)
ignore_container.grid(row=7, column=0, columnspan=3, sticky="nsew", pady=2)
# Text widget with both vertical and horizontal scrollbars so long patterns are readable
self.ignore_text = tk.Text(ignore_container, height=4, wrap="none")
self.ignore_text.grid(row=0, column=0, sticky="nsew")
vsb_ignore = ttk.Scrollbar(ignore_container, orient="vertical", command=self.ignore_text.yview)
vsb_ignore.grid(row=0, column=1, sticky="ns")
hsb_ignore = ttk.Scrollbar(ignore_container, orient="horizontal", command=self.ignore_text.xview)
hsb_ignore.grid(row=1, column=0, columnspan=2, sticky="ew")
self.ignore_text.configure(yscrollcommand=vsb_ignore.set, xscrollcommand=hsb_ignore.set)
ignore_container.columnconfigure(0, weight=1)
ignore_container.rowconfigure(0, weight=1)
# Buttons (place below the large edit boxes)
btn_frame = ttk.Frame(right)
btn_frame.grid(row=8, column=0, columnspan=3, pady=(12, 0))
ttk.Button(btn_frame, text="New", command=self._new).grid(row=0, column=0, padx=4)
ttk.Button(btn_frame, text="Save", command=self._save).grid(row=0, column=1, padx=4)
ttk.Button(btn_frame, text="Delete", command=self._delete).grid(row=0, column=2, padx=4)
ttk.Button(btn_frame, text="Close", command=self.destroy).grid(row=0, column=3, padx=4)
right.columnconfigure(1, weight=1)
# If no profiles exist, prefill form with sensible defaults (widgets are ready)
if not self.profiles:
self._new()
def _browse_path(self):
d = filedialog.askdirectory()
if d:
self.path_var.set(str(Path(d)))
def _on_select(self, _evt=None):
sel = self.listbox.curselection()
if not sel:
return
idx = sel[0]
pr = self.profiles[idx]
self._load_profile(pr)
def _load_profile(self, pr):
self.name_var.set(pr.get("name", ""))
self.path_var.set(pr.get("path", ""))
langs = pr.get("languages", []) or []
for ln, var in self.lang_vars.items():
var.set(ln in langs)
custom = ",".join([l for l in langs if l not in self.COMMON_LANGS])
# populate custom_text (large edit box)
self.custom_text.delete("1.0", "end")
if custom:
self.custom_text.insert("1.0", custom)
# populate ignore_text
self.ignore_text.delete("1.0", "end")
ignores = ",".join(pr.get("ignore", []))
if ignores:
self.ignore_text.insert("1.0", ignores)
def _new(self):
self.name_var.set("")
self.path_var.set("")
for var in self.lang_vars.values():
var.set(False)
self.custom_text.delete("1.0", "end")
self.ignore_text.delete("1.0", "end")
# insert the DEFAULT_IGNORES joined by commas
self.ignore_text.insert("1.0", ",".join(self.DEFAULT_IGNORES))
def _save(self):
name = self.name_var.get().strip()
if not name:
messagebox.showwarning("Validation", "Profile must have a name.")
return
path = self.path_var.get().strip()
langs: List[str] = [ln for ln, var in self.lang_vars.items() if var.get()]
raw_custom = self.custom_text.get("1.0", "end").strip()
raw_custom = raw_custom.replace("\n", ",")
custom = [c.strip() for c in raw_custom.split(",") if c.strip()]
langs.extend([c for c in custom if c])
raw_ignore = self.ignore_text.get("1.0", "end").strip()
raw_ignore = raw_ignore.replace("\n", ",")
ignore = [s.strip() for s in raw_ignore.split(",") if s.strip()]
profile = {"name": name, "path": path, "languages": langs, "ignore": ignore}
profiles_cfg.add_or_update_profile(profile)
self.profiles = profiles_cfg.load_profiles()
# refresh listbox
self.listbox.delete(0, "end")
for p in self.profiles:
self.listbox.insert("end", p.get("name"))
messagebox.showinfo("Saved", f"Profile '{name}' saved.")
if self.on_change:
self.on_change()
def _delete(self):
name = self.name_var.get().strip()
if not name:
return
if not messagebox.askyesno("Delete", f"Delete profile '{name}'?"):
return
profiles_cfg.delete_profile(name)
self.profiles = profiles_cfg.load_profiles()
self.listbox.delete(0, "end")
for p in self.profiles:
self.listbox.insert("end", p.get("name"))
self._new()
if self.on_change:
self.on_change()