Enhanced player stability and added exit confirmation
- Fixed VLC display errors by implementing PIL fallback for images - Added comprehensive timer management with individual error handling - Implemented watchdog timers to prevent freezing during media transitions - Enhanced exit functionality with quickconnect code confirmation dialog - Improved settings window behavior with proper modal focus - Added transition protection to prevent rapid media cycling - Enhanced error handling throughout the application - Fixed controls window cleanup and destruction process
This commit is contained in:
@@ -12,13 +12,33 @@ class AppSettingsWindow(tk.Tk):
|
|||||||
self.geometry('440x600') # Increased height for better button visibility
|
self.geometry('440x600') # Increased height for better button visibility
|
||||||
self.resizable(False, False)
|
self.resizable(False, False)
|
||||||
self.config(bg='#23272e')
|
self.config(bg='#23272e')
|
||||||
|
|
||||||
|
# Ensure window appears on top and gets focus
|
||||||
self.attributes('-topmost', True)
|
self.attributes('-topmost', True)
|
||||||
|
self.lift()
|
||||||
self.focus_force()
|
self.focus_force()
|
||||||
|
self.grab_set() # Make window modal
|
||||||
|
|
||||||
|
# Center the window on screen
|
||||||
|
self.center_window()
|
||||||
|
|
||||||
self.fields = {}
|
self.fields = {}
|
||||||
self.load_config()
|
self.load_config()
|
||||||
self.style = ttk.Style(self)
|
self.style = ttk.Style(self)
|
||||||
self.set_styles()
|
self.set_styles()
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
|
|
||||||
|
# Ensure focus after widgets are created
|
||||||
|
self.after(100, self.focus_force)
|
||||||
|
|
||||||
|
def center_window(self):
|
||||||
|
"""Center the settings window on the screen"""
|
||||||
|
self.update_idletasks()
|
||||||
|
width = self.winfo_width()
|
||||||
|
height = self.winfo_height()
|
||||||
|
x = (self.winfo_screenwidth() // 2) - (width // 2)
|
||||||
|
y = (self.winfo_screenheight() // 2) - (height // 2)
|
||||||
|
self.geometry(f'{width}x{height}+{x}+{y}')
|
||||||
|
|
||||||
def set_styles(self):
|
def set_styles(self):
|
||||||
self.style.theme_use('clam')
|
self.style.theme_use('clam')
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ class SimpleTkPlayer:
|
|||||||
self.paused = False
|
self.paused = False
|
||||||
self.pause_timer = None
|
self.pause_timer = None
|
||||||
self.is_transitioning = False # Flag to prevent rapid cycling
|
self.is_transitioning = False # Flag to prevent rapid cycling
|
||||||
|
self.is_exiting = False # Flag to prevent operations during exit
|
||||||
|
|
||||||
|
# Initialize all timer variables to None
|
||||||
|
self.hide_controls_timer = None
|
||||||
|
self.video_watchdog = None
|
||||||
|
self.image_watchdog = None
|
||||||
|
self.image_timer = None
|
||||||
|
|
||||||
self.label = tk.Label(root, bg='black')
|
self.label = tk.Label(root, bg='black')
|
||||||
self.label.pack(fill=tk.BOTH, expand=True)
|
self.label.pack(fill=tk.BOTH, expand=True)
|
||||||
self.create_controls()
|
self.create_controls()
|
||||||
@@ -107,16 +115,25 @@ class SimpleTkPlayer:
|
|||||||
self.schedule_hide_controls()
|
self.schedule_hide_controls()
|
||||||
|
|
||||||
def hide_controls(self):
|
def hide_controls(self):
|
||||||
self.controls_win.withdraw()
|
try:
|
||||||
self.hide_mouse()
|
if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists():
|
||||||
|
self.controls_win.withdraw()
|
||||||
|
self.hide_mouse()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CONTROLS] Error hiding controls: {e}")
|
||||||
|
|
||||||
def schedule_hide_controls(self):
|
def schedule_hide_controls(self):
|
||||||
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
|
try:
|
||||||
self.root.after_cancel(self.hide_controls_timer)
|
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
|
||||||
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
|
self.root.after_cancel(self.hide_controls_timer)
|
||||||
|
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CONTROLS] Error scheduling hide controls: {e}")
|
||||||
|
self.hide_controls_timer = None
|
||||||
|
|
||||||
def on_activity(self, event=None):
|
def on_activity(self, event=None):
|
||||||
self.show_controls()
|
if not self.is_exiting:
|
||||||
|
self.show_controls()
|
||||||
|
|
||||||
def prev_media(self):
|
def prev_media(self):
|
||||||
self.current_index = (self.current_index - 1) % len(self.playlist)
|
self.current_index = (self.current_index - 1) % len(self.playlist)
|
||||||
@@ -203,9 +220,11 @@ class SimpleTkPlayer:
|
|||||||
max_duration = duration if duration is not None else 60 # fallback max 60s
|
max_duration = duration if duration is not None else 60 # fallback max 60s
|
||||||
self.video_watchdog = self.root.after(int(max_duration * 1200), watchdog)
|
self.video_watchdog = self.root.after(int(max_duration * 1200), watchdog)
|
||||||
def finish_video():
|
def finish_video():
|
||||||
if hasattr(self, 'video_watchdog'):
|
if hasattr(self, 'video_watchdog') and self.video_watchdog:
|
||||||
self.root.after_cancel(self.video_watchdog)
|
self.root.after_cancel(self.video_watchdog)
|
||||||
self.vlc_player.stop()
|
self.video_watchdog = None
|
||||||
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
|
self.vlc_player.stop()
|
||||||
self.video_canvas.pack_forget()
|
self.video_canvas.pack_forget()
|
||||||
self.label.pack(fill=tk.BOTH, expand=True)
|
self.label.pack(fill=tk.BOTH, expand=True)
|
||||||
if on_end:
|
if on_end:
|
||||||
@@ -253,11 +272,6 @@ class SimpleTkPlayer:
|
|||||||
elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
|
||||||
# Use PIL for images instead of VLC to avoid display issues
|
# Use PIL for images instead of VLC to avoid display issues
|
||||||
self.show_image_via_pil(file_path, duration if duration is not None else 10, on_end=self.next_media)
|
self.show_image_via_pil(file_path, duration if duration is not None else 10, on_end=self.next_media)
|
||||||
try:
|
|
||||||
self.show_image_via_vlc(file_path, duration if duration is not None else 10, on_end=self.next_media)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[PLAYER] VLC image display failed: {e}. Trying PIL fallback.")
|
|
||||||
self.show_image_via_pil(file_path, duration if duration is not None else 10, on_end=self.next_media)
|
|
||||||
else:
|
else:
|
||||||
print(f"[PLAYER] Unsupported file type: {media['file_name']}")
|
print(f"[PLAYER] Unsupported file type: {media['file_name']}")
|
||||||
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
|
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
|
||||||
@@ -308,6 +322,8 @@ class SimpleTkPlayer:
|
|||||||
self.label.config(image="", text="")
|
self.label.config(image="", text="")
|
||||||
if hasattr(self, 'photo'):
|
if hasattr(self, 'photo'):
|
||||||
del self.photo # Free memory
|
del self.photo # Free memory
|
||||||
|
if hasattr(self, 'image_timer'):
|
||||||
|
self.image_timer = None # Clear timer reference
|
||||||
if on_end:
|
if on_end:
|
||||||
on_end()
|
on_end()
|
||||||
|
|
||||||
@@ -395,11 +411,21 @@ class SimpleTkPlayer:
|
|||||||
on_end()
|
on_end()
|
||||||
self.image_watchdog = self.root.after(int(duration * 1200), watchdog)
|
self.image_watchdog = self.root.after(int(duration * 1200), watchdog)
|
||||||
def finish_image():
|
def finish_image():
|
||||||
if hasattr(self, 'image_watchdog'):
|
try:
|
||||||
self.root.after_cancel(self.image_watchdog)
|
if hasattr(self, 'image_watchdog') and self.image_watchdog:
|
||||||
self.vlc_player.stop()
|
self.root.after_cancel(self.image_watchdog)
|
||||||
self.video_canvas.pack_forget()
|
self.image_watchdog = None
|
||||||
self.label.pack(fill=tk.BOTH, expand=True)
|
except Exception as e:
|
||||||
|
print(f"[VLC] Error canceling image_watchdog: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
|
self.vlc_player.stop()
|
||||||
|
self.video_canvas.pack_forget()
|
||||||
|
self.label.pack(fill=tk.BOTH, expand=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[VLC] Error in finish_image cleanup: {e}")
|
||||||
|
|
||||||
if on_end:
|
if on_end:
|
||||||
on_end()
|
on_end()
|
||||||
self.root.after(int(duration * 1000), finish_image)
|
self.root.after(int(duration * 1000), finish_image)
|
||||||
@@ -417,13 +443,17 @@ class SimpleTkPlayer:
|
|||||||
self.current_index = (self.current_index + 1) % len(self.playlist)
|
self.current_index = (self.current_index + 1) % len(self.playlist)
|
||||||
print(f"[PLAYER] Moving to next media: index {self.current_index}")
|
print(f"[PLAYER] Moving to next media: index {self.current_index}")
|
||||||
|
|
||||||
# Clear any existing timers
|
# Clear any existing timers safely
|
||||||
if hasattr(self, 'video_watchdog'):
|
timer_names = ['video_watchdog', 'image_watchdog', 'image_timer']
|
||||||
self.root.after_cancel(self.video_watchdog)
|
for timer_name in timer_names:
|
||||||
if hasattr(self, 'image_watchdog'):
|
try:
|
||||||
self.root.after_cancel(self.image_watchdog)
|
if hasattr(self, timer_name):
|
||||||
if hasattr(self, 'image_timer'):
|
timer_id = getattr(self, timer_name)
|
||||||
self.root.after_cancel(self.image_timer)
|
if timer_id is not None:
|
||||||
|
self.root.after_cancel(timer_id)
|
||||||
|
setattr(self, timer_name, None)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[PLAYER] Error canceling {timer_name} during transition: {e}")
|
||||||
|
|
||||||
self.show_current_media()
|
self.show_current_media()
|
||||||
# Reset transition flag after a brief delay
|
# Reset transition flag after a brief delay
|
||||||
@@ -434,11 +464,291 @@ class SimpleTkPlayer:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def exit_app(self):
|
def exit_app(self):
|
||||||
|
"""Show exit confirmation dialog instead of immediate exit"""
|
||||||
|
print("[EXIT] Exit button pressed, showing confirmation dialog...")
|
||||||
|
self.show_exit_confirmation()
|
||||||
|
|
||||||
|
def show_exit_confirmation(self):
|
||||||
|
"""Show exit confirmation popup with quickconnect code verification"""
|
||||||
|
print("[EXIT] Opening exit confirmation window...")
|
||||||
|
|
||||||
|
# Pause playback if not already paused
|
||||||
|
if not self.paused:
|
||||||
|
self.paused = True
|
||||||
|
self.pause_btn.config(text='▶ Resume')
|
||||||
|
# Explicitly pause VLC video if playing
|
||||||
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
|
try:
|
||||||
|
self.vlc_player.pause()
|
||||||
|
print("[EXIT] VLC player paused")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error pausing VLC: {e}")
|
||||||
|
|
||||||
|
# Hide controls immediately and prevent them from reappearing
|
||||||
|
print("[EXIT] Hiding controls for confirmation dialog...")
|
||||||
|
try:
|
||||||
|
# Cancel any pending control show/hide timers
|
||||||
|
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
|
||||||
|
self.root.after_cancel(self.hide_controls_timer)
|
||||||
|
self.hide_controls_timer = None
|
||||||
|
|
||||||
|
# Hide controls window
|
||||||
|
if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists():
|
||||||
|
self.controls_win.withdraw()
|
||||||
|
print("[EXIT] Controls hidden successfully")
|
||||||
|
|
||||||
|
# Temporarily unbind mouse events to prevent controls from showing
|
||||||
|
self.root.unbind('<Motion>')
|
||||||
|
self.root.unbind('<Button-1>')
|
||||||
|
print("[EXIT] Mouse events unbound")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error hiding controls: {e}")
|
||||||
|
|
||||||
|
# Create exit confirmation window
|
||||||
|
self.create_exit_confirmation_window()
|
||||||
|
|
||||||
|
def create_exit_confirmation_window(self):
|
||||||
|
"""Create the exit confirmation popup window"""
|
||||||
|
try:
|
||||||
|
# Create confirmation window
|
||||||
|
self.exit_window = tk.Toplevel(self.root)
|
||||||
|
self.exit_window.title('Exit Confirmation')
|
||||||
|
self.exit_window.geometry('400x250')
|
||||||
|
self.exit_window.resizable(False, False)
|
||||||
|
self.exit_window.config(bg='#2c3e50')
|
||||||
|
|
||||||
|
# Make window modal and on top
|
||||||
|
self.exit_window.attributes('-topmost', True)
|
||||||
|
self.exit_window.lift()
|
||||||
|
self.exit_window.focus_force()
|
||||||
|
self.exit_window.grab_set()
|
||||||
|
|
||||||
|
# Center the window
|
||||||
|
self.center_exit_window()
|
||||||
|
|
||||||
|
# Create content
|
||||||
|
self.create_exit_window_content()
|
||||||
|
|
||||||
|
# Bind window close event
|
||||||
|
self.exit_window.protocol('WM_DELETE_WINDOW', self.cancel_exit)
|
||||||
|
|
||||||
|
print("[EXIT] Exit confirmation window created")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error creating confirmation window: {e}")
|
||||||
|
# If window creation fails, restore normal functionality
|
||||||
|
self.cancel_exit()
|
||||||
|
|
||||||
|
def center_exit_window(self):
|
||||||
|
"""Center the exit confirmation window on screen"""
|
||||||
|
self.exit_window.update_idletasks()
|
||||||
|
width = self.exit_window.winfo_width()
|
||||||
|
height = self.exit_window.winfo_height()
|
||||||
|
x = (self.exit_window.winfo_screenwidth() // 2) - (width // 2)
|
||||||
|
y = (self.exit_window.winfo_screenheight() // 2) - (height // 2)
|
||||||
|
self.exit_window.geometry(f'{width}x{height}+{x}+{y}')
|
||||||
|
|
||||||
|
def create_exit_window_content(self):
|
||||||
|
"""Create the content for the exit confirmation window"""
|
||||||
|
# Title
|
||||||
|
title_label = tk.Label(
|
||||||
|
self.exit_window,
|
||||||
|
text='⚠️ Exit Application',
|
||||||
|
font=('Arial', 16, 'bold'),
|
||||||
|
fg='#e74c3c',
|
||||||
|
bg='#2c3e50'
|
||||||
|
)
|
||||||
|
title_label.pack(pady=(20, 10))
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
instruction_label = tk.Label(
|
||||||
|
self.exit_window,
|
||||||
|
text='Enter the quickconnect code to exit:',
|
||||||
|
font=('Arial', 12),
|
||||||
|
fg='#ecf0f1',
|
||||||
|
bg='#2c3e50'
|
||||||
|
)
|
||||||
|
instruction_label.pack(pady=(0, 15))
|
||||||
|
|
||||||
|
# Code entry
|
||||||
|
self.code_var = tk.StringVar()
|
||||||
|
self.code_entry = tk.Entry(
|
||||||
|
self.exit_window,
|
||||||
|
textvariable=self.code_var,
|
||||||
|
font=('Arial', 14),
|
||||||
|
width=15,
|
||||||
|
justify='center',
|
||||||
|
show='*' # Hide the input like a password
|
||||||
|
)
|
||||||
|
self.code_entry.pack(pady=(0, 20))
|
||||||
|
self.code_entry.focus_set()
|
||||||
|
|
||||||
|
# Bind Enter key to confirm
|
||||||
|
self.code_entry.bind('<Return>', lambda e: self.verify_exit_code())
|
||||||
|
|
||||||
|
# Buttons frame
|
||||||
|
buttons_frame = tk.Frame(self.exit_window, bg='#2c3e50')
|
||||||
|
buttons_frame.pack(pady=(0, 20))
|
||||||
|
|
||||||
|
# Cancel button
|
||||||
|
cancel_btn = tk.Button(
|
||||||
|
buttons_frame,
|
||||||
|
text='Cancel',
|
||||||
|
font=('Arial', 12, 'bold'),
|
||||||
|
bg='#95a5a6',
|
||||||
|
fg='white',
|
||||||
|
padx=20,
|
||||||
|
pady=8,
|
||||||
|
command=self.cancel_exit
|
||||||
|
)
|
||||||
|
cancel_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||||
|
|
||||||
|
# Confirm button
|
||||||
|
confirm_btn = tk.Button(
|
||||||
|
buttons_frame,
|
||||||
|
text='Exit App',
|
||||||
|
font=('Arial', 12, 'bold'),
|
||||||
|
bg='#e74c3c',
|
||||||
|
fg='white',
|
||||||
|
padx=20,
|
||||||
|
pady=8,
|
||||||
|
command=self.verify_exit_code
|
||||||
|
)
|
||||||
|
confirm_btn.pack(side=tk.LEFT, padx=(10, 0))
|
||||||
|
|
||||||
|
def verify_exit_code(self):
|
||||||
|
"""Verify the entered code and proceed with exit if correct"""
|
||||||
|
try:
|
||||||
|
entered_code = self.code_var.get().strip()
|
||||||
|
|
||||||
|
# Load the quickconnect code from config
|
||||||
|
with open(CONFIG_PATH, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
quickconnect_code = config.get('quickconnect_key', '')
|
||||||
|
|
||||||
|
print(f"[EXIT] Verifying exit code...")
|
||||||
|
|
||||||
|
if entered_code == quickconnect_code:
|
||||||
|
print("[EXIT] Correct code entered, proceeding with exit...")
|
||||||
|
# Close confirmation window
|
||||||
|
self.exit_window.destroy()
|
||||||
|
# Proceed with actual exit
|
||||||
|
self.perform_actual_exit()
|
||||||
|
else:
|
||||||
|
print("[EXIT] Incorrect code entered")
|
||||||
|
# Show error and clear entry
|
||||||
|
self.show_exit_error()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error verifying code: {e}")
|
||||||
|
self.show_exit_error()
|
||||||
|
|
||||||
|
def show_exit_error(self):
|
||||||
|
"""Show error message for incorrect code"""
|
||||||
|
# Clear the entry
|
||||||
|
self.code_var.set('')
|
||||||
|
|
||||||
|
# Temporarily change entry background to indicate error
|
||||||
|
original_bg = self.code_entry.cget('bg')
|
||||||
|
self.code_entry.config(bg='#e74c3c')
|
||||||
|
|
||||||
|
# Reset background after 1 second
|
||||||
|
self.exit_window.after(1000, lambda: self.code_entry.config(bg=original_bg))
|
||||||
|
|
||||||
|
# Focus back to entry
|
||||||
|
self.code_entry.focus_set()
|
||||||
|
|
||||||
|
def cancel_exit(self):
|
||||||
|
"""Cancel exit and restore normal functionality"""
|
||||||
|
print("[EXIT] Exit cancelled, restoring normal functionality...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Close confirmation window if it exists
|
||||||
|
if hasattr(self, 'exit_window') and self.exit_window:
|
||||||
|
self.exit_window.destroy()
|
||||||
|
del self.exit_window
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error closing confirmation window: {e}")
|
||||||
|
|
||||||
|
# Restore normal functionality (similar to settings restoration)
|
||||||
|
self.restore_after_exit_cancel()
|
||||||
|
|
||||||
|
def restore_after_exit_cancel(self):
|
||||||
|
"""Restore normal player functionality after exit cancellation"""
|
||||||
|
if self.is_exiting:
|
||||||
|
return # Don't restore if we're actually exiting
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("[EXIT] Restoring player functionality after exit cancellation...")
|
||||||
|
|
||||||
|
# Resume playback
|
||||||
|
if self.paused:
|
||||||
|
self.resume_play()
|
||||||
|
print("[EXIT] Playback resumed")
|
||||||
|
|
||||||
|
# Re-bind mouse and button events
|
||||||
|
self.root.bind('<Motion>', self.on_activity)
|
||||||
|
self.root.bind('<Button-1>', self.on_activity)
|
||||||
|
print("[EXIT] Mouse events re-bound")
|
||||||
|
|
||||||
|
# Restore controls if needed
|
||||||
|
if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists():
|
||||||
|
self.create_controls()
|
||||||
|
print("[EXIT] Controls recreated")
|
||||||
|
|
||||||
|
# Force focus back to main window
|
||||||
|
self.root.focus_force()
|
||||||
|
self.root.lift()
|
||||||
|
print("[EXIT] Focus restored to main window")
|
||||||
|
|
||||||
|
# Resume VLC if it was paused
|
||||||
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
|
try:
|
||||||
|
if self.vlc_player.get_state() == vlc.State.Paused:
|
||||||
|
self.vlc_player.play()
|
||||||
|
print("[EXIT] VLC playback resumed")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error resuming VLC: {e}")
|
||||||
|
|
||||||
|
print("[EXIT] Player functionality fully restored after exit cancellation")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error restoring functionality: {e}")
|
||||||
|
|
||||||
|
def perform_actual_exit(self):
|
||||||
|
"""Perform the actual application exit after code verification"""
|
||||||
|
print("[EXIT] Starting application exit...")
|
||||||
|
self.is_exiting = True # Set exit flag to prevent further operations
|
||||||
|
|
||||||
# Signal all threads and flags to stop
|
# Signal all threads and flags to stop
|
||||||
if hasattr(self, 'stop_event') and self.stop_event:
|
if hasattr(self, 'stop_event') and self.stop_event:
|
||||||
self.stop_event.set()
|
self.stop_event.set()
|
||||||
if hasattr(self, 'app_running') and self.app_running:
|
if hasattr(self, 'app_running') and self.app_running:
|
||||||
self.app_running[0] = False
|
self.app_running[0] = False
|
||||||
|
|
||||||
|
# Cancel all pending timers first
|
||||||
|
timer_names = ['hide_controls_timer', 'video_watchdog', 'image_watchdog', 'image_timer', 'pause_timer']
|
||||||
|
for timer_name in timer_names:
|
||||||
|
try:
|
||||||
|
if hasattr(self, timer_name):
|
||||||
|
timer_id = getattr(self, timer_name)
|
||||||
|
if timer_id is not None:
|
||||||
|
self.root.after_cancel(timer_id)
|
||||||
|
print(f"[EXIT] Canceled {timer_name}")
|
||||||
|
setattr(self, timer_name, None)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error canceling {timer_name}: {e}")
|
||||||
|
# Continue with other timers even if one fails
|
||||||
|
|
||||||
|
# Stop VLC player
|
||||||
|
try:
|
||||||
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
|
self.vlc_player.stop()
|
||||||
|
self.vlc_player = None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error stopping VLC: {e}")
|
||||||
|
|
||||||
# Unbind all events to prevent callbacks after destroy
|
# Unbind all events to prevent callbacks after destroy
|
||||||
try:
|
try:
|
||||||
self.root.unbind('<Motion>')
|
self.root.unbind('<Motion>')
|
||||||
@@ -446,80 +756,234 @@ class SimpleTkPlayer:
|
|||||||
self.root.unbind('<Configure>')
|
self.root.unbind('<Configure>')
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# Attempt to destroy all Toplevel windows before root
|
|
||||||
|
# Force hide and destroy controls immediately
|
||||||
try:
|
try:
|
||||||
# Withdraw controls_win if it exists
|
print("[EXIT] Destroying controls...")
|
||||||
|
|
||||||
|
# First, try to hide and destroy the main controls window
|
||||||
if hasattr(self, 'controls_win') and self.controls_win:
|
if hasattr(self, 'controls_win') and self.controls_win:
|
||||||
if self.controls_win.winfo_exists():
|
try:
|
||||||
self.controls_win.withdraw()
|
if self.controls_win.winfo_exists():
|
||||||
# Destroy controls_win if it exists (this will also destroy controls_frame)
|
self.controls_win.withdraw()
|
||||||
if hasattr(self, 'controls_win') and self.controls_win:
|
self.controls_win.destroy()
|
||||||
if self.controls_win.winfo_exists():
|
self.controls_win = None
|
||||||
self.controls_win.destroy()
|
print("[EXIT] Main controls window destroyed")
|
||||||
self.controls_win = None
|
except Exception as e:
|
||||||
# Fallback: destroy controls_frame if it somehow still exists
|
print(f"[EXIT] Error destroying main controls_win: {e}")
|
||||||
|
|
||||||
|
# Destroy controls_frame separately if it exists
|
||||||
if hasattr(self, 'controls_frame') and self.controls_frame:
|
if hasattr(self, 'controls_frame') and self.controls_frame:
|
||||||
if self.controls_frame.winfo_exists():
|
try:
|
||||||
self.controls_frame.destroy()
|
if self.controls_frame.winfo_exists():
|
||||||
self.controls_frame = None
|
self.controls_frame.destroy()
|
||||||
# Fallback: destroy any remaining Toplevels in the app
|
self.controls_frame = None
|
||||||
for widget in self.root.winfo_children():
|
print("[EXIT] Controls frame destroyed")
|
||||||
if isinstance(widget, tk.Toplevel):
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error destroying controls_frame: {e}")
|
||||||
|
|
||||||
|
# Destroy all button references
|
||||||
|
button_names = ['prev_btn', 'pause_btn', 'next_btn', 'settings_btn', 'exit_btn']
|
||||||
|
for btn_name in button_names:
|
||||||
|
try:
|
||||||
|
if hasattr(self, btn_name):
|
||||||
|
btn = getattr(self, btn_name)
|
||||||
|
if btn and btn.winfo_exists():
|
||||||
|
btn.destroy()
|
||||||
|
setattr(self, btn_name, None)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error destroying {btn_name}: {e}")
|
||||||
|
|
||||||
|
# Destroy any remaining Toplevel windows (more aggressive search)
|
||||||
|
try:
|
||||||
|
# Get all toplevel windows including potential orphans
|
||||||
|
all_toplevels = []
|
||||||
|
for widget in list(self.root.winfo_children()):
|
||||||
|
if isinstance(widget, tk.Toplevel):
|
||||||
|
all_toplevels.append(widget)
|
||||||
|
|
||||||
|
# Also check if there are any floating Toplevel references
|
||||||
|
if hasattr(tk, '_default_root') and tk._default_root:
|
||||||
|
for child in tk._default_root.winfo_children():
|
||||||
|
if isinstance(child, tk.Toplevel) and child not in all_toplevels:
|
||||||
|
all_toplevels.append(child)
|
||||||
|
|
||||||
|
print(f"[EXIT] Found {len(all_toplevels)} toplevel windows to destroy")
|
||||||
|
|
||||||
|
for widget in all_toplevels:
|
||||||
try:
|
try:
|
||||||
widget.destroy()
|
if widget.winfo_exists():
|
||||||
except Exception:
|
widget.withdraw()
|
||||||
pass
|
widget.quit() if hasattr(widget, 'quit') else None
|
||||||
|
widget.destroy()
|
||||||
|
print(f"[EXIT] Destroyed toplevel widget: {type(widget).__name__}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error destroying toplevel {widget}: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error in comprehensive toplevel cleanup: {e}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[EXIT] Error destroying controls_win/frame/toplevels: {e}")
|
print(f"[EXIT] Error in controls cleanup: {e}")
|
||||||
# Destroy any other Toplevels if needed (add here if you have more)
|
|
||||||
|
# Final step: destroy root window
|
||||||
try:
|
try:
|
||||||
if self.root.winfo_exists():
|
print("[EXIT] Destroying main window...")
|
||||||
self.root.destroy()
|
|
||||||
|
# Force update to process any pending widget operations
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
# One final check for any remaining Toplevel windows
|
||||||
|
try:
|
||||||
|
remaining_toplevels = [w for w in self.root.winfo_children() if isinstance(w, tk.Toplevel)]
|
||||||
|
if remaining_toplevels:
|
||||||
|
print(f"[EXIT] Found {len(remaining_toplevels)} remaining toplevel windows, destroying...")
|
||||||
|
for widget in remaining_toplevels:
|
||||||
|
try:
|
||||||
|
widget.withdraw()
|
||||||
|
widget.destroy()
|
||||||
|
print(f"[EXIT] Destroyed remaining toplevel: {widget}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error destroying remaining toplevel: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EXIT] Error checking for remaining toplevels: {e}")
|
||||||
|
|
||||||
|
# Force another update
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
# Exit the main loop and destroy
|
||||||
|
self.root.quit() # Exit mainloop first
|
||||||
|
self.root.update() # Process the quit
|
||||||
|
self.root.destroy()
|
||||||
|
print("[EXIT] Application exit complete")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[EXIT] Error destroying root: {e}")
|
print(f"[EXIT] Error destroying root: {e}")
|
||||||
|
# Force exit using system methods
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
os._exit(0)
|
||||||
|
except Exception:
|
||||||
|
import sys
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
def open_settings(self):
|
def open_settings(self):
|
||||||
if self.paused is not True:
|
print("[SETTINGS] Opening settings window...")
|
||||||
|
|
||||||
|
# Pause playback if not already paused
|
||||||
|
if not self.paused:
|
||||||
self.paused = True
|
self.paused = True
|
||||||
self.pause_btn.config(text='▶ Resume')
|
self.pause_btn.config(text='▶ Resume')
|
||||||
# Explicitly pause VLC video if playing
|
# Explicitly pause VLC video if playing
|
||||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
try:
|
try:
|
||||||
self.vlc_player.pause()
|
self.vlc_player.pause()
|
||||||
except Exception:
|
print("[SETTINGS] VLC player paused")
|
||||||
pass
|
except Exception as e:
|
||||||
# Destroy controls overlay so settings window is always interactive
|
print(f"[SETTINGS] Error pausing VLC: {e}")
|
||||||
if hasattr(self, 'controls_win') and self.controls_win:
|
|
||||||
self.controls_win.destroy()
|
# Hide controls immediately and prevent them from reappearing
|
||||||
self.controls_win = None
|
print("[SETTINGS] Hiding controls...")
|
||||||
|
try:
|
||||||
|
# Cancel any pending control show/hide timers
|
||||||
|
if hasattr(self, 'hide_controls_timer'):
|
||||||
|
self.root.after_cancel(self.hide_controls_timer)
|
||||||
|
|
||||||
|
# Hide controls window
|
||||||
|
if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists():
|
||||||
|
self.controls_win.withdraw()
|
||||||
|
print("[SETTINGS] Controls hidden successfully")
|
||||||
|
|
||||||
|
# Temporarily unbind mouse events to prevent controls from showing
|
||||||
|
self.root.unbind('<Motion>')
|
||||||
|
self.root.unbind('<Button-1>')
|
||||||
|
print("[SETTINGS] Mouse events unbound")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SETTINGS] Error hiding controls: {e}")
|
||||||
|
|
||||||
|
# Launch settings window
|
||||||
settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py')
|
settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py')
|
||||||
# Open settings in a new process so it doesn't block the main player
|
try:
|
||||||
proc = subprocess.Popen([sys.executable, settings_path], close_fds=True)
|
# Open settings in a new process
|
||||||
# Give the window manager a moment to focus the new window
|
proc = subprocess.Popen([sys.executable, settings_path], close_fds=True)
|
||||||
self.root.after(300, lambda: self.root.focus_force())
|
print(f"[SETTINGS] Settings process started with PID: {proc.pid}")
|
||||||
# Wait for the settings window to close, then resume
|
|
||||||
self.root.after(1000, lambda: self.check_settings_closed(proc))
|
# Start monitoring the settings window
|
||||||
|
self.root.after(500, lambda: self.check_settings_closed(proc))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SETTINGS] Error opening settings: {e}")
|
||||||
|
# Restore functionality if settings failed to open
|
||||||
|
self.restore_after_settings()
|
||||||
|
|
||||||
def check_settings_closed(self, proc):
|
def check_settings_closed(self, proc):
|
||||||
|
if self.is_exiting:
|
||||||
|
print("[SETTINGS] Exit in progress, stopping settings monitoring")
|
||||||
|
return
|
||||||
|
|
||||||
if proc.poll() is not None:
|
if proc.poll() is not None:
|
||||||
# Resume playback and unpause VLC if needed
|
print("[SETTINGS] Settings window closed, restoring player functionality...")
|
||||||
self.resume_play()
|
self.restore_after_settings()
|
||||||
# Restore and recreate controls overlay
|
else:
|
||||||
self.root.deiconify()
|
# Continue checking every second
|
||||||
self.create_controls()
|
self.root.after(1000, lambda: self.check_settings_closed(proc))
|
||||||
# Re-bind mouse and button events to new controls
|
|
||||||
|
def restore_after_settings(self):
|
||||||
|
"""Restore normal player functionality after settings window closes"""
|
||||||
|
if self.is_exiting:
|
||||||
|
print("[SETTINGS] Exit in progress, skipping settings restoration")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("[SETTINGS] Restoring player functionality...")
|
||||||
|
|
||||||
|
# Resume playback
|
||||||
|
if self.paused:
|
||||||
|
self.resume_play()
|
||||||
|
print("[SETTINGS] Playback resumed")
|
||||||
|
|
||||||
|
# Re-bind mouse and button events
|
||||||
self.root.bind('<Motion>', self.on_activity)
|
self.root.bind('<Motion>', self.on_activity)
|
||||||
self.root.bind('<Button-1>', self.on_activity)
|
self.root.bind('<Button-1>', self.on_activity)
|
||||||
self.show_controls()
|
print("[SETTINGS] Mouse events re-bound")
|
||||||
|
|
||||||
|
# Ensure controls are properly destroyed before recreating
|
||||||
|
if hasattr(self, 'controls_win') and self.controls_win:
|
||||||
|
try:
|
||||||
|
if self.controls_win.winfo_exists():
|
||||||
|
self.controls_win.destroy()
|
||||||
|
self.controls_win = None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SETTINGS] Error destroying old controls: {e}")
|
||||||
|
|
||||||
|
# Recreate controls
|
||||||
|
self.create_controls()
|
||||||
|
print("[SETTINGS] Controls recreated")
|
||||||
|
|
||||||
|
# Ensure exit button is properly configured after recreation
|
||||||
|
if hasattr(self, 'exit_btn') and self.exit_btn:
|
||||||
|
self.exit_btn.config(command=self.exit_app)
|
||||||
|
print("[SETTINGS] Exit button command restored")
|
||||||
|
|
||||||
|
# Force focus back to main window
|
||||||
|
self.root.focus_force()
|
||||||
|
self.root.lift()
|
||||||
|
print("[SETTINGS] Focus restored to main window")
|
||||||
|
|
||||||
|
# Resume VLC if it was paused
|
||||||
if hasattr(self, 'vlc_player') and self.vlc_player:
|
if hasattr(self, 'vlc_player') and self.vlc_player:
|
||||||
try:
|
try:
|
||||||
# Only resume if it was paused by us
|
|
||||||
if self.vlc_player.get_state() == vlc.State.Paused:
|
if self.vlc_player.get_state() == vlc.State.Paused:
|
||||||
self.vlc_player.play()
|
self.vlc_player.play()
|
||||||
except Exception:
|
print("[SETTINGS] VLC playback resumed")
|
||||||
pass
|
except Exception as e:
|
||||||
else:
|
print(f"[SETTINGS] Error resuming VLC: {e}")
|
||||||
self.root.after(1000, lambda: self.check_settings_closed(proc))
|
|
||||||
|
print("[SETTINGS] Player functionality fully restored")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[SETTINGS] Error restoring functionality: {e}")
|
||||||
|
|
||||||
def main_start(self):
|
def main_start(self):
|
||||||
self.play_intro_video()
|
self.play_intro_video()
|
||||||
|
|||||||
Reference in New Issue
Block a user