173 lines
6.9 KiB
Python
173 lines
6.9 KiB
Python
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("<<ComboboxSelected>>", 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()
|