407 lines
17 KiB
Python
407 lines
17 KiB
Python
# BackupApp/backup_app/gui/dialogs.py
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, Toplevel, simpledialog, messagebox
|
|
from typing import List, Tuple, Dict, Any, Callable
|
|
|
|
# Tuple format for file details: (filename: str, size_mb: float, full_path: str)
|
|
FileDetail = Tuple[str, float, str]
|
|
|
|
# Default groups of exclusions for common C/C++ and Python projects
|
|
DEFAULT_EXCLUSION_GROUPS: Dict[str, List[str]] = {
|
|
"C/C++ build & bin": [
|
|
".o", ".obj", ".out", ".exe", ".dll", ".so", ".dylib", ".a", ".lib", ".elf", ".pdb", ".ilk", ".map", ".hex", ".bin"
|
|
],
|
|
"Build system / CMake / Make": [
|
|
"CMakeFiles/", "CMakeCache.txt", "build/", "Makefile"
|
|
],
|
|
"Python bytecode & packaging": [
|
|
".pyc", ".pyo", ".pyd", "__pycache__/", ".egg-info/", ".eggs/", "dist/", "build/", "pip-wheel-metadata/"
|
|
],
|
|
"Virtualenv / envs": [
|
|
".venv/", "venv/", "env/", "pip-wheel-metadata/"
|
|
],
|
|
"IDE / Editor files": [
|
|
".vscode/", ".idea/", ".vs/", ".suo", ".user", ".ncb", ".VC.db", "*.vcxproj.user"
|
|
],
|
|
"Editor temporary / swap / backup": [
|
|
"~", ".swp", ".swo", ".bak", ".tmp", ".temp", ".~lock"
|
|
],
|
|
"Cache / test / coverage": [
|
|
".pytest_cache/", ".coverage", "coverage.xml", "*.gcda", "*.gcno", "*.prof", "*.profdata"
|
|
],
|
|
"Logs / local DB / state": [
|
|
"*.log", "*.sqlite", "*.db", "*.sqlite3", "*-journal"
|
|
],
|
|
"Archives / distributables (optional)": [
|
|
".zip", ".tar", ".tar.gz", ".tgz", ".rar", ".7z", ".whl", ".egg"
|
|
],
|
|
"OS & misc": [
|
|
".DS_Store", "Thumbs.db", "desktop.ini", ".Trash-"
|
|
],
|
|
"Optional dependency folders": [
|
|
"node_modules/", ".m2/"
|
|
]
|
|
}
|
|
|
|
|
|
def center_window(parent_window: tk.Tk, child_window: Toplevel, width: int, height: int) -> None:
|
|
"""
|
|
Centers the child_window relative to the parent_window.
|
|
If parent_window is not yet drawn, it might not center perfectly initially.
|
|
"""
|
|
parent_window.update_idletasks() # Ensure parent dimensions are current
|
|
|
|
main_width = parent_window.winfo_width()
|
|
main_height = parent_window.winfo_height()
|
|
main_x = parent_window.winfo_x()
|
|
main_y = parent_window.winfo_y()
|
|
|
|
# Calculate position for the child window
|
|
x = main_x + (main_width // 2) - (width // 2)
|
|
y = main_y + (main_height // 2) - (height // 2)
|
|
|
|
# Ensure the window is not placed off-screen, especially if parent is minimized
|
|
screen_width = parent_window.winfo_screenwidth()
|
|
screen_height = parent_window.winfo_screenheight()
|
|
|
|
if x < 0: x = 0
|
|
if y < 0: y = 0
|
|
if x + width > screen_width: x = screen_width - width
|
|
if y + height > screen_height: y = screen_height - height
|
|
|
|
child_window.geometry(f"{width}x{height}+{x}+{y}")
|
|
|
|
|
|
def _sort_treeview_column(tree: ttk.Treeview, col_id: str, reverse: bool) -> None:
|
|
"""Helper function to sort a treeview column."""
|
|
# Extract data for sorting
|
|
# For numeric columns, we need to convert to float if possible
|
|
data_list = []
|
|
for child_id in tree.get_children(""):
|
|
value = tree.set(child_id, col_id)
|
|
try:
|
|
# Attempt to convert to float for numeric sorting (e.g., "Size")
|
|
# The 'Size' column in show_file_details_dialog contains " MB" suffix.
|
|
# The 'Size' column in show_extension_stats_dialog is already a float string.
|
|
if "MB" in value: # Heuristic for file details size
|
|
numeric_val = float(value.replace(" MB", "").strip())
|
|
else:
|
|
numeric_val = float(value)
|
|
data_list.append((numeric_val, child_id))
|
|
except ValueError:
|
|
# Fallback to string sorting if conversion fails
|
|
data_list.append((value.lower(), child_id)) # .lower() for case-insensitive string sort
|
|
|
|
data_list.sort(key=lambda t: t[0], reverse=reverse)
|
|
|
|
# Reorder items in the treeview
|
|
for index, (_, child_id) in enumerate(data_list):
|
|
tree.move(child_id, "", index)
|
|
|
|
# Update the heading command to toggle sort direction
|
|
tree.heading(col_id, command=lambda: _sort_treeview_column(tree, col_id, not reverse))
|
|
|
|
|
|
def show_file_details_dialog(
|
|
parent: tk.Tk,
|
|
included_files: List[FileDetail],
|
|
excluded_files: List[FileDetail]
|
|
) -> None:
|
|
"""
|
|
Displays a dialog showing lists of included and excluded files.
|
|
Derived from BackupApp.show_file_details.
|
|
"""
|
|
dialog = Toplevel(parent)
|
|
dialog.title("File Scan Details")
|
|
center_window(parent, dialog, 850, 600)
|
|
dialog.grab_set() # Modal behavior
|
|
|
|
tab_control = ttk.Notebook(dialog)
|
|
|
|
# --- Included Files Tab ---
|
|
included_frame = ttk.Frame(tab_control, padding=10)
|
|
tab_control.add(included_frame, text=f"Included Files ({len(included_files)})")
|
|
|
|
cols_included = ("Name", "Size (MB)", "Path")
|
|
included_tree = ttk.Treeview(included_frame, columns=cols_included, show="headings", height=15)
|
|
|
|
included_tree.heading("Name", text="Name", anchor="w", command=lambda: _sort_treeview_column(included_tree, "Name", False))
|
|
included_tree.column("Name", width=200, stretch=tk.NO, anchor="w")
|
|
included_tree.heading("Size (MB)", text="Size (MB)", anchor="e", command=lambda: _sort_treeview_column(included_tree, "Size (MB)", False))
|
|
included_tree.column("Size (MB)", width=100, stretch=tk.NO, anchor="e")
|
|
included_tree.heading("Path", text="Full Path", anchor="w", command=lambda: _sort_treeview_column(included_tree, "Path", False))
|
|
included_tree.column("Path", width=500, anchor="w") # Stretch enabled by default
|
|
|
|
included_scrollbar_y = ttk.Scrollbar(included_frame, orient="vertical", command=included_tree.yview)
|
|
included_scrollbar_x = ttk.Scrollbar(included_frame, orient="horizontal", command=included_tree.xview)
|
|
included_tree.configure(yscrollcommand=included_scrollbar_y.set, xscrollcommand=included_scrollbar_x.set)
|
|
|
|
included_tree.grid(row=0, column=0, sticky="nsew")
|
|
included_scrollbar_y.grid(row=0, column=1, sticky="ns")
|
|
included_scrollbar_x.grid(row=1, column=0, sticky="ew")
|
|
included_frame.grid_rowconfigure(0, weight=1)
|
|
included_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
for file_name, size_mb, file_path in included_files:
|
|
included_tree.insert("", "end", values=(file_name, f"{size_mb:.2f}", file_path))
|
|
|
|
# --- Excluded Files Tab ---
|
|
excluded_frame = ttk.Frame(tab_control, padding=10)
|
|
tab_control.add(excluded_frame, text=f"Excluded Files ({len(excluded_files)})")
|
|
|
|
cols_excluded = ("Name", "Size (MB)", "Path")
|
|
excluded_tree = ttk.Treeview(excluded_frame, columns=cols_excluded, show="headings", height=15)
|
|
|
|
excluded_tree.heading("Name", text="Name", anchor="w", command=lambda: _sort_treeview_column(excluded_tree, "Name", False))
|
|
excluded_tree.column("Name", width=200, stretch=tk.NO, anchor="w")
|
|
excluded_tree.heading("Size (MB)", text="Size (MB)", anchor="e", command=lambda: _sort_treeview_column(excluded_tree, "Size (MB)", False))
|
|
excluded_tree.column("Size (MB)", width=100, stretch=tk.NO, anchor="e")
|
|
excluded_tree.heading("Path", text="Full Path", anchor="w", command=lambda: _sort_treeview_column(excluded_tree, "Path", False))
|
|
excluded_tree.column("Path", width=500, anchor="w")
|
|
|
|
excluded_scrollbar_y = ttk.Scrollbar(excluded_frame, orient="vertical", command=excluded_tree.yview)
|
|
excluded_scrollbar_x = ttk.Scrollbar(excluded_frame, orient="horizontal", command=excluded_tree.xview)
|
|
excluded_tree.configure(yscrollcommand=excluded_scrollbar_y.set, xscrollcommand=excluded_scrollbar_x.set)
|
|
|
|
excluded_tree.grid(row=0, column=0, sticky="nsew")
|
|
excluded_scrollbar_y.grid(row=0, column=1, sticky="ns")
|
|
excluded_scrollbar_x.grid(row=1, column=0, sticky="ew")
|
|
excluded_frame.grid_rowconfigure(0, weight=1)
|
|
excluded_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
for file_name, size_mb, file_path in excluded_files:
|
|
excluded_tree.insert("", "end", values=(file_name, f"{size_mb:.2f}", file_path))
|
|
|
|
tab_control.pack(expand=True, fill="both", padx=5, pady=5)
|
|
|
|
# --- Totals ---
|
|
included_total_size_mb = sum(size for _, size, _ in included_files)
|
|
excluded_total_size_mb = sum(size for _, size, _ in excluded_files)
|
|
|
|
summary_text = (
|
|
f"Included: {len(included_files)} files, {included_total_size_mb:.2f} MB | "
|
|
f"Excluded: {len(excluded_files)} files, {excluded_total_size_mb:.2f} MB"
|
|
)
|
|
total_label = ttk.Label(dialog, text=summary_text, padding=(0, 5, 0, 10))
|
|
total_label.pack(fill=tk.X)
|
|
|
|
close_button = ttk.Button(dialog, text="Close", command=dialog.destroy)
|
|
close_button.pack(pady=(0,10))
|
|
|
|
|
|
def show_extension_stats_dialog(
|
|
parent: tk.Tk,
|
|
extension_stats: Dict[str, Dict[str, Any]], # {'ext': {'count': int, 'size': float_mb}}
|
|
title: str = "File Extension Statistics"
|
|
) -> None:
|
|
"""
|
|
Displays a dialog showing file statistics grouped by extension.
|
|
Derived from BackupApp.show_extension_data.
|
|
"""
|
|
dialog = Toplevel(parent)
|
|
dialog.title(title)
|
|
center_window(parent, dialog, 550, 350) # Adjusted size
|
|
dialog.grab_set() # Modal behavior
|
|
|
|
container = ttk.Frame(dialog, padding=10)
|
|
container.pack(fill="both", expand=True)
|
|
|
|
cols = ("Extension", "File Count", "Total Size (MB)")
|
|
tree = ttk.Treeview(container, columns=cols, show="headings", height=10)
|
|
|
|
tree.heading("Extension", text="Extension", anchor="w", command=lambda: _sort_treeview_column(tree, "Extension", False))
|
|
tree.column("Extension", width=150, stretch=tk.NO, anchor="w")
|
|
tree.heading("File Count", text="File Count", anchor="e", command=lambda: _sort_treeview_column(tree, "File Count", False))
|
|
tree.column("File Count", width=100, stretch=tk.NO, anchor="e")
|
|
tree.heading("Total Size (MB)", text="Total Size (MB)", anchor="e", command=lambda: _sort_treeview_column(tree, "Total Size (MB)", False))
|
|
tree.column("Total Size (MB)", width=150, stretch=tk.NO, anchor="e")
|
|
|
|
scrollbar_y = ttk.Scrollbar(container, orient="vertical", command=tree.yview)
|
|
tree.configure(yscrollcommand=scrollbar_y.set)
|
|
|
|
tree.grid(row=0, column=0, sticky="nsew")
|
|
scrollbar_y.grid(row=0, column=1, sticky="ns")
|
|
container.grid_rowconfigure(0, weight=1)
|
|
container.grid_columnconfigure(0, weight=1)
|
|
|
|
if extension_stats:
|
|
for ext, data in extension_stats.items():
|
|
count = data.get('count', 0)
|
|
size_mb = data.get('size', 0.0)
|
|
tree.insert("", "end", values=(ext, count, f"{size_mb:.2f}"))
|
|
else:
|
|
tree.insert("", "end", values=("No data available", "", ""))
|
|
|
|
|
|
close_button = ttk.Button(dialog, text="Close", command=dialog.destroy)
|
|
close_button.pack(pady=(10,10))
|
|
|
|
|
|
def show_exclusion_selector_dialog(
|
|
parent: tk.Tk,
|
|
existing_exclusions: List[str],
|
|
on_add: Callable[[List[str]], None]
|
|
) -> None:
|
|
"""
|
|
Shows a dialog that lets the user select groups/individual patterns from
|
|
DEFAULT_EXCLUSION_GROUPS. Calls `on_add` with a list of selected patterns
|
|
when the user clicks Add.
|
|
"""
|
|
dialog = Toplevel(parent)
|
|
dialog.title("Default Exclusions")
|
|
center_window(parent, dialog, 700, 520)
|
|
dialog.grab_set()
|
|
|
|
container = ttk.Frame(dialog, padding=10)
|
|
container.pack(fill="both", expand=True)
|
|
|
|
# Canvas + scrollable frame for many groups
|
|
canvas = tk.Canvas(container)
|
|
scrollbar = ttk.Scrollbar(container, orient="vertical", command=canvas.yview)
|
|
scroll_frame = ttk.Frame(canvas)
|
|
|
|
scroll_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
|
canvas.create_window((0, 0), window=scroll_frame, anchor="nw")
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
|
|
canvas.grid(row=0, column=0, sticky="nsew")
|
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
|
container.grid_rowconfigure(0, weight=1)
|
|
container.grid_columnconfigure(0, weight=1)
|
|
|
|
# Structure to hold BooleanVars
|
|
pattern_vars: Dict[str, tk.BooleanVar] = {}
|
|
group_vars: Dict[str, List[str]] = {}
|
|
|
|
# Layout groups in a responsive grid and place checkbuttons in multiple columns
|
|
groups = list(DEFAULT_EXCLUSION_GROUPS.items())
|
|
groups_per_row = 2 # Number of group columns to show across the dialog
|
|
checkbox_cols = 3 # Number of checkbox columns inside each group
|
|
|
|
# Ensure the scroll_frame grid will expand columns evenly
|
|
for c in range(groups_per_row):
|
|
scroll_frame.grid_columnconfigure(c, weight=1)
|
|
|
|
for idx, (group_name, patterns) in enumerate(groups):
|
|
row = idx // groups_per_row
|
|
col = idx % groups_per_row
|
|
gf = ttk.LabelFrame(scroll_frame, text=group_name, padding=6)
|
|
gf.grid(row=row, column=col, sticky="nsew", padx=6, pady=6)
|
|
|
|
# Buttons select all / clear for this group (placed at top-right)
|
|
btn_frame = ttk.Frame(gf)
|
|
btn_frame.grid(row=0, column=0, sticky="e", columnspan=checkbox_cols)
|
|
|
|
def make_select_all(local_patterns):
|
|
return lambda: [pattern_vars[p].set(True) for p in local_patterns if p in pattern_vars]
|
|
|
|
def make_clear(local_patterns):
|
|
return lambda: [pattern_vars[p].set(False) for p in local_patterns if p in pattern_vars]
|
|
|
|
ttk.Button(btn_frame, text="Clear", width=8, command=make_clear(patterns)).pack(side="right", padx=2)
|
|
ttk.Button(btn_frame, text="Select All", width=10, command=make_select_all(patterns)).pack(side="right", padx=2)
|
|
|
|
# Checkbuttons arranged in a grid to fill available width
|
|
cb_frame = ttk.Frame(gf)
|
|
cb_frame.grid(row=1, column=0, sticky="nsew", pady=(6,0))
|
|
group_vars[group_name] = []
|
|
for i, p in enumerate(patterns):
|
|
# Normalize pattern whitespace
|
|
pat = p.strip()
|
|
var = tk.BooleanVar(value=False)
|
|
pattern_vars[pat] = var
|
|
group_vars[group_name].append(pat)
|
|
r = i // checkbox_cols
|
|
c = i % checkbox_cols
|
|
cb = ttk.Checkbutton(cb_frame, text=pat, variable=var)
|
|
cb.grid(row=r, column=c, sticky="w", padx=4, pady=2)
|
|
|
|
# Make checkbox columns expand evenly
|
|
for cidx in range(checkbox_cols):
|
|
cb_frame.grid_columnconfigure(cidx, weight=1)
|
|
|
|
# Bottom controls: Select All groups / Clear All / Add / Cancel
|
|
controls = ttk.Frame(dialog, padding=(10,6))
|
|
controls.pack(fill="x")
|
|
|
|
def select_all_groups():
|
|
for v in pattern_vars.values():
|
|
v.set(True)
|
|
|
|
def clear_all_groups():
|
|
for v in pattern_vars.values():
|
|
v.set(False)
|
|
|
|
ttk.Button(controls, text="Select All", command=select_all_groups).pack(side="left", padx=4)
|
|
ttk.Button(controls, text="Clear All", command=clear_all_groups).pack(side="left", padx=4)
|
|
|
|
spacer = ttk.Label(controls, text="")
|
|
spacer.pack(side="left", expand=True)
|
|
|
|
def on_add_click():
|
|
selected = [p for p, v in pattern_vars.items() if v.get()]
|
|
if not selected:
|
|
messagebox.showinfo("No Selection", "No exclusions selected to add.", parent=dialog)
|
|
return
|
|
dialog.destroy()
|
|
try:
|
|
on_add(selected)
|
|
except Exception:
|
|
# Swallow exceptions from callback to avoid crashing UI
|
|
pass
|
|
|
|
ttk.Button(controls, text="Add", style="Accent.TButton", command=on_add_click).pack(side="right", padx=6)
|
|
ttk.Button(controls, text="Cancel", command=dialog.destroy).pack(side="right")
|
|
|
|
|
|
|
|
def show_backup_confirmation_dialog(
|
|
parent: tk.Tk,
|
|
included_files_count: int,
|
|
included_total_size_mb: float,
|
|
on_proceed: Callable[[], None], # Callback function if user clicks "Proceed"
|
|
on_show_file_details: Callable[[], None],
|
|
on_show_included_ext_stats: Callable[[], None],
|
|
on_show_excluded_ext_stats: Callable[[], None]
|
|
) -> None:
|
|
"""
|
|
Displays a confirmation dialog before starting the backup.
|
|
Derived from BackupApp.show_confirmation.
|
|
"""
|
|
dialog = Toplevel(parent)
|
|
dialog.title("Confirm Backup Operation")
|
|
center_window(parent, dialog, 450, 220) # Adjusted size
|
|
dialog.protocol("WM_DELETE_WINDOW", dialog.destroy) # Handle window close button
|
|
dialog.grab_set()
|
|
|
|
main_frame = ttk.Frame(dialog, padding=15)
|
|
main_frame.pack(expand=True, fill="both")
|
|
|
|
ttk.Label(main_frame, text=f"The backup will include {included_files_count} files.", font=("Arial", 11)).pack(pady=(0, 5))
|
|
ttk.Label(main_frame, text=f"Total estimated size: {included_total_size_mb:.2f} MB", font=("Arial", 11)).pack(pady=(0, 15))
|
|
|
|
details_frame = ttk.Frame(main_frame)
|
|
details_frame.pack(pady=(0, 15))
|
|
|
|
ttk.Button(details_frame, text="File List Details", command=on_show_file_details).pack(side="left", padx=5)
|
|
ttk.Button(details_frame, text="Included Ext. Stats", command=on_show_included_ext_stats).pack(side="left", padx=5)
|
|
ttk.Button(details_frame, text="Excluded Ext. Stats", command=on_show_excluded_ext_stats).pack(side="left", padx=5)
|
|
|
|
action_buttons_frame = ttk.Frame(main_frame)
|
|
action_buttons_frame.pack(pady=(10,0))
|
|
|
|
def _proceed_action():
|
|
dialog.destroy()
|
|
on_proceed()
|
|
|
|
proceed_button = ttk.Button(action_buttons_frame, text="Proceed with Backup", command=_proceed_action, style="Accent.TButton")
|
|
proceed_button.pack(side="left", padx=10)
|
|
|
|
cancel_button = ttk.Button(action_buttons_frame, text="Cancel", command=dialog.destroy)
|
|
cancel_button.pack(side="left", padx=10)
|
|
|
|
# Set focus to proceed button
|
|
proceed_button.focus_set()
|
|
dialog.bind("<Return>", lambda e: _proceed_action())
|
|
dialog.bind("<Escape>", lambda e: dialog.destroy()) |