Source code for pyelink.display.psychopy_display

"""PsychoPy display backend implementation."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import pyglet
from psychopy import event, visual

from .base import BaseDisplay

if TYPE_CHECKING:
    from pathlib import Path


[docs] class PsychopyDisplay(BaseDisplay): """PsychoPy implementation of display window management. Manages PsychoPy Window creation, event handling, and drawing operations. Provides both direct access to visual.Window and backend-agnostic helpers. """ @property def backend_name(self) -> str: """Get backend identifier.""" return "psychopy" def _create_window(self, settings: object) -> visual.Window: # noqa: PLR6301 """Create PsychoPy window. Args: settings: Settings object with SCREEN_RES, FULLSCREEN, DISPLAY_INDEX Returns: psychopy.visual.Window object """ display = pyglet.canvas.get_display() screens = display.get_screens() screen_index = 0 if len(screens) <= settings.display_index else settings.display_index window = visual.Window( size=settings.screen_res, fullscr=settings.fullscreen, screen=screen_index, units="pix", color=[0, 0, 0], allowGUI=False, ) return window
[docs] def flip(self) -> None: """Update display.""" self._window.flip()
[docs] def close(self) -> None: """Close PsychoPy window.""" self._window.close()
[docs] def get_events(self) -> list[dict[str, Any]]: """Get PsychoPy events as unified dicts. Returns: List of event dicts with unified keys """ events = [] keys = event.getKeys(timeStamped=False, modifiers=True) for key_info in keys: # key_info is tuple (key_name, modifiers_dict) when modifiers=True key_name = key_info[0] if isinstance(key_info, tuple) else key_info modifiers = key_info[1] if isinstance(key_info, tuple) and len(key_info) > 1 else {} # Check for Ctrl+C (when window has focus, SIGINT won't fire) if key_name == "c" and modifiers.get("ctrl", False): if self.shutdown_handler: self.shutdown_handler(None, None) # Call signal handler directly return events # Return immediately after shutdown initiated event_dict = { "type": "keydown", "key": key_name, "unicode": key_name if len(key_name) == 1 else "", } events.append(event_dict) return events
[docs] def fill(self, color: tuple[int, int, int]) -> None: """Fill window with color. PsychoPy uses normalized RGB (-1 to 1), so we convert from 0-255. Args: color: RGB tuple (0-255, 0-255, 0-255) """ normalized_color = [(c / 255.0) * 2 - 1 for c in color] self._window.color = normalized_color self._window.flip()
[docs] def clear(self) -> None: """Clear window to black.""" self._window.color = [-1, -1, -1] self._window.flip()
[docs] def get_size(self) -> tuple[int, int]: """Get window dimensions. Returns: (width, height) in pixels """ return tuple(self._window.size)
[docs] def draw_text( self, text: str, pos: tuple[int, int] | None = None, center: bool = False, size: int = 24, color: tuple[int, int, int] = (255, 255, 255), ) -> None: """Draw text on window. Args: text: Text string to display pos: (x, y) position in pixels center: If True, center text on screen size: Font size in points color: RGB color tuple (0-255, 0-255, 0-255) """ normalized_color = [(c / 255.0) * 2 - 1 for c in color] text_pos = (0, 0) if center else pos text_stim = visual.TextStim( self._window, text=text, pos=text_pos, height=size, color=normalized_color, units="pix", ) text_stim.draw()
[docs] def draw_image( self, image_path: str | Path, pos: tuple[int, int] | None = None, center: bool = False, scale: float | None = None, ) -> None: """Draw image on window. Args: image_path: Path to image file pos: (x, y) position in pixels center: If True, center image on screen scale: Scale factor for image size """ image_pos = (0, 0) if center else pos image_stim = visual.ImageStim(self._window, image=str(image_path), pos=image_pos, units="pix") if scale is not None: original_size = image_stim.size image_stim.size = (original_size[0] * scale, original_size[1] * scale) image_stim.draw()