Source code for mesofield.devices.subprocesses.psychopy

"""Windows helpers for launching PsychoPy as a subprocess.

The PsychoPy stimulus *lifecycle* now lives on the shared
:class:`~mesofield.devices.stimulus_base.SubprocessStimulusDevice` engine (see
:mod:`mesofield.devices.psychopy_device`); this module retains only the two
Windows-specific helpers that the device and the GUI start gate still need:

- :func:`get_psychopy_python_exe` -- locate the standalone PsychoPy interpreter
  via the registry, so the stimulus script runs in PsychoPy's own environment.
- :func:`force_foreground` -- raise a Qt widget over PsychoPy's full-screen
  window so an operator keypress lands on our dialog, not the stimulus.
"""

import os
import winreg


def get_psychopy_python_exe():
    try:
        key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\PsychoPy", 0, winreg.KEY_READ)
        install_path, _ = winreg.QueryValueEx(key, "InstallPath")
        winreg.CloseKey(key)
        python_exe = os.path.join(install_path, "python.exe")
        if os.path.exists(python_exe):
            return python_exe
    except OSError:
        pass
    return r"C:\Program Files\PsychoPy\python.exe"


[docs] def force_foreground(widget) -> None: """Force *widget* to the foreground with keyboard focus. PsychoPy runs its stimulus in a separate process whose full-screen window owns the foreground, and Windows blocks a background process from simply calling ``SetForegroundWindow``. The reliable workaround is to momentarily attach our input thread to the current foreground window's thread, raise our window, then detach. Without this the operator has to click the dialog by hand before a keypress reaches it. Best-effort and Windows-specific; any failure is swallowed (the dialog still works, it just may not auto-focus). """ from PyQt6.QtCore import Qt widget.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True) widget.show() widget.raise_() widget.activateWindow() # Best-effort Win32 foreground nudge. We intentionally avoid # AttachThreadInput here: coupling our GUI thread's input queue to # PsychoPy's full-screen-window thread can wedge input handling, and # WindowStaysOnTopHint + activateWindow is enough in practice. try: import ctypes user32 = ctypes.windll.user32 hwnd = int(widget.winId()) user32.ShowWindow(hwnd, 5) # SW_SHOW user32.BringWindowToTop(hwnd) user32.SetForegroundWindow(hwnd) except Exception: pass