SXXXXXXX_PyUCC/pyucc/gui/dialogs.py

178 lines
6.6 KiB
Python

import tkinter as tk
from tkinter import ttk, messagebox
from typing import Optional
class DuplicatesDialog(tk.Toplevel):
"""Modal dialog to request duplicates search parameters.
Returns result via the `result` attribute (dict) or None if cancelled.
"""
def __init__(
self, parent, initial: Optional[dict] = None, allow_edit_extensions: bool = True
):
super().__init__(parent)
self.transient(parent)
self.grab_set()
self.title("Duplicates search parameters")
self.parent = parent
self.result = None
init = initial or {}
# Defaults
threshold = init.get("threshold", 5.0)
exts = init.get("extensions")
k = init.get("k", 25)
window = init.get("window", 4)
frm = ttk.Frame(self)
frm.pack(padx=12, pady=12, fill="both", expand=True)
# Threshold
ttk.Label(frm, text="Max percent changed lines (dup threshold):").grid(
row=0, column=0, sticky="w"
)
self.threshold_var = tk.StringVar(value=str(threshold))
self.threshold_entry = ttk.Entry(frm, textvariable=self.threshold_var, width=12)
self.threshold_entry.grid(row=0, column=1, sticky="w", padx=(8, 0))
# Extensions (comma separated)
ttk.Label(
frm, text="Extensions to include (comma separated, e.g. .py,.c):"
).grid(row=1, column=0, sticky="w", pady=(8, 0))
exts_str = ",".join(exts) if exts else ""
self.exts_var = tk.StringVar(value=exts_str)
self.allow_edit_extensions = bool(allow_edit_extensions)
if self.allow_edit_extensions:
self.exts_entry = ttk.Entry(frm, textvariable=self.exts_var, width=40)
self.exts_entry.grid(row=1, column=1, sticky="w", padx=(8, 0), pady=(8, 0))
else:
# show as read-only label and hint it's provided by profile
lbl = ttk.Label(frm, text=exts_str or "(none)")
lbl.grid(row=1, column=1, sticky="w", padx=(8, 0), pady=(8, 0))
hint = ttk.Label(
frm, text="(Extensions fixed by selected profile)", foreground="#555555"
)
hint.grid(row=2, column=1, sticky="w", padx=(8, 0), pady=(0, 4))
# move numeric fields down by one row
# adjust subsequent widget grid rows below
# We'll shift k/window placement by one
k_row = 3
window_row = 4
# Fingerprint params
if self.allow_edit_extensions:
k_row = 2
window_row = 3
ttk.Label(frm, text="Fingerprint k (k-gram size):").grid(
row=k_row, column=0, sticky="w", pady=(8, 0)
)
self.k_var = tk.StringVar(value=str(k))
self.k_entry = ttk.Entry(frm, textvariable=self.k_var, width=8)
self.k_entry.grid(row=k_row, column=1, sticky="w", padx=(8, 0), pady=(8, 0))
ttk.Label(frm, text="Winnowing window size:").grid(
row=window_row, column=0, sticky="w", pady=(8, 0)
)
self.window_var = tk.StringVar(value=str(window))
self.window_entry = ttk.Entry(frm, textvariable=self.window_var, width=8)
self.window_entry.grid(
row=window_row, column=1, sticky="w", padx=(8, 0), pady=(8, 0)
)
# Buttons
btn_frame = ttk.Frame(self)
btn_frame.pack(fill="x", padx=12, pady=(0, 12))
ok_btn = ttk.Button(btn_frame, text="✅ OK", command=self._on_ok)
ok_btn.pack(side="right", padx=(0, 8))
cancel_btn = ttk.Button(btn_frame, text="❌ Cancel", command=self._on_cancel)
cancel_btn.pack(side="right")
# make dialog centered
self.update_idletasks()
try:
pw = parent.winfo_width()
ph = parent.winfo_height()
px = parent.winfo_rootx()
py = parent.winfo_rooty()
dw = self.winfo_reqwidth()
dh = self.winfo_reqheight()
x = px + (pw - dw) // 2
y = py + (ph - dh) // 2
self.geometry(f"+{x}+{y}")
except Exception:
pass
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
self.threshold_entry.focus_set()
# Legend / explanation
expl = (
"Legend:\n"
"- Threshold: maximum percent of changed lines to still consider two files duplicates. Lower = stricter.\n"
"- Extensions: file extensions to scan. If fixed by the selected profile, you cannot edit them here.\n"
"- Fingerprint k: k-gram size used to build fingerprints (larger k = less sensitive to small changes).\n"
"- Winnowing window: window size used by the winnowing algorithm (controls density of fingerprints)."
)
try:
# place explanation under the form (non-editable label)
expl_lbl = ttk.Label(self, text=expl, justify="left", foreground="#333333")
expl_lbl.pack(fill="x", padx=12, pady=(0, 8))
except Exception:
pass
def _on_ok(self):
# validate threshold
try:
thr = float(self.threshold_var.get())
if thr < 0 or thr > 100:
raise ValueError()
except Exception:
messagebox.showwarning(
self, "Invalid value", "Threshold must be a number between 0 and 100"
)
return
# parse extensions
exts_raw = self.exts_var.get().strip()
exts = None
if exts_raw:
parts = [p.strip() for p in exts_raw.split(",") if p.strip()]
normalized = []
for p in parts:
if not p.startswith("."):
p = "." + p
normalized.append(p.lower())
exts = normalized
# k and window with safe ranges
try:
k = int(self.k_var.get())
if k < 3 or k > 100:
raise ValueError()
except Exception:
messagebox.showwarning(
self, "Invalid value", "k must be an integer between 3 and 100"
)
return
try:
w = int(self.window_var.get())
if w < 1 or w > 100:
raise ValueError()
except Exception:
messagebox.showwarning(
self, "Invalid value", "window must be an integer between 1 and 100"
)
return
self.result = {
"threshold": float(thr),
"extensions": exts,
"k": int(k),
"window": int(w),
}
self.destroy()
def _on_cancel(self):
self.result = None
self.destroy()