Don't want to need this so Fix this Exception in Tkinter callback Traceback (most recent call last):   File "C:\Users\picky\AppData\Local\Programs\Python\Python310\lib\tkinter\__init__.py", line 1921, in __call__     return self.func(*args)   File "D:\cookie monster\DeepAscension Video2Image+.7z-20241113T104527Z-001\video.py", line 335, in on_closing     if Messagebox.askokcancel("Quit", "Do you want to quit?"): AttributeError: type object 'Messagebox' has no attribute 'askokcancel' Exception in Tkinter callback Traceback (most recent call last):   File "C:\Users\picky\AppData\Local\Programs\Python\Python310\lib\tkinter\__init__.py", line 1921, in __call__     return self.func(*args)   File "D:\cookie monster\DeepAscension Video2Image+.7z-20241113T104527Z-001\video.py", line 335, in on_closing     if Messagebox.askokcancel("Quit", "Do you want to quit?"): AttributeError: type object 'Messagebox' has no attribute 'askokcancel' here is code import ttkbootstrap as tb
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox
from tkinter import filedialog
import os
import subprocess
import threading
import concurrent.futures
import queue
import psutil
import sys

# Constants for the application
APP_NAME = "DeepAscension Video2Image+"
DEFAULT_MAX_CONCURRENT_TASKS = 4
DISK_CHECK_INTERVAL = 5000  # in milliseconds

# Initialize the main window with ttkbootstrap
root = tb.Window(themename="darkly")
root.title(APP_NAME)
root.geometry("1200x800")
root.resizable(True, True)  # Allow resizing for better usability

# Initialize the lists to store input folders and videos
input_folders = []
input_videos = []

# Variable to store output folder path
output_folder_var = tb.StringVar()

# Variable for disk space threshold
disk_space_threshold_mb = 500  # Default to 500 MB

# Variable for output format
output_format_var = tb.StringVar(value="jpg")  # Default to 'jpg'

# Pause state
is_paused = False

# Event to signal stopping of all tasks
stop_all_event = threading.Event()

# Queue for inter-thread communication
progress_queue = queue.Queue()

# List to collect errors
errors = []

# Configure styles using ttkbootstrap
style = tb.Style()

# ================================
# Function Definitions
# ================================

def remove_selected_items():
    """Remove selected items from the Treeview and corresponding lists."""
    selected_items = tree.selection()
    for item in selected_items:
        item_type = tree.set(item, "Type")
        if item_type == "Folder":
            input_folders.remove(item)
        elif item_type == "Video File":
            input_videos.remove(item)
        tree.delete(item)
    update_remove_button()

def select_input_folders():
    """Allow user to select multiple input folders."""
    while True:
        folder = filedialog.askdirectory(title="Select Input Folder (Cancel to finish)")
        if folder:
            if folder not in input_folders and folder not in input_videos:
                input_folders.append(folder)
                tree.insert('', 'end', iid=folder, values=(folder, "Folder", "Pending"))
            else:
                Messagebox.show_warning("Duplicate Entry", f"The entry '{folder}' is already selected.")
        else:
            break

def select_input_videos():
    """Allow user to select multiple video files."""
    files = filedialog.askopenfilenames(title="Select Video Files",
                                        filetypes=[("Video Files", "*.mp4 *.avi *.mov *.mkv *.flv *.wmv")])
    for file in files:
        if file not in input_videos and file not in input_folders:
            input_videos.append(file)
            tree.insert('', 'end', iid=file, values=(file, "Video File", "Pending"))
        else:
            Messagebox.show_warning("Duplicate Entry", f"The file '{file}' is already selected.")

def select_output_folder():
    """Allow user to select an output folder."""
    folder = filedialog.askdirectory(title="Select Output Folder")
    if folder:
        output_folder_var.set(folder)
        output_entry.config(state='normal')
        output_entry.delete(0, tb.END)
        output_entry.insert(0, folder)
        output_entry.config(state='readonly')

def sanitize_folder_name(name):
    """Sanitize the folder name to remove or replace problematic characters."""
    return "".join(c if c.isalnum() or c in (' ', '_', '-') else "_" for c in name).replace(" ", "_")

def process_videos_thread():
    """Run the video processing in a separate thread."""
    threading.Thread(target=process_videos, daemon=True).start()

