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