""" Progress dialog with progress bar, ETA, and cancel button. """ import tkinter as tk from tkinter import ttk import time import threading class ProgressDialog: """ A modal progress dialog with progress bar, status text, ETA, and cancel button. Thread-safe for updating from background threads. """ def __init__(self, parent, title="Progress"): self.parent = parent self.cancelled = False self._start_time = time.time() # Create dialog self.dialog = tk.Toplevel(parent) self.dialog.title(title) self.dialog.geometry("500x150") self.dialog.resizable(False, False) # Make it modal self.dialog.transient(parent) self.dialog.grab_set() # Center on parent self.dialog.update_idletasks() x = parent.winfo_x() + (parent.winfo_width() - 500) // 2 y = parent.winfo_y() + (parent.winfo_height() - 150) // 2 self.dialog.geometry(f"+{x}+{y}") # Status label self.status_var = tk.StringVar(value="Starting...") ttk.Label(self.dialog, textvariable=self.status_var, font=("Segoe UI", 10)).pack(pady=(15, 5)) # Progress bar self.progress = ttk.Progressbar(self.dialog, length=450, mode='determinate') self.progress.pack(pady=(5, 5), padx=25) # ETA label self.eta_var = tk.StringVar(value="Calculating...") ttk.Label(self.dialog, textvariable=self.eta_var, font=("Segoe UI", 9), foreground="#666").pack(pady=(0, 10)) # Cancel button self.cancel_btn = ttk.Button(self.dialog, text="Cancel", command=self.cancel) self.cancel_btn.pack(pady=(0, 15)) # Handle window close self.dialog.protocol("WM_DELETE_WINDOW", self.cancel) def update(self, current, total, status_text=None): """ Update progress bar and ETA. Thread-safe - can be called from background threads. Args: current: Current item number (0-based or 1-based) total: Total number of items status_text: Optional status text to display """ def _update(): if self.cancelled: return # Update progress bar if total > 0: percentage = (current / total) * 100 self.progress['value'] = percentage # Update status text if status_text: self.status_var.set(status_text) # Calculate and update ETA if current > 0 and total > 0: elapsed = time.time() - self._start_time avg_time_per_item = elapsed / current remaining_items = total - current eta_seconds = remaining_items * avg_time_per_item if eta_seconds < 60: eta_text = f"ETA: {int(eta_seconds)}s" elif eta_seconds < 3600: eta_text = f"ETA: {int(eta_seconds / 60)}m {int(eta_seconds % 60)}s" else: hours = int(eta_seconds / 3600) minutes = int((eta_seconds % 3600) / 60) eta_text = f"ETA: {hours}h {minutes}m" self.eta_var.set(f"{current}/{total} items - {eta_text}") else: self.eta_var.set(f"{current}/{total} items") # Schedule update on main thread if threading.current_thread() == threading.main_thread(): _update() else: self.dialog.after(0, _update) def cancel(self): """Mark as cancelled and close dialog.""" self.cancelled = True self.close() def close(self): """Close the dialog.""" def _close(): try: self.dialog.grab_release() self.dialog.destroy() except: pass # Schedule on main thread if threading.current_thread() == threading.main_thread(): _close() else: self.dialog.after(0, _close) def is_cancelled(self): """Check if user cancelled the operation.""" return self.cancelled