Files
PartDB_Helper_App/ui/progress_dialog.py

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