SXXXXXXX_PyUCC/pyucc/gui/topbar.py

261 lines
9.8 KiB
Python

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("<<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))
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():
# remember current selection, reload profiles and restore selection
cur = self.profile_var.get()
self._load_profiles()
vals = list(self.profile_cb["values"]) if self.profile_cb["values"] else []
if cur and cur in vals:
# keep same selection and refresh derived UI
self.profile_var.set(cur)
self._on_profile_selected()
elif vals:
# if there is at least one profile, ensure UI reflects a valid selection
# keep existing selection if already set, otherwise pick first
if not self.profile_var.get():
self.profile_var.set(vals[0])
self._on_profile_selected()
else:
# no profiles available
self.profile_var.set("")
self.current_profile = None
self.path_var.set("")
self.project_type_var.set("-")
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()