def process_videos():
    """Main function to process videos and folders."""
    global is_paused
    output_folder = output_folder_var.get()

    if not input_folders and not input_videos:
        Messagebox.show_error("Error", "Please select at least one input folder or video file.")
        set_buttons_state(NORMAL)
        return
    if not output_folder:
        Messagebox.show_error("Error", "Please select an output folder.")
        set_buttons_state(NORMAL)
        return

    # Ensure the output directory exists
    os.makedirs(output_folder, exist_ok=True)

    # Disable the process button and folder selection buttons to prevent multiple clicks
    set_buttons_state(DISABLED)

    # Initialize overall progress bar
    total_tasks = len(input_folders) + len(input_videos)
    progress_bar['maximum'] = total_tasks
    progress_bar['value'] = 0
    status_label.config(text="Starting processing...")

    # Clear previous errors
    global errors
    errors = []

    # Create a combined tasks queue
    tasks_queue = queue.Queue()
    for folder in input_folders:
        tasks_queue.put(("Folder", folder))
    for video in input_videos:
        tasks_queue.put(("Video File", video))

    # Determine optimal number of workers based on CPU cores
    max_workers = min(os.cpu_count() * 2 or DEFAULT_MAX_CONCURRENT_TASKS, 32)

    def worker(task_type, path):
        """Worker function to process individual tasks."""
        while is_paused:
            if stop_all_event.is_set():
                return
            threading.Event().wait(0.1)
        if stop_all_event.is_set():
            return

        # Update Treeview status to "Processing"
        progress_queue.put(("status", path, "Processing"))

        base_name = os.path.basename(os.path.normpath(path))
        sanitized_base = sanitize_folder_name(base_name)
        if task_type == "Folder":
            unique_output_dir = os.path.join(output_folder, f"{sanitized_base}_{base_name}")
            input_path = path
        elif task_type == "Video File":
            name, _ = os.path.splitext(base_name)
            unique_output_dir = os.path.join(output_folder, f"{sanitize_folder_name(name)}_output")
            input_path = path
        os.makedirs(unique_output_dir, exist_ok=True)
        try:
            # Retrieve the selected output format
            selected_format = output_format_var.get()

            # Run the video2image command with the unique output directory and selected format
            subprocess.run(
                ['video2image', '-i', input_path, '-o', unique_output_dir, '-f', selected_format],
                check=True,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL
            )
            progress_queue.put(("success", path))
        except subprocess.CalledProcessError as e:
            error_msg = f"Failed to process {task_type.lower()}: {path}\nError Code: {e.returncode}"
            progress_queue.put(("error", path, error_msg))
        except Exception as ex:
            error_msg = f"An unexpected error occurred while processing {task_type.lower()}: {path}\nError: {ex}"
            progress_queue.put(("error", path, error_msg))

    # Initialize ThreadPoolExecutor
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {}
        for _ in range(min(max_workers, tasks_queue.qsize())):
            if not tasks_queue.empty():
                task_type, path = tasks_queue.get()
                futures[executor.submit(worker, task_type, path)] = path

        while futures and not stop_all_event.is_set():
            done, _ = concurrent.futures.wait(futures, return_when=concurrent.futures.FIRST_COMPLETED)
            for future in done:
                folder = futures.pop(future)
                try:
                    future.result()
                except Exception as e:
                    error_msg = f"Error processing {folder}\nError: {e}"
                    progress_queue.put(("error", folder, error_msg))
                if not tasks_queue.empty() and not is_paused and not stop_all_event.is_set():
                    task_type, next_path = tasks_queue.get()
                    futures[executor.submit(worker, task_type, next_path)] = next_path

    # Finalize processing
    finalize_processing()

    # Start disk space monitoring
    monitor_disk_space()

def set_buttons_state(state):
    """Enable or disable buttons based on the provided state."""
    process_button.config(state=state)
    select_input_button.config(state=state)
    select_output_button.config(state=state)
    select_videos_button.config(state=state)
    remove_button.config(state=NORMAL if (input_folders or input_videos) and state == NORMAL else DISABLED)
    pause_button.config(state=state if state == NORMAL else DISABLED)
    resume_button.config(state=DISABLED if state == NORMAL else NORMAL)
    output_format_combobox.config(state=DISABLED if state == DISABLED else NORMAL)

def on_pause():
    """Pause the processing."""
    global is_paused
    is_paused = True
    status_label.config(text="Processing paused.")
    pause_button.config(state=DISABLED)
    resume_button.config(state=NORMAL)

def on_resume():
    """Resume the processing."""
    global is_paused
    is_paused = False
    status_label.config(text="Resuming processing...")
    pause_button.config(state=NORMAL)
    resume_button.config(state=DISABLED)
    # No need to start a new thread; existing workers will continue

