diff --git a/wow_sync/tray/tray_macos.py b/wow_sync/tray/tray_macos.py index 5462379..ad84c5d 100644 --- a/wow_sync/tray/tray_macos.py +++ b/wow_sync/tray/tray_macos.py @@ -2,8 +2,7 @@ macOS native implementation using PyObjC. Uses PyObjC to directly access macOS NSStatusBar APIs for native -status bar icon integration. This integrates with the main thread -using performSelectorOnMainThread to avoid threading issues. +status bar icon integration. """ from typing import Optional, Callable @@ -200,32 +199,24 @@ async def setup(self): str(icon_path), callbacks ) - # Setup status bar on main thread - # performSelectorOnMainThread doesn't return the selector's result, - # so we call it and then check if status_item was created - self.controller.performSelectorOnMainThread_withObject_waitUntilDone_( - objc.selector(self.controller.setupStatusBar, signature=b'B@:'), - None, - True # Wait until done - ) + # Setup status bar directly + # This is called from async_mainloop which runs on the main thread, + # satisfying AppKit's requirement for UI operations on the main thread. + # Callbacks are already wrapped with root.after() for thread safety. + success = self.controller.setupStatusBar() - # Check if setup was successful by verifying status_item exists - if not self.controller.status_item: + if not success: raise RuntimeError("Failed to setup status bar") - # Longer delay to ensure the menu is fully initialized - await asyncio.sleep(0.3) + # Small delay to ensure the menu is fully initialized + await asyncio.sleep(0.1) def update_monitor_menu(self, is_enabled: bool): """Update the auto-sync menu item label.""" if self.controller and self.controller.monitor_menu_item: label = "Disable Auto-Sync" if is_enabled else "Enable Auto-Sync" - # Update on main thread - self.controller.performSelectorOnMainThread_withObject_waitUntilDone_( - objc.selector(self.controller.updateMonitorMenuTitle_, signature=b'v@:@'), - label, - False # Don't wait - async update is fine - ) + # Update directly - called from MainWindow which is on the main thread + self.controller.updateMonitorMenuTitle_(label) def cleanup(self): """Cleanup tray icon.""" diff --git a/wow_sync/ui/main_window.py b/wow_sync/ui/main_window.py index f15ea56..3e4b576 100644 --- a/wow_sync/ui/main_window.py +++ b/wow_sync/ui/main_window.py @@ -159,12 +159,15 @@ def _create_widgets(self): async def _setup_tray(self): """Setup system tray icon with automatic fallback between implementations.""" try: + # Wrap callbacks with root.after() to ensure they run on the main Tkinter thread + # This is necessary because tray implementations (especially Windows) may invoke + # callbacks from different threads, and Tkinter is not thread-safe self.tray_icon = TrayIcon( - on_show=self._show_window, - on_quit=self.quit_app, - on_pull=self._pull_from_remote, - on_push=self._push_to_remote, - on_toggle_monitor=self._tray_toggle_monitor + on_show=lambda: self.root.after(0, self._show_window), + on_quit=lambda: self.root.after(0, self.quit_app), + on_pull=lambda: self.root.after(0, self._pull_from_remote), + on_push=lambda: self.root.after(0, self._push_to_remote), + on_toggle_monitor=lambda: self.root.after(0, self._tray_toggle_monitor) ) success, message = await self.tray_icon.setup()