fix: Replace all invokeMethod calls with Qt signals for thread-safety

BUG: QMetaObject.invokeMethod with Q_ARG doesn't work properly in PyQt6
and was causing TypeError exceptions.

FIX:
- Added proper Qt signals at class level:
  * update_status_signal(str, bool, bool)
  * update_session_table_signal(object)
  * update_counters_signal()
  * enable_scan_button_signal(bool)

- Connected all signals to slot methods in initialize()
- Replaced all invokeMethod calls with signal.emit()
- Thread callbacks now emit signals instead of calling invokeMethod
- UI updates happen in main Qt thread via signal/slot mechanism

This is the correct PyQt6 way to do cross-thread communication.
All UI updates are now thread-safe and won't cause TypeErrors.
This commit is contained in:
LemonNexus 2026-02-15 00:51:15 +00:00
parent d0ccb791f7
commit 05f8c06312
1 changed files with 32 additions and 55 deletions

View File

@ -150,8 +150,12 @@ class SkillScannerPlugin(BasePlugin):
description = "Uses core OCR and Log services" description = "Uses core OCR and Log services"
hotkey = "ctrl+shift+s" hotkey = "ctrl+shift+s"
# Signal for thread-safe hotkey scanning # Signals for thread-safe UI updates
hotkey_triggered = pyqtSignal() hotkey_triggered = pyqtSignal()
update_status_signal = pyqtSignal(str, bool, bool) # message, success, error
update_session_table_signal = pyqtSignal(object) # skills dict
update_counters_signal = pyqtSignal()
enable_scan_button_signal = pyqtSignal(bool)
def initialize(self): def initialize(self):
"""Setup skill scanner.""" """Setup skill scanner."""
@ -167,8 +171,12 @@ class SkillScannerPlugin(BasePlugin):
self.current_scan_session = {} # Skills collected in current multi-page scan self.current_scan_session = {} # Skills collected in current multi-page scan
self.pages_scanned = 0 self.pages_scanned = 0
# Connect hotkey signal # Connect signals
self.hotkey_triggered.connect(self._scan_page_for_multi) self.hotkey_triggered.connect(self._scan_page_for_multi)
self.update_status_signal.connect(self._update_multi_page_status_slot)
self.update_session_table_signal.connect(self._update_session_table)
self.update_counters_signal.connect(self._update_counters_slot)
self.enable_scan_button_signal.connect(self.scan_page_btn.setEnabled)
# Subscribe to skill gain events from core Log service # Subscribe to skill gain events from core Log service
try: try:
@ -514,7 +522,7 @@ class SkillScannerPlugin(BasePlugin):
ocr_service = get_ocr_service() ocr_service = get_ocr_service()
if not ocr_service.is_available(): if not ocr_service.is_available():
self._update_multi_page_status("Error: OCR not available", error=True) self.update_status_signal.emit("Error: OCR not available", False, True)
return return
# Capture and OCR # Capture and OCR
@ -530,33 +538,22 @@ class SkillScannerPlugin(BasePlugin):
self.pages_scanned += 1 self.pages_scanned += 1
# Update UI # Update UI via signals (thread-safe)
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG self.update_session_table_signal.emit(self.current_scan_session)
QMetaObject.invokeMethod(
self, "_update_session_table",
Qt.ConnectionType.QueuedConnection,
Q_ARG(object, self.current_scan_session)
)
# Show success with checkmark and beep # Show success with checkmark and beep
self._update_multi_page_status( self.update_status_signal.emit(
f"✅ Page {self.pages_scanned} scanned! {len(skills)} skills found. Click Next Page in game →", f"✅ Page {self.pages_scanned} scanned! {len(skills)} skills found. Click Next Page in game →",
success=True True, False
) )
# Play beep sound # Play beep sound
self._play_beep() self._play_beep()
except Exception as e: except Exception as e:
self._update_multi_page_status(f"Error: {str(e)}", error=True) self.update_status_signal.emit(f"Error: {str(e)}", False, True)
finally: finally:
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG self.enable_scan_button_signal.emit(True)
QMetaObject.invokeMethod(
self.scan_page_btn,
"setEnabled",
Qt.ConnectionType.QueuedConnection,
Q_ARG(bool, True)
)
thread = Thread(target=do_scan) thread = Thread(target=do_scan)
thread.daemon = True thread.daemon = True
@ -609,38 +606,18 @@ class SkillScannerPlugin(BasePlugin):
return skills return skills
def _update_multi_page_status(self, message, success=False, error=False): def _update_multi_page_status_slot(self, message, success=False, error=False):
"""Update multi-page status label.""" """Slot for updating multi-page status (called via signal)."""
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
color = "#4ecdc4" if success else "#ff4757" if error else "#ff8c42" color = "#4ecdc4" if success else "#ff4757" if error else "#ff8c42"
QMetaObject.invokeMethod( self.multi_page_status.setText(message)
self.multi_page_status, self.multi_page_status.setStyleSheet(f"color: {color}; font-size: 14px;")
"setText", self._update_counters_slot()
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, message) def _update_counters_slot(self):
) """Slot for updating counters (called via signal)."""
QMetaObject.invokeMethod( self.pages_scanned_label.setText(f"Pages: {self.pages_scanned}")
self.multi_page_status, self.total_skills_label.setText(f"Skills: {len(self.current_scan_session)}")
"setStyleSheet",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"color: {color}; font-size: 14px;")
)
# Update counters
QMetaObject.invokeMethod(
self.pages_scanned_label,
"setText",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"Pages: {self.pages_scanned}")
)
QMetaObject.invokeMethod(
self.total_skills_label,
"setText",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"Skills: {len(self.current_scan_session)}")
)
def _play_beep(self): def _play_beep(self):
"""Play a beep sound to notify user.""" """Play a beep sound to notify user."""
@ -715,7 +692,7 @@ class SkillScannerPlugin(BasePlugin):
self._start_auto_scan_with_hotkey() self._start_auto_scan_with_hotkey()
elif mode == 1: # Hotkey only elif mode == 1: # Hotkey only
self._register_hotkey() self._register_hotkey()
self._update_multi_page_status("Hotkey F12 ready! Navigate to first page and press F12", success=True) self.update_status_signal.emit("Hotkey F12 ready! Navigate to first page and press F12", True, False)
else: # Manual click else: # Manual click
self._scan_page_for_multi() self._scan_page_for_multi()
@ -729,7 +706,7 @@ class SkillScannerPlugin(BasePlugin):
self._register_hotkey() self._register_hotkey()
# Start monitoring # Start monitoring
self._update_multi_page_status("🤖 Auto-detect started! Navigate to page 1...", success=True) self.update_status_signal.emit("🤖 Auto-detect started! Navigate to page 1...", True, False)
# Start auto-detection timer # Start auto-detection timer
self.auto_scan_timer = QTimer() self.auto_scan_timer = QTimer()
@ -797,7 +774,7 @@ class SkillScannerPlugin(BasePlugin):
# If page changed, trigger scan # If page changed, trigger scan
if self.last_page_number is not None and current_page != self.last_page_number: if self.last_page_number is not None and current_page != self.last_page_number:
self._update_multi_page_status(f"📄 Page change detected: {current_page}/{total_pages}", success=True) self.update_status_signal.emit(f"📄 Page change detected: {current_page}/{total_pages}", True, False)
self._scan_page_for_multi() self._scan_page_for_multi()
self.last_page_number = current_page self.last_page_number = current_page
@ -820,9 +797,9 @@ class SkillScannerPlugin(BasePlugin):
self.auto_scan_active = False self.auto_scan_active = False
# Keep hotkey registered # Keep hotkey registered
self._update_multi_page_status( self.update_status_signal.emit(
"⚠️ Auto-detect unreliable. Use F12 hotkey to scan each page manually!", "⚠️ Auto-detect unreliable. Use F12 hotkey to scan each page manually!",
error=True False, True
) )
# Play alert sound # Play alert sound