def monitor_disk_space():
    """Monitor disk space and auto-pause if below threshold."""
    if stop_all_event.is_set():
        return

    output_path = output_folder_var.get() or '/'
    try:
        total, used, free = psutil.disk_usage(output_path)
        if free < disk_space_threshold_mb * 1024 * 1024:
            if not is_paused:
                on_pause()
                Messagebox.show_warning("Auto-Pause", "Disk space below threshold. Processing has been paused.")
    except Exception as e:
        Messagebox.show_error("Disk Space Error", f"Unable to determine disk space for '{output_path}'.\nError: {e}")
    if not stop_all_event.is_set():
        root.after(DISK_CHECK_INTERVAL, monitor_disk_space)

def set_disk_threshold():
    """Set the disk space threshold for auto-pausing."""
    try:
        value = int(disk_threshold_entry.get())
        if value <= 0:
            raise ValueError
        global disk_space_threshold_mb
        disk_space_threshold_mb = value
        Messagebox.show_info("Threshold Set", f"Auto-pause threshold set to {value} MB.")
    except ValueError:
        Messagebox.show_error("Invalid Input", "Please enter a valid positive integer for the disk space threshold.")

def update_remove_button():
    """Enable or disable the remove button based on folder/video selection."""
    if input_folders or input_videos:
        remove_button.config(state=NORMAL)
    else:
        remove_button.config(state=DISABLED)

def finalize_processing():
    """Finalize the processing by updating the UI and displaying any errors."""
    set_buttons_state(NORMAL)
    if errors:
        error_message = "\n\n".join(errors)
        Messagebox.show_error("Processing Completed with Errors", error_message)
    else:
        Messagebox.show_success("Success", "All videos have been successfully processed.")
    progress_bar['value'] = 0
    status_label.config(text="Processing completed.")

def update_progress():
    """Update the progress bar and status labels based on messages from worker threads."""
    try:
        while True:
            msg = progress_queue.get_nowait()
            if msg[0] == "status":
                _, path, status = msg
                tree.set(path, "Status", status)
            elif msg[0] == "success":
                _, path = msg
                tree.set(path, "Status", "Completed")
                progress_bar['value'] += 1
            elif msg[0] == "error":
                _, path, error_msg = msg
                tree.set(path, "Status", "Error")
                errors.append(error_msg)
                progress_bar['value'] += 1

            # Update overall progress
            overall = progress_bar['value']
            total = progress_bar['maximum']
            status_label.config(text=f"Processed {overall} of {total} tasks.")

            if overall >= total:
                finalize_processing()
    except queue.Empty:
        pass
    if progress_bar['value'] < progress_bar['maximum']:
        root.after(100, update_progress)

def monitor_input_folders_and_videos():
    """Periodically check if input_folders or input_videos is empty to enable/disable the remove button."""
    update_remove_button()
    root.after(500, monitor_input_folders_and_videos)

def periodic_update():
    """Schedule periodic updates for progress."""
    update_progress()
    root.after(100, periodic_update)

def on_closing():
    """Handle window closing to ensure all threads are properly terminated."""
    if Messagebox.askokcancel("Quit", "Do you want to quit?"):
        stop_all_event.set()
        root.destroy()

# ================================
# GUI Component Creation
# ================================

# Frame for Input Folders and Videos
input_frame = tb.LabelFrame(root, text="Input Folders and Videos")
input_frame.pack(fill="both", expand=True, padx=20, pady=10)

# Treeview to display selected input folders and videos with their statuses
columns = ("Path", "Type", "Status")
tree = tb.Treeview(input_frame, columns=columns, show='headings', selectmode='extended', height=15)
tree.heading("Path", text="Path")
tree.heading("Type", text="Type")
tree.heading("Status", text="Status")
tree.column("Path", anchor='w', width=800)
tree.column("Type", anchor='center', width=100)
tree.column("Status", anchor='center', width=200)
tree.pack(side=tb.LEFT, fill=tb.BOTH, expand=True, padx=(0,5), pady=5)

# Scrollbar for the Treeview
tree_scroll = tb.Scrollbar(input_frame, orient=tb.VERTICAL, command=tree.yview)
tree_scroll.pack(side=tb.RIGHT, fill=tb.Y)
tree.config(yscrollcommand=tree_scroll.set)

# Frame for Input Buttons
input_button_frame = tb.Frame(root)
input_button_frame.pack(fill='x', padx=20, pady=(0,10))

