Skip to content
31 changes: 11 additions & 20 deletions wow_sync/tray/tray_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
13 changes: 8 additions & 5 deletions wow_sync/ui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down