import tkinter as tk from tkinter import ttk, filedialog, messagebox import threading from pathlib import Path from .profile_manager import ProfileManager from ..config import profiles as profiles_cfg from ..config import settings as app_settings from ..core.differ import BaselineManager, Differ class TopBar(ttk.Frame): """Shared top bar containing profile selection and folder selection. The TopBar exposes `path_var` (StringVar) and `current_profile` dict that other tabs can read to apply profile-specific settings. """ def __init__(self, parent, *args, **kwargs): """Initialize the TopBar. Args: parent: The parent Tk widget where the top bar will be placed. """ super().__init__(parent, *args, **kwargs) self.path_var = tk.StringVar() self.current_profile = None # Profiles combobox ttk.Label(self, text="Profile:").grid(row=0, column=0, sticky="w", padx=(8, 4), pady=8) self.profile_var = tk.StringVar() self.profile_cb = ttk.Combobox(self, textvariable=self.profile_var, state="readonly") self._load_profiles() self.profile_cb.grid(row=0, column=1, sticky="ew", padx=(0, 6)) self.profile_cb.bind("<>", self._on_profile_selected) manage_btn = ttk.Button(self, text="Manage...", command=self._open_manager) manage_btn.grid(row=0, column=2, sticky="w", padx=(4, 4)) settings_btn = ttk.Button(self, text="Settings...", command=self._open_settings) settings_btn.grid(row=0, column=3, sticky="w", padx=(4, 4)) # Note: Differing action moved to the main Actions bar in the GUI # Info area: project type above folder label (read-only, driven by profile) info = ttk.Frame(self) info.grid(row=0, column=4, columnspan=2, sticky="ew", padx=(8, 8)) ttk.Label(info, text="Type:").grid(row=0, column=0, sticky="w") self.project_type_var = tk.StringVar(value="-") ttk.Label(info, textvariable=self.project_type_var).grid(row=0, column=1, sticky="w", padx=(6, 0)) # Folder display removed: profiles can contain multiple paths now self.columnconfigure(1, weight=0) self.columnconfigure(4, weight=1) def _load_profiles(self): profs = profiles_cfg.load_profiles() names = [p.get("name") for p in profs] self.profile_cb["values"] = names def _on_profile_selected(self, _evt=None): name = self.profile_var.get() if not name: return pr = profiles_cfg.find_profile(name) if not pr: return self.current_profile = pr # Set folder and optionally other UI hints # prefer new 'paths' list (no legacy compatibility) paths = pr.get("paths") or [] first = paths[0] if paths else "" self.path_var.set(first) # determine a simple project type hint from profile languages langs = pr.get("languages", []) or [] ptype = "" if "Python" in langs: ptype = "Python" elif "C++" in langs or "C" in langs: ptype = "C/C++" elif "Java" in langs: ptype = "Java" elif len(langs) == 1: ptype = langs[0] elif langs: ptype = ",".join(langs) else: ptype = "Unknown" self.project_type_var.set(ptype) def _open_manager(self): def _refresh(): self._load_profiles() pm = ProfileManager(self.master, on_change=_refresh) pm.grab_set() def _open_settings(self): try: from .settings_dialog import SettingsDialog dlg = SettingsDialog(self.master) dlg.grab_set() except Exception as e: try: messagebox.showerror("Settings", f"Failed to open settings: {e}") except Exception: pass def browse(self) -> None: """Open a directory selection dialog and update `path_var`. The selected path is stored as an absolute string in `path_var`. """ directory = filedialog.askdirectory() if directory: self.path_var.set(str(Path(directory))) def _on_differ(self): proj_path = self.path_var.get() if not proj_path: messagebox.showerror("Differing", "No project path selected.") return bm = BaselineManager(proj_path) baselines = bm.list_baselines() if not baselines: # no baseline: ask to create create = messagebox.askyesno("Differing", "No baseline found for this project. Create baseline now?") if not create: return try: # Create baseline from the selected folder (snapshot) profile_name = self.current_profile.get('name') if self.current_profile else None baseline_id = bm.create_baseline_from_dir(proj_path, baseline_id=None, snapshot=True, compute_sha1=True, ignore_patterns=None, profile_name=profile_name, max_keep=5) except Exception as e: messagebox.showerror("Differing", f"Failed to create baseline: {e}") return messagebox.showinfo("Differing", f"Baseline created: {baseline_id}") return # If baselines exist, pick the most recent by name (lexicographic) latest = sorted(baselines)[-1] try: metadata = bm.load_metadata(latest) except Exception as e: messagebox.showerror("Differing", f"Failed to load baseline metadata: {e}") return # Run diff in background thread def _run_diff(): btn = None try: # respect profile ignore patterns when diffing pr = self.current_profile ignore_patterns = pr.get('ignore', []) if pr else None d = Differ(metadata, proj_path, ignore_patterns=ignore_patterns) result = d.diff() total = result.get("total", {}) msg = f"Diff completed. Added: {total.get('added',0)}, Deleted: {total.get('deleted',0)}, Modified: {total.get('modified',0)}" messagebox.showinfo("Differing", msg) # save current state as a new baseline so it's available next time try: profile_name = self.current_profile.get('name') if self.current_profile else None bm2 = BaselineManager(proj_path) mk = app_settings.get_max_keep() bm2.create_baseline_from_dir(proj_path, baseline_id=None, snapshot=True, compute_sha1=True, ignore_patterns=ignore_patterns, profile_name=profile_name, max_keep=mk) except Exception: pass except Exception as e: messagebox.showerror("Differing", f"Diff failed: {e}") t = threading.Thread(target=_run_diff, daemon=True) t.start()