128 lines
4.3 KiB
Python
128 lines
4.3 KiB
Python
"""
|
|
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
|