# Button to select input folders
select_input_button = tb.Button(
    input_button_frame, 
    text="Select Input Folders", 
    bootstyle=PRIMARY, 
    command=lambda: threading.Thread(target=select_input_folders, daemon=True).start()
)
select_input_button.pack(side=tb.LEFT, padx=5)

# Button to select video files
select_videos_button = tb.Button(
    input_button_frame, 
    text="Select Videos", 
    bootstyle=PRIMARY, 
    command=lambda: threading.Thread(target=select_input_videos, daemon=True).start()
)
select_videos_button.pack(side=tb.LEFT, padx=5)

# Button to remove selected items
remove_button = tb.Button(
    input_button_frame, 
    text="Remove Selected", 
    bootstyle=DANGER, 
    command=remove_selected_items, 
    state=DISABLED
)
remove_button.pack(side=tb.LEFT, padx=5)

# Frame for Output Folder
output_frame = tb.LabelFrame(root, text="Output Folder")
output_frame.pack(fill="both", expand=True, padx=20, pady=10)

# Entry widget to display selected output folder
output_entry = tb.Entry(output_frame, textvariable=output_folder_var, width=80, state='readonly')
output_entry.pack(side=tb.LEFT, padx=(0, 5), expand=True, fill=tb.X)

# Button to select output folder
select_output_button = tb.Button(
    output_frame, 
    text="Select Output Folder", 
    bootstyle=PRIMARY, 
    command=select_output_folder
)
select_output_button.pack(side=tb.LEFT, padx=5)

# Frame for Output Format Selection
format_frame = tb.LabelFrame(root, text="Output Format")
format_frame.pack(fill="x", expand=True, padx=20, pady=10)

format_label = tb.Label(format_frame, text="Select Output Format:")
format_label.pack(side=tb.LEFT, padx=(0,5))

output_format_combobox = tb.Combobox(
    format_frame, 
    textvariable=output_format_var, 
    values=["jpg", "png"], 
    state="readonly"
)
output_format_combobox.pack(side=tb.LEFT, padx=5)
output_format_combobox.current(0)  # Set default to 'jpg'

# Frame for Disk Space Threshold
disk_frame = tb.LabelFrame(root, text="Auto-Pause Disk Space Threshold (MB)")
disk_frame.pack(fill="x", expand=True, padx=20, pady=10)

disk_threshold_entry = tb.Entry(disk_frame, width=10, justify='center')
disk_threshold_entry.insert(0, str(disk_space_threshold_mb))
disk_threshold_entry.pack(side=tb.LEFT, padx=(0,5))

set_threshold_button = tb.Button(
    disk_frame, 
    text="Set Threshold", 
    bootstyle=SUCCESS, 
    command=set_disk_threshold
)
set_threshold_button.pack(side=tb.LEFT, padx=5)

# Progress Frame with Overall Progress Bar
progress_frame = tb.Frame(root)
progress_frame.pack(fill='x', padx=20, pady=10)

progress_label = tb.Label(progress_frame, text="Overall Progress:")
progress_label.pack(anchor='w')

progress_bar = tb.Progressbar(
    progress_frame, 
    orient='horizontal', 
    length=1000, 
    mode='determinate', 
    bootstyle="info"
)
progress_bar.pack(pady=5, fill=tb.X)

# Status Label
status_label = tb.Label(root, text="Ready.", relief=RAISED, anchor='w', bootstyle="secondary")
status_label.pack(fill='x', padx=20, pady=(0,10))

# Frame for Control Buttons
control_button_frame = tb.Frame(root)
control_button_frame.pack(fill='x', padx=20, pady=10)

# Process Button
process_button = tb.Button(
    control_button_frame, 
    text="Process Videos", 
    bootstyle=SUCCESS, 
    command=process_videos_thread
)
process_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)

# Pause Button
pause_button = tb.Button(
    control_button_frame, 
    text="Pause", 
    bootstyle=WARNING, 
    command=on_pause, 
    state=DISABLED
)
pause_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)

# Resume Button
resume_button = tb.Button(
    control_button_frame, 
    text="Resume", 
    bootstyle=INFO, 
    command=on_resume, 
    state=DISABLED
)
resume_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)

# ================================
# Start Monitoring and GUI Loop
# ================================

# Start monitoring input selections and progress
monitor_input_folders_and_videos()
periodic_update()

# Handle window closing to ensure all threads are properly terminated
root.protocol("WM_DELETE_WINDOW", on_closing)

# Run the GUI loop
root.mainloop()