178 lines
6.6 KiB
Python
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()
|