import tkinter as tk from tkinter import ttk, messagebox, filedialog from pathlib import Path import threading import queue import csv from datetime import datetime import tkinter as tk from tkinter import ttk, messagebox, filedialog from pathlib import Path import threading import queue import csv from datetime import datetime from ..core.scanner import find_source_files from ..config.languages import LANGUAGE_EXTENSIONS from ..utils import logger as app_logger import logging from .file_viewer import FileViewer class ScannerTab(ttk.Frame): """Tab that runs the source files scanner and shows results. The ScannerTab relies on a shared `topbar` instance for folder selection. This reduces duplication between tabs. """ def __init__(self, parent, topbar, app=None, *args, **kwargs): """Initialize the ScannerTab. Args: parent: Notebook widget that contains this tab. topbar: Instance of `TopBar` exposing `path_var`. """ super().__init__(parent, *args, **kwargs) self.topbar = topbar self.app = app self.queue = queue.Queue() self.worker = None # Controls controls = ttk.Frame(self) controls.grid(row=0, column=0, sticky="ew", padx=8, pady=6) self.scan_btn = ttk.Button(controls, text="Start Scan", command=self.start_scan) self.scan_btn.grid(row=0, column=0, sticky="w") self.cancel_btn = ttk.Button(controls, text="Cancel", command=self.cancel_scan, state="disabled") self.cancel_btn.grid(row=0, column=1, padx=(8, 0)) # Export button next to scanner controls self.export_btn = ttk.Button(controls, text="Export CSV", command=self._export_csv, state="disabled") self.export_btn.grid(row=0, column=2, padx=(8, 0)) # Progress and output self.progress = ttk.Progressbar(self, mode="indeterminate") self.progress.grid(row=1, column=0, sticky="ew", padx=8, pady=(6, 0)) # Treeview for scanner results: filename, path, size, created, modified columns = ("name", "path", "size", "created", "modified") self.tree = ttk.Treeview(self, columns=columns, show="headings") self.tree.heading("name", text="File", command=lambda: self._sort_by("name", False)) self.tree.heading("path", text="Path", command=lambda: self._sort_by("path", False)) self.tree.heading("size", text="Size (bytes)", command=lambda: self._sort_by("size", False)) self.tree.heading("created", text="Created", command=lambda: self._sort_by("created", False)) self.tree.heading("modified", text="Modified", command=lambda: self._sort_by("modified", False)) # column sizing: make numeric columns narrower and right-aligned self.tree.column("name", width=300, anchor="w", stretch=True) self.tree.column("path", width=400, anchor="w", stretch=True) self.tree.column("size", width=90, anchor="e", stretch=False) self.tree.column("created", width=160, anchor="w", stretch=False) self.tree.column("modified", width=160, anchor="w", stretch=False) self.tree.grid(row=2, column=0, sticky="nsew", padx=8, pady=8) vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview) hs = ttk.Scrollbar(self, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hs.set) vsb.grid(row=2, column=1, sticky="ns") hs.grid(row=3, column=0, columnspan=1, sticky="ew", padx=8) self.rowconfigure(2, weight=1) self.columnconfigure(0, weight=1) # bind double-click to open file viewer self.tree.bind("", self._on_double_click) def start_scan(self): """Start scanning the folder configured in the shared TopBar.""" path = self.topbar.path_var.get().strip() if not path: messagebox.showwarning("Missing folder", "Please select a folder to analyze first.") return directory = Path(path) if not directory.is_dir(): messagebox.showerror("Error", f"The path '{path}' is not a valid folder.") return self.scan_btn.config(state="disabled") # log start try: if getattr(self, 'app', None): self.app.log(f"Scan started on: {path}", "INFO") except Exception: pass self.cancel_btn.config(state="normal") # clear tree for item in self.tree.get_children(): self.tree.delete(item) self.progress.start(50) self.worker = threading.Thread(target=self._worker_scan, args=(directory,), daemon=True) self.worker.start() self.after(200, self._poll_queue) def cancel_scan(self): """Request cancellation (informational only — scanner must support cancel).""" if self.worker and self.worker.is_alive(): messagebox.showinfo("Cancel", "Cancellation requested but not supported by the worker.") try: if getattr(self, 'app', None): self.app.log("Scan cancelled by user", "WARNING") except Exception: pass self._finish_scan() def _worker_scan(self, directory: Path): """Worker thread: call `find_source_files` and push results to queue. If a profile is selected in the TopBar, build an allowed extensions set from the profile languages and pass it to the scanner so only relevant files are returned. """ try: allowed_exts = None pr = getattr(self.topbar, "current_profile", None) # fallback: if current_profile not set, try to resolve from combobox selection if not pr: pname = getattr(self.topbar, "profile_var", None) if pname: from ..config import profiles as profiles_cfg pr = profiles_cfg.find_profile(pname.get()) if hasattr(pname, "get") else profiles_cfg.find_profile(str(pname)) if pr: langs = pr.get("languages", []) or [] exts = [] for ln in langs: if ln in LANGUAGE_EXTENSIONS: exts.extend(LANGUAGE_EXTENSIONS[ln]) else: val = ln.strip() if val.startswith('.'): exts.append(val.lower()) elif len(val) <= 5 and val.isalnum(): # simple heuristic: treat short alnum tokens as extension-like exts.append(f".{val.lower()}") else: pass if exts: allowed_exts = set(exts) ignore_patterns = pr.get("ignore", []) if pr else None files = find_source_files(directory, allowed_extensions=allowed_exts, ignore_patterns=ignore_patterns) self.queue.put(("done", files)) except Exception as e: self.queue.put(("error", str(e))) def _poll_queue(self): """Poll the internal queue and update UI when results arrive.""" try: while True: item = self.queue.get_nowait() tag, payload = item if tag == "done": files = payload # populate tree with file metadata for p in files: try: stat = p.stat() size = stat.st_size created = datetime.fromtimestamp(stat.st_ctime).isoformat(sep=' ', timespec='seconds') modified = datetime.fromtimestamp(stat.st_mtime).isoformat(sep=' ', timespec='seconds') except Exception: size = 0 created = "" modified = "" self.tree.insert("", "end", values=(p.name, str(p), size, created, modified)) self.export_btn.config(state="normal") try: if getattr(self, 'app', None): self.app.log(f"Scan completed: {len(files)} files found", "INFO") except Exception: pass self._finish_scan() elif tag == "error": messagebox.showerror("Error", f"Error during scanning: {payload}") try: if getattr(self, 'app', None): self.app.log(f"Error during scanning: {payload}", "ERROR") except Exception: pass self._finish_scan() except queue.Empty: if self.worker and self.worker.is_alive(): self.after(200, self._poll_queue) def _on_double_click(self, _evt=None): sel = self.tree.selection() if not sel: return item = sel[0] vals = self.tree.item(item, "values") # values: (name, path, size, created, modified) if len(vals) >= 2: path = vals[1] FileViewer(self, path) def _finish_scan(self): """Finalize UI state after scan finished or cancelled.""" self.progress.stop() self.scan_btn.config(state="normal") self.cancel_btn.config(state="disabled") def _sort_by(self, col, descending): col_map = {"name": 0, "path": 1, "size": 2, "created": 3, "modified": 4} idx = col_map.get(col, 0) children = list(self.tree.get_children("")) def _key(item): val = self.tree.item(item, "values")[idx] # numeric for size if col == "size": try: return int(val) except Exception: return 0 # otherwise string try: return str(val).lower() except Exception: return "" children.sort(key=_key, reverse=descending) for index, item in enumerate(children): self.tree.move(item, "", index) self.tree.heading(col, command=lambda c=col: self._sort_by(c, not descending)) def _export_csv(self): path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files", "*.csv"), ("All files", "*")]) if not path: return headers = ["name", "path", "size", "created", "modified"] from ..utils.csv_exporter import export_rows_to_csv try: rows = (self.tree.item(child, "values") for child in self.tree.get_children()) written = export_rows_to_csv(path, headers, rows) messagebox.showinfo("Export", f"Exported {written} rows to {path}") try: if getattr(self, 'app', None): self.app.log(f"Exported {written} rows to {path}", "INFO") except Exception: pass except Exception as e: messagebox.showerror("Export Error", str(e)) try: if getattr(self, 'app', None): self.app.log(f"Export error: {e}", "ERROR") except Exception: pass