import tkinter as tk from tkinter import ttk, filedialog, messagebox import threading import os 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)) baselines_btn = ttk.Button( self, text="📁 Baselines", command=self._open_baselines_folder ) baselines_btn.grid(row=0, column=4, 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=5, 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(5, 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 _open_baselines_folder(self): """Open the baselines folder in Windows Explorer.""" try: import subprocess from ..config import settings as app_settings # Get baseline directory from settings or use default baseline_dir = app_settings.get_baseline_dir() if not baseline_dir or not os.path.exists(baseline_dir): # Fallback to default location baseline_dir = os.path.join(os.getcwd(), "baseline") if not os.path.exists(baseline_dir): os.makedirs(baseline_dir, exist_ok=True) # Open in Windows Explorer subprocess.Popen(["explorer", os.path.abspath(baseline_dir)]) except Exception as e: try: messagebox.showerror( "Open Baselines Folder", f"Failed to open folder: {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 baseline_files_dir = bm.get_baseline_files_dir(latest) d = Differ( metadata, proj_path, ignore_patterns=ignore_patterns, baseline_files_dir=baseline_files_dir, ) 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()