# 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] 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_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("", lambda e: _proceed_action()) dialog.bind("", lambda e: dialog.destroy())