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
|
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
|
from codebridge.gui.main_window import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -21,9 +21,23 @@ class CodeComparer:
|
|||||||
"thumbs.db"
|
"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.source_path = source_path
|
||||||
self.destination_path = destination_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.added: List[str] = []
|
||||||
self.modified: List[str] = []
|
self.modified: List[str] = []
|
||||||
self.deleted: List[str] = []
|
self.deleted: List[str] = []
|
||||||
@ -32,10 +46,22 @@ class CodeComparer:
|
|||||||
"""
|
"""
|
||||||
Checks if a file or directory name matches the ignore patterns.
|
Checks if a file or directory name matches the ignore patterns.
|
||||||
"""
|
"""
|
||||||
|
# exact-name ignores
|
||||||
if name in self.IGNORE_PATTERNS:
|
if name in self.IGNORE_PATTERNS:
|
||||||
return True
|
return True
|
||||||
if name.endswith(".pyc"):
|
|
||||||
return True
|
# 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
|
return False
|
||||||
|
|
||||||
def compute_hash(self, file_path: str) -> str:
|
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)
|
self.text_area.pack(padx=10, pady=5, fill="both", expand=True)
|
||||||
btn_frame = ttk.Frame(self)
|
btn_frame = ttk.Frame(self)
|
||||||
btn_frame.pack(fill="x", pady=10)
|
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)
|
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)
|
cancel_btn.pack(side="right", padx=5)
|
||||||
|
|
||||||
def _on_confirm(self) -> None:
|
def _on_confirm(self) -> None:
|
||||||
|
|||||||
@ -27,7 +27,7 @@ class DiffViewer(tk.Toplevel):
|
|||||||
# Toolbar
|
# Toolbar
|
||||||
toolbar = ttk.Frame(self, padding=5)
|
toolbar = ttk.Frame(self, padding=5)
|
||||||
toolbar.pack(fill="x", side="top")
|
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)
|
copy_all_btn.pack(side="left", padx=5)
|
||||||
# Main diff container
|
# Main diff container
|
||||||
main_frame = ttk.Frame(self)
|
main_frame = ttk.Frame(self)
|
||||||
|
|||||||
@ -1,10 +1,26 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, filedialog, messagebox
|
from tkinter import ttk, filedialog, messagebox
|
||||||
|
from tkinter.scrolledtext import ScrolledText
|
||||||
import os
|
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.comparer import CodeComparer
|
||||||
from codebridge.core.sync_manager import SyncManager
|
from codebridge.core.sync_manager import SyncManager
|
||||||
from codebridge.gui.diff_viewer import DiffViewer
|
from codebridge.gui.diff_viewer import DiffViewer
|
||||||
from codebridge.gui.commit_dialog import CommitDialog
|
from codebridge.gui.commit_dialog import CommitDialog
|
||||||
|
from codebridge.gui.profile_dialog import ProfileManagerDialog, ProfileManagerFrame
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
class MainWindow:
|
class MainWindow:
|
||||||
@ -18,7 +34,58 @@ class MainWindow:
|
|||||||
self.root.geometry("900x600")
|
self.root.geometry("900x600")
|
||||||
self.comparer = None
|
self.comparer = None
|
||||||
self.sync_manager = SyncManager()
|
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()
|
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:
|
def _setup_ui(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -27,6 +94,15 @@ class MainWindow:
|
|||||||
# Path selection frame
|
# Path selection frame
|
||||||
path_frame = ttk.LabelFrame(self.root, text="Path Selection", padding=10)
|
path_frame = ttk.LabelFrame(self.root, text="Path Selection", padding=10)
|
||||||
path_frame.pack(fill="x", padx=10, pady=5)
|
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.src_path_var = tk.StringVar()
|
||||||
self.dst_path_var = tk.StringVar()
|
self.dst_path_var = tk.StringVar()
|
||||||
self._create_path_row(path_frame, "Source (New):", self.src_path_var, 0)
|
self._create_path_row(path_frame, "Source (New):", self.src_path_var, 0)
|
||||||
@ -34,11 +110,11 @@ class MainWindow:
|
|||||||
# Action buttons
|
# Action buttons
|
||||||
btn_frame = ttk.Frame(self.root, padding=5)
|
btn_frame = ttk.Frame(self.root, padding=5)
|
||||||
btn_frame.pack(fill="x", padx=10)
|
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)
|
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)
|
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)
|
import_btn.pack(side="left", padx=5)
|
||||||
# Results treeview
|
# Results treeview
|
||||||
self._setup_treeview()
|
self._setup_treeview()
|
||||||
@ -47,9 +123,18 @@ class MainWindow:
|
|||||||
"""
|
"""
|
||||||
Creates a row with a label, entry and browse button for paths.
|
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.Label(parent, text=label).grid(row=row+1, column=0, sticky="w", padx=5)
|
||||||
ttk.Entry(parent, textvariable=var, width=80).grid(row=row, column=1, padx=5, pady=2)
|
entry = ttk.Entry(parent, textvariable=var, width=80)
|
||||||
ttk.Button(parent, text="Browse", command=lambda: self._browse_folder(var)).grid(row=row, column=2, padx=5)
|
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:
|
def _browse_folder(self, var: tk.StringVar) -> None:
|
||||||
"""
|
"""
|
||||||
@ -79,6 +164,92 @@ class MainWindow:
|
|||||||
self.tree.pack(side="left", fill="both", expand=True)
|
self.tree.pack(side="left", fill="both", expand=True)
|
||||||
scrollbar.pack(side="right", fill="y")
|
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:
|
def _run_comparison(self) -> None:
|
||||||
"""
|
"""
|
||||||
Executes comparison and populates the treeview.
|
Executes comparison and populates the treeview.
|
||||||
@ -88,7 +259,7 @@ class MainWindow:
|
|||||||
if not src or not dst:
|
if not src or not dst:
|
||||||
messagebox.showwarning("Warning", "Please select both source and destination folders.")
|
messagebox.showwarning("Warning", "Please select both source and destination folders.")
|
||||||
return
|
return
|
||||||
self.comparer = CodeComparer(src, dst)
|
self.comparer = CodeComparer(src, dst, ignore_extensions=self.current_ignore_exts)
|
||||||
added, modified, deleted = self.comparer.compare()
|
added, modified, deleted = self.comparer.compare()
|
||||||
for item in self.tree.get_children():
|
for item in self.tree.get_children():
|
||||||
self.tree.delete(item)
|
self.tree.delete(item)
|
||||||
@ -153,4 +324,32 @@ class MainWindow:
|
|||||||
if confirm:
|
if confirm:
|
||||||
commit_msg = self.sync_manager.apply_package(package_path, dst)
|
commit_msg = self.sync_manager.apply_package(package_path, dst)
|
||||||
messagebox.showinfo("Import Complete", f"Package applied successfully.\n\nCommit Message:\n{commit_msg}")
|
messagebox.showinfo("Import Complete", f"Package applied successfully.\n\nCommit Message:\n{commit_msg}")
|
||||||
self._run_comparison()
|
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