modificata la gui ed aggiunto sistema profili
This commit is contained in:
parent
9cea537345
commit
ae53019b23
@ -1,4 +1,18 @@
|
||||
import os
|
||||
import sys
|
||||
import tkinter as tk
|
||||
|
||||
# Ensure `externals` submodules are importable at runtime by adding
|
||||
# their directories to sys.path. This keeps the externals as lightweight
|
||||
# submodules without requiring packaging.
|
||||
ROOT = os.path.dirname(os.path.dirname(__file__))
|
||||
EXT_DIR = os.path.join(ROOT, "externals")
|
||||
TKLOGGER_PATH = os.path.join(EXT_DIR, "python-tkinter-logger")
|
||||
RESMON_PATH = os.path.join(EXT_DIR, "python-resource-monitor")
|
||||
for p in (TKLOGGER_PATH, RESMON_PATH):
|
||||
if os.path.isdir(p) and p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
from codebridge.gui.main_window import MainWindow
|
||||
|
||||
|
||||
|
||||
@ -21,9 +21,23 @@ class CodeComparer:
|
||||
"thumbs.db"
|
||||
}
|
||||
|
||||
def __init__(self, source_path: str, destination_path: str):
|
||||
def __init__(self, source_path: str, destination_path: str, ignore_extensions=None):
|
||||
self.source_path = source_path
|
||||
self.destination_path = destination_path
|
||||
# ignore_extensions: iterable of extensions (including dot), e.g. ['.o', '.d']
|
||||
# Normalize: strip whitespace, ensure leading dot, lowercase
|
||||
norm = set()
|
||||
for e in (ignore_extensions or []):
|
||||
try:
|
||||
s = str(e).strip()
|
||||
except Exception:
|
||||
continue
|
||||
if not s:
|
||||
continue
|
||||
if not s.startswith('.'):
|
||||
s = '.' + s
|
||||
norm.add(s.lower())
|
||||
self.ignore_extensions = norm
|
||||
self.added: List[str] = []
|
||||
self.modified: List[str] = []
|
||||
self.deleted: List[str] = []
|
||||
@ -32,10 +46,22 @@ class CodeComparer:
|
||||
"""
|
||||
Checks if a file or directory name matches the ignore patterns.
|
||||
"""
|
||||
# exact-name ignores
|
||||
if name in self.IGNORE_PATTERNS:
|
||||
return True
|
||||
if name.endswith(".pyc"):
|
||||
|
||||
# wildcard-like patterns in IGNORE_PATTERNS (simple suffix check for patterns like '*.pyc')
|
||||
for pat in self.IGNORE_PATTERNS:
|
||||
if pat.startswith("*") and name.lower().endswith(pat.lstrip("*").lower()):
|
||||
return True
|
||||
|
||||
# explicit extension ignores provided at runtime
|
||||
# compare case-insensitive
|
||||
lname = name.lower()
|
||||
for ext in self.ignore_extensions:
|
||||
if ext and lname.endswith(ext):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def compute_hash(self, file_path: str) -> str:
|
||||
|
||||
@ -27,9 +27,9 @@ class CommitDialog(tk.Toplevel):
|
||||
self.text_area.pack(padx=10, pady=5, fill="both", expand=True)
|
||||
btn_frame = ttk.Frame(self)
|
||||
btn_frame.pack(fill="x", pady=10)
|
||||
ok_btn = ttk.Button(btn_frame, text="Confirm", command=self._on_confirm)
|
||||
ok_btn = ttk.Button(btn_frame, text="✅ Confirm", command=self._on_confirm)
|
||||
ok_btn.pack(side="right", padx=10)
|
||||
cancel_btn = ttk.Button(btn_frame, text="Cancel", command=self.destroy)
|
||||
cancel_btn = ttk.Button(btn_frame, text="✖ Cancel", command=self.destroy)
|
||||
cancel_btn.pack(side="right", padx=5)
|
||||
|
||||
def _on_confirm(self) -> None:
|
||||
|
||||
@ -27,7 +27,7 @@ class DiffViewer(tk.Toplevel):
|
||||
# Toolbar
|
||||
toolbar = ttk.Frame(self, padding=5)
|
||||
toolbar.pack(fill="x", side="top")
|
||||
copy_all_btn = ttk.Button(toolbar, text="Copy Source to Destination", command=self._copy_source_to_dest)
|
||||
copy_all_btn = ttk.Button(toolbar, text="⤴ Copy Source to Destination", command=self._copy_source_to_dest)
|
||||
copy_all_btn.pack(side="left", padx=5)
|
||||
# Main diff container
|
||||
main_frame = ttk.Frame(self)
|
||||
|
||||
@ -1,10 +1,26 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
from tkinter.scrolledtext import ScrolledText
|
||||
import os
|
||||
import logging
|
||||
|
||||
# Externals (made importable by __main__.py adding their folders to sys.path)
|
||||
try:
|
||||
from tkinter_logger import TkinterLogger
|
||||
except Exception:
|
||||
TkinterLogger = None # type: ignore
|
||||
|
||||
try:
|
||||
from resource_monitor import TkinterResourceMonitor, is_psutil_available
|
||||
except Exception:
|
||||
TkinterResourceMonitor = None # type: ignore
|
||||
is_psutil_available = lambda: False
|
||||
from codebridge.core.comparer import CodeComparer
|
||||
from codebridge.core.sync_manager import SyncManager
|
||||
from codebridge.gui.diff_viewer import DiffViewer
|
||||
from codebridge.gui.commit_dialog import CommitDialog
|
||||
from codebridge.gui.profile_dialog import ProfileManagerDialog, ProfileManagerFrame
|
||||
import json
|
||||
|
||||
|
||||
class MainWindow:
|
||||
@ -18,7 +34,58 @@ class MainWindow:
|
||||
self.root.geometry("900x600")
|
||||
self.comparer = None
|
||||
self.sync_manager = SyncManager()
|
||||
# Initialize optional externals
|
||||
self.logger_sys = None
|
||||
if TkinterLogger is not None:
|
||||
try:
|
||||
self.logger_sys = TkinterLogger(self.root)
|
||||
# enable console by default; tkinter handler will be added when log window opens
|
||||
self.logger_sys.setup(enable_console=True, enable_tkinter=True)
|
||||
logging.getLogger(__name__).info("TkinterLogger initialized")
|
||||
except Exception:
|
||||
self.logger_sys = None
|
||||
|
||||
# Resource monitor
|
||||
self._resource_monitor = None
|
||||
self.status_var = tk.StringVar()
|
||||
if TkinterResourceMonitor is not None and is_psutil_available():
|
||||
try:
|
||||
self._resource_monitor = TkinterResourceMonitor(self.root, self.status_var, poll_interval=1.0)
|
||||
self._resource_monitor.start()
|
||||
except Exception:
|
||||
self._resource_monitor = None
|
||||
else:
|
||||
# psutil not available -> show note
|
||||
self.status_var.set("Resource monitor: psutil not installed")
|
||||
|
||||
# Profiles: load/save path in program root
|
||||
try:
|
||||
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
except Exception:
|
||||
repo_root = os.getcwd()
|
||||
self._profiles_path = os.path.join(repo_root, "profiles.json")
|
||||
self.profiles = self._load_profiles()
|
||||
self.current_ignore_exts = []
|
||||
|
||||
# Setup UI and close handler
|
||||
self._setup_ui()
|
||||
# Attach logger handler to persistent log widget if available
|
||||
try:
|
||||
if self.logger_sys and hasattr(self, "log_text"):
|
||||
try:
|
||||
self.logger_sys.add_tkinter_handler(self.log_text)
|
||||
except Exception:
|
||||
logging.getLogger(__name__).exception("Failed to attach tkinter log handler to main UI")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Populate profiles combobox if any
|
||||
try:
|
||||
self._populate_profiles()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
|
||||
|
||||
def _setup_ui(self) -> None:
|
||||
"""
|
||||
@ -27,6 +94,15 @@ class MainWindow:
|
||||
# Path selection frame
|
||||
path_frame = ttk.LabelFrame(self.root, text="Path Selection", padding=10)
|
||||
path_frame.pack(fill="x", padx=10, pady=5)
|
||||
# Profiles combobox + Manage button
|
||||
prof_frame = ttk.Frame(path_frame)
|
||||
prof_frame.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0,6))
|
||||
ttk.Label(prof_frame, text="Profile:").pack(side="left")
|
||||
self.profile_var = tk.StringVar()
|
||||
self.profile_cb = ttk.Combobox(prof_frame, textvariable=self.profile_var, state="readonly", width=40)
|
||||
self.profile_cb.pack(side="left", padx=6)
|
||||
self.profile_cb.bind("<<ComboboxSelected>>", lambda e: self._on_profile_change())
|
||||
ttk.Button(prof_frame, text="⚙️ Manage Profiles...", command=self._open_manage_profiles).pack(side="left")
|
||||
self.src_path_var = tk.StringVar()
|
||||
self.dst_path_var = tk.StringVar()
|
||||
self._create_path_row(path_frame, "Source (New):", self.src_path_var, 0)
|
||||
@ -34,11 +110,11 @@ class MainWindow:
|
||||
# Action buttons
|
||||
btn_frame = ttk.Frame(self.root, padding=5)
|
||||
btn_frame.pack(fill="x", padx=10)
|
||||
compare_btn = ttk.Button(btn_frame, text="Compare Folders", command=self._run_comparison)
|
||||
compare_btn = ttk.Button(btn_frame, text="🔍 Compare Folders", command=self._run_comparison)
|
||||
compare_btn.pack(side="left", padx=5)
|
||||
export_btn = ttk.Button(btn_frame, text="Export Changes (ZIP)", command=self._export_changes)
|
||||
export_btn = ttk.Button(btn_frame, text="📦 Export Changes (ZIP)", command=self._export_changes)
|
||||
export_btn.pack(side="left", padx=5)
|
||||
import_btn = ttk.Button(btn_frame, text="Import Package (ZIP)", command=self._import_package)
|
||||
import_btn = ttk.Button(btn_frame, text="📥 Import Package (ZIP)", command=self._import_package)
|
||||
import_btn.pack(side="left", padx=5)
|
||||
# Results treeview
|
||||
self._setup_treeview()
|
||||
@ -47,9 +123,18 @@ class MainWindow:
|
||||
"""
|
||||
Creates a row with a label, entry and browse button for paths.
|
||||
"""
|
||||
ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", padx=5)
|
||||
ttk.Entry(parent, textvariable=var, width=80).grid(row=row, column=1, padx=5, pady=2)
|
||||
ttk.Button(parent, text="Browse", command=lambda: self._browse_folder(var)).grid(row=row, column=2, padx=5)
|
||||
ttk.Label(parent, text=label).grid(row=row+1, column=0, sticky="w", padx=5)
|
||||
entry = ttk.Entry(parent, textvariable=var, width=80)
|
||||
entry.grid(row=row+1, column=1, padx=5, pady=2)
|
||||
btn = ttk.Button(parent, text="📁 Browse", command=lambda: self._browse_folder(var))
|
||||
btn.grid(row=row+1, column=2, padx=5)
|
||||
# store references so they can be made readonly when a profile is selected
|
||||
if label.lower().startswith("source"):
|
||||
self.src_entry = entry
|
||||
self.src_browse = btn
|
||||
else:
|
||||
self.dst_entry = entry
|
||||
self.dst_browse = btn
|
||||
|
||||
def _browse_folder(self, var: tk.StringVar) -> None:
|
||||
"""
|
||||
@ -79,6 +164,92 @@ class MainWindow:
|
||||
self.tree.pack(side="left", fill="both", expand=True)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
|
||||
# Persistent log box below the treeview
|
||||
log_frame = ttk.Frame(self.root, padding=(5, 2))
|
||||
log_frame.pack(fill="x")
|
||||
self.log_text = ScrolledText(log_frame, height=8, state=tk.DISABLED)
|
||||
self.log_text.pack(fill="both", expand=True)
|
||||
|
||||
# Status bar at bottom
|
||||
status_frame = ttk.Frame(self.root, padding=(5, 2))
|
||||
status_frame.pack(fill="x", side="bottom")
|
||||
ttk.Label(status_frame, textvariable=self.status_var).pack(side="left")
|
||||
|
||||
# --- Profiles persistence ---
|
||||
def _load_profiles(self):
|
||||
try:
|
||||
if os.path.exists(self._profiles_path):
|
||||
with open(self._profiles_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
def _save_profiles(self):
|
||||
try:
|
||||
with open(self._profiles_path, "w", encoding="utf-8") as f:
|
||||
json.dump(self.profiles, f, indent=2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _populate_profiles(self):
|
||||
names = ["(None)"] + sorted(self.profiles.keys())
|
||||
self.profile_cb["values"] = names
|
||||
self.profile_cb.set(names[0])
|
||||
|
||||
def _open_manage_profiles(self):
|
||||
dlg = ProfileManagerDialog(self.root, self._profiles_path, self.profiles)
|
||||
self.root.wait_window(dlg)
|
||||
# reload and refresh
|
||||
self.profiles = self._load_profiles()
|
||||
self._populate_profiles()
|
||||
|
||||
def _close_profile_panel(self):
|
||||
try:
|
||||
if hasattr(self, "profile_panel") and self.profile_panel and self.profile_panel.winfo_exists():
|
||||
self.profile_panel.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
# reload and refresh
|
||||
self.profiles = self._load_profiles()
|
||||
self._populate_profiles()
|
||||
|
||||
def _on_profile_change(self):
|
||||
sel = self.profile_var.get()
|
||||
if not sel or sel == "(None)":
|
||||
# enable editing of entries
|
||||
try:
|
||||
self.src_entry.config(state="normal")
|
||||
self.dst_entry.config(state="normal")
|
||||
self.src_browse.config(state="normal")
|
||||
self.dst_browse.config(state="normal")
|
||||
except Exception:
|
||||
pass
|
||||
self.current_ignore_exts = []
|
||||
return
|
||||
profile = self.profiles.get(sel)
|
||||
if not profile:
|
||||
return
|
||||
# set entries and make readonly
|
||||
self.src_path_var.set(profile.get("source", ""))
|
||||
self.dst_path_var.set(profile.get("destination", ""))
|
||||
# load ignore extensions for comparisons
|
||||
ign = profile.get("ignore_extensions", [])
|
||||
if isinstance(ign, (list, tuple)):
|
||||
self.current_ignore_exts = ign
|
||||
elif isinstance(ign, str) and ign:
|
||||
# comma-separated string
|
||||
self.current_ignore_exts = [e if e.startswith('.') else '.' + e for e in (x.strip() for x in ign.split(',')) if e]
|
||||
else:
|
||||
self.current_ignore_exts = []
|
||||
try:
|
||||
self.src_entry.config(state="readonly")
|
||||
self.dst_entry.config(state="readonly")
|
||||
self.src_browse.config(state="disabled")
|
||||
self.dst_browse.config(state="disabled")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _run_comparison(self) -> None:
|
||||
"""
|
||||
Executes comparison and populates the treeview.
|
||||
@ -88,7 +259,7 @@ class MainWindow:
|
||||
if not src or not dst:
|
||||
messagebox.showwarning("Warning", "Please select both source and destination folders.")
|
||||
return
|
||||
self.comparer = CodeComparer(src, dst)
|
||||
self.comparer = CodeComparer(src, dst, ignore_extensions=self.current_ignore_exts)
|
||||
added, modified, deleted = self.comparer.compare()
|
||||
for item in self.tree.get_children():
|
||||
self.tree.delete(item)
|
||||
@ -154,3 +325,31 @@ class MainWindow:
|
||||
commit_msg = self.sync_manager.apply_package(package_path, dst)
|
||||
messagebox.showinfo("Import Complete", f"Package applied successfully.\n\nCommit Message:\n{commit_msg}")
|
||||
self._run_comparison()
|
||||
|
||||
|
||||
|
||||
def _on_closing(self) -> None:
|
||||
"""Clean shutdown: stop resource monitor and logging, then exit."""
|
||||
try:
|
||||
if self._resource_monitor:
|
||||
try:
|
||||
self._resource_monitor.stop()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if self.logger_sys:
|
||||
try:
|
||||
self.logger_sys.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.root.destroy()
|
||||
except Exception:
|
||||
# fallback
|
||||
os._exit(0)
|
||||
223
codebridge/gui/profile_dialog.py
Normal file
223
codebridge/gui/profile_dialog.py
Normal file
@ -0,0 +1,223 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, filedialog
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
class ProfileManagerFrame(ttk.Frame):
|
||||
"""Embeddable frame to create/edit/delete folder profiles.
|
||||
|
||||
Can be placed inside the main window (centered) or inside a Toplevel.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, profiles_path: str, profiles: Dict[str, Any]):
|
||||
super().__init__(parent, padding=8, relief="raised")
|
||||
self.parent = parent
|
||||
self.profiles_path = profiles_path
|
||||
self.profiles = profiles.copy()
|
||||
self._setup_ui()
|
||||
self._populate_list()
|
||||
|
||||
def _setup_ui(self):
|
||||
left = ttk.Frame(self, padding=8)
|
||||
left.grid(row=0, column=0, sticky="ns")
|
||||
ttk.Label(left, text="Profiles").pack(anchor="w")
|
||||
self.listbox = tk.Listbox(left, width=30, height=20)
|
||||
self.listbox.pack(fill="y", expand=True)
|
||||
self.listbox.bind("<<ListboxSelect>>", self._on_select)
|
||||
|
||||
right = ttk.Frame(self, padding=8)
|
||||
right.grid(row=0, column=1, sticky="nsew")
|
||||
|
||||
# Name (required)
|
||||
ttk.Label(right, text="Name (required)").grid(row=0, column=0, sticky="w")
|
||||
self.name_var = tk.StringVar()
|
||||
self.name_entry = ttk.Entry(right, textvariable=self.name_var)
|
||||
self.name_entry.grid(row=0, column=1, sticky="ew", pady=2)
|
||||
|
||||
# Description (optional)
|
||||
ttk.Label(right, text="Description").grid(row=1, column=0, sticky="w")
|
||||
self.desc_var = tk.StringVar()
|
||||
self.desc_entry = ttk.Entry(right, textvariable=self.desc_var)
|
||||
self.desc_entry.grid(row=1, column=1, sticky="ew", pady=2)
|
||||
|
||||
# Source folder
|
||||
ttk.Label(right, text="Source folder").grid(row=2, column=0, sticky="w")
|
||||
self.src_var = tk.StringVar()
|
||||
src_row = ttk.Frame(right)
|
||||
src_row.grid(row=2, column=1, sticky="ew", pady=2)
|
||||
self.src_entry = ttk.Entry(src_row, textvariable=self.src_var)
|
||||
self.src_entry.pack(side="left", fill="x", expand=True)
|
||||
ttk.Button(src_row, text="📁 Browse", command=self._browse_src).pack(side="left", padx=4)
|
||||
|
||||
# Destination folder
|
||||
ttk.Label(right, text="Destination folder").grid(row=3, column=0, sticky="w")
|
||||
self.dst_var = tk.StringVar()
|
||||
dst_row = ttk.Frame(right)
|
||||
dst_row.grid(row=3, column=1, sticky="ew", pady=2)
|
||||
self.dst_entry = ttk.Entry(dst_row, textvariable=self.dst_var)
|
||||
self.dst_entry.pack(side="left", fill="x", expand=True)
|
||||
ttk.Button(dst_row, text="📁 Browse", command=self._browse_dst).pack(side="left", padx=4)
|
||||
|
||||
# Ignore extensions (multi-line text for more space)
|
||||
ttk.Label(right, text="Ignore extensions (comma-separated)").grid(row=4, column=0, sticky="nw")
|
||||
ignore_row = ttk.Frame(right)
|
||||
ignore_row.grid(row=4, column=1, sticky="ew", pady=2)
|
||||
self.ignore_text = tk.Text(ignore_row, height=3, width=60)
|
||||
self.ignore_text.pack(side="left", fill="both", expand=True)
|
||||
|
||||
# Buttons
|
||||
btn_row = ttk.Frame(right)
|
||||
btn_row.grid(row=5, column=0, columnspan=2, pady=12, sticky="ew")
|
||||
ttk.Button(btn_row, text="➕ New", command=self._new).pack(side="left", padx=4)
|
||||
ttk.Button(btn_row, text="💾 Save", command=self._save).pack(side="left", padx=4)
|
||||
ttk.Button(btn_row, text="🗑️ Delete", command=self._delete).pack(side="left", padx=4)
|
||||
ttk.Button(btn_row, text="Insert common", command=self._insert_common_extensions).pack(side="left", padx=4)
|
||||
ttk.Button(btn_row, text="✖ Close", command=self._on_close_requested).pack(side="right", padx=4)
|
||||
|
||||
right.columnconfigure(1, weight=1)
|
||||
self.columnconfigure(1, weight=1)
|
||||
|
||||
# --- Internal helpers ---
|
||||
def _populate_list(self):
|
||||
self.listbox.delete(0, "end")
|
||||
for name in sorted(self.profiles.keys()):
|
||||
self.listbox.insert("end", name)
|
||||
|
||||
def _on_select(self, _evt=None):
|
||||
sel = self.listbox.curselection()
|
||||
if not sel:
|
||||
return
|
||||
name = self.listbox.get(sel[0])
|
||||
p = self.profiles.get(name, {})
|
||||
self.name_var.set(name)
|
||||
self.desc_var.set(p.get("description", ""))
|
||||
self.src_var.set(p.get("source", ""))
|
||||
self.dst_var.set(p.get("destination", ""))
|
||||
# load ignore extensions
|
||||
ign = p.get("ignore_extensions")
|
||||
if isinstance(ign, (list, tuple)):
|
||||
self.ignore_text.delete("1.0", "end")
|
||||
self.ignore_text.insert("1.0", ",".join([e.lstrip('.') for e in ign]))
|
||||
else:
|
||||
self.ignore_text.delete("1.0", "end")
|
||||
self.ignore_text.insert("1.0", ign or "")
|
||||
|
||||
def _browse_src(self):
|
||||
parent = self.winfo_toplevel()
|
||||
d = filedialog.askdirectory(parent=parent)
|
||||
if d:
|
||||
self.src_var.set(d)
|
||||
|
||||
def _browse_dst(self):
|
||||
parent = self.winfo_toplevel()
|
||||
d = filedialog.askdirectory(parent=parent)
|
||||
if d:
|
||||
self.dst_var.set(d)
|
||||
|
||||
def _new(self):
|
||||
self.listbox.selection_clear(0, "end")
|
||||
self.name_var.set("")
|
||||
self.desc_var.set("")
|
||||
self.src_var.set("")
|
||||
self.dst_var.set("")
|
||||
self.ignore_text.delete("1.0", "end")
|
||||
self.name_entry.focus()
|
||||
|
||||
def _save(self):
|
||||
name = self.name_var.get().strip()
|
||||
if not name:
|
||||
messagebox.showwarning("Validation", "Profile name is required.", parent=self.winfo_toplevel())
|
||||
return
|
||||
profile = {
|
||||
"description": self.desc_var.get().strip(),
|
||||
"source": self.src_var.get().strip(),
|
||||
"destination": self.dst_var.get().strip()
|
||||
}
|
||||
# parse ignore extensions from comma-separated input
|
||||
raw = self.ignore_text.get("1.0", "end").strip()
|
||||
if raw:
|
||||
parts = [p.strip() for p in raw.split(",") if p.strip()]
|
||||
# normalize to start with dot
|
||||
norm = []
|
||||
for p in parts:
|
||||
if not p.startswith("."):
|
||||
p = "." + p
|
||||
norm.append(p)
|
||||
profile["ignore_extensions"] = norm
|
||||
else:
|
||||
profile["ignore_extensions"] = []
|
||||
self.profiles[name] = profile
|
||||
try:
|
||||
with open(self.profiles_path, "w", encoding="utf-8") as f:
|
||||
json.dump(self.profiles, f, indent=2)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to save profiles: {e}", parent=self.winfo_toplevel())
|
||||
return
|
||||
self._populate_list()
|
||||
messagebox.showinfo("Saved", "Profile saved.", parent=self.winfo_toplevel())
|
||||
|
||||
def _delete(self):
|
||||
name = self.name_var.get().strip()
|
||||
if not name:
|
||||
return
|
||||
if name not in self.profiles:
|
||||
messagebox.showwarning("Not found", "Profile not found.", parent=self.winfo_toplevel())
|
||||
return
|
||||
if not messagebox.askyesno("Confirm", f"Delete profile '{name}'?", parent=self.winfo_toplevel()):
|
||||
return
|
||||
del self.profiles[name]
|
||||
try:
|
||||
with open(self.profiles_path, "w", encoding="utf-8") as f:
|
||||
json.dump(self.profiles, f, indent=2)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to save profiles: {e}", parent=self.winfo_toplevel())
|
||||
return
|
||||
self._populate_list()
|
||||
self._new()
|
||||
|
||||
def _insert_common_extensions(self):
|
||||
# common list without dots (entry convenience)
|
||||
common = ",".join([
|
||||
"o", "d", "obj", "class", "pyc", "pyo", "log", "tmp", "swp", "DS_Store",
|
||||
"exe", "a", "mk", "bak"
|
||||
])
|
||||
self.ignore_text.delete("1.0", "end")
|
||||
self.ignore_text.insert("1.0", common)
|
||||
|
||||
def _on_close_requested(self):
|
||||
# emit a virtual event so parent can handle closing/hiding
|
||||
try:
|
||||
self.event_generate("<<ProfileManagerClose>>")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class ProfileManagerDialog(tk.Toplevel):
|
||||
"""Backward-compatible Toplevel wrapper using the embeddable frame."""
|
||||
|
||||
def __init__(self, parent, profiles_path: str, profiles: Dict[str, Any]):
|
||||
super().__init__(parent)
|
||||
self.parent = parent
|
||||
self.title("Manage Profiles")
|
||||
desired_w, desired_h = 760, 420
|
||||
self.geometry(f"{desired_w}x{desired_h}")
|
||||
frame = ProfileManagerFrame(self, profiles_path, profiles)
|
||||
frame.pack(fill="both", expand=True)
|
||||
frame.bind("<<ProfileManagerClose>>", lambda e: self.destroy())
|
||||
self.transient(parent)
|
||||
# center over parent window
|
||||
try:
|
||||
self.update_idletasks()
|
||||
px = parent.winfo_rootx()
|
||||
py = parent.winfo_rooty()
|
||||
pw = parent.winfo_width()
|
||||
ph = parent.winfo_height()
|
||||
x = px + max(0, (pw - desired_w) // 2)
|
||||
y = py + max(0, (ph - desired_h) // 2)
|
||||
self.geometry(f"{desired_w}x{desired_h}+{x}+{y}")
|
||||
except Exception:
|
||||
# fallback: keep default geometry
|
||||
pass
|
||||
self.grab_set()
|
||||
47
profiles.json
Normal file
47
profiles.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"DevEnv": {
|
||||
"description": "DevEnv Grifo E",
|
||||
"source": "//tsclient/D/__BACKUP/GrifoE/GRIFO-E_svn/DevEnv",
|
||||
"destination": "C:/src/GRIFO-E - Copia/DevEnv",
|
||||
"ignore_extensions": [
|
||||
".o",
|
||||
".d",
|
||||
".obj",
|
||||
".class",
|
||||
".pyc",
|
||||
".pyo",
|
||||
".log",
|
||||
".tmp",
|
||||
".swp",
|
||||
".DS_Store",
|
||||
".exe",
|
||||
".a",
|
||||
".mk",
|
||||
".bak",
|
||||
".defs",
|
||||
".txt",
|
||||
".pdom"
|
||||
]
|
||||
},
|
||||
"REP": {
|
||||
"description": "REP Grifo E code base",
|
||||
"source": "C:/src/GRIFO-E - Copia/REP",
|
||||
"destination": "//tsclient/D/__BACKUP/GrifoE/GRIFO-E_svn/REP",
|
||||
"ignore_extensions": [
|
||||
".o",
|
||||
".d",
|
||||
".obj",
|
||||
".class",
|
||||
".pyc",
|
||||
".pyo",
|
||||
".log",
|
||||
".tmp",
|
||||
".swp",
|
||||
".DS_Store",
|
||||
".exe",
|
||||
".a",
|
||||
".mk",
|
||||
".bak"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user