Source code for psychos.visual.window

"""psychos.visual.window: Extension of the Pyglet window class with additional functionality."""

from typing import Iterable, Optional, TYPE_CHECKING, Union, Tuple

import pyglet
from pyglet.window import Window as PygletWindow

from .units import Unit, parse_height, parse_width
from ..core.keys import wait_key
from ..core.time import wait
from ..utils import Color

if TYPE_CHECKING:
    from ..types import ColorType, UnitType, Literal, KeyEvent
    from ..core.time import Clock

__all__ = ["Window", "get_window"]


[docs] def get_window() -> "Window": """ Retrieve the current default window. Returns ------- Window The current default window. Raises ------ RuntimeError If no window has been created yet. """ windows = list(pyglet.app.windows) if not windows: raise RuntimeError("No window has been created yet.") return windows[0]
[docs] class Window(PygletWindow): # pylint: disable=abstract-method """ Custom window for displaying visual elements. A subclass of `pyglet.window.Window`, this class provides additional functionality for handling units, background color, and event management, while maintaining full compatibility with the `pyglet` library. For more information on `pyglet`, refer to the official documentation. Parameters ---------- width : Optional[int], default=None The width of the window in pixels or another unit type. If None, a default width is used. height : Optional[int], default=None The height of the window in pixels or another unit type. If None, a default height is used. caption : Optional[str], default=None The caption or title of the window. If None, no caption is displayed. fullscreen : bool, default=False Indicates if the window should be displayed in fullscreen mode. visible : bool, default=True Specifies whether the window is visible upon creation. background_color : Optional[ColorType], default=None The background color of the window, provided as an RGB/RGBA tuple or a named color string. If None, the default color is used. mouse_visible : bool, default=False Determines if the mouse cursor should be visible within the window. units : Union[UnitType, Unit], default="norm" The unit system for the window. Can be a string or a Unit object to manage coordinates and sizes. distance : Optional[float], default=50 The viewing distance from the screen in centimeters, used for visual angle calculations (e.g., degrees). inches : Optional[float], default=None The diagonal size of the monitor in inches. Required for accurate DPI (dots per inch) and physical size calculations. clear_after_flip : bool, default=True If True, the window will be cleared after flipping the frame buffer, preparing it for the next frame. kwargs : dict Additional keyword arguments to be passed to the Pyglet window constructor. Attributes ---------- distance : float The viewing distance in centimeters, used for units like degrees of visual angle. inches : float The diagonal size of the monitor in inches, used for DPI calculations. clear_after_flip : bool Whether the window is automatically cleared after each frame flip. units : Unit The current unit system used to convert between different coordinate and size units. Examples -------- Basic usage for displaying visual elements: .. code-block:: python from psychos import Window, Text # Create a new window window = Window() # Create a text stimulus and draw it text = Text("Hello, World!").draw() # Display the drawn elements on the screen window.flip() # Keep the window open for 3 seconds window.wait(3) You can also display other elements like images: .. code-block:: python from psychos import Window, Image from pathlib import Path # Create a new window window = Window() # Load and draw an image img_path = Path("image.png") image = Image(img_path).draw() # Flip the window to display the drawn elements window.flip() # Keep the window open for 3 seconds window.wait(3) """
[docs] def __init__( self, width: Optional[int] = None, height: Optional[int] = None, caption: Optional[str] = None, fullscreen: bool = False, visible: bool = True, background_color: Optional["ColorType"] = None, mouse_visible: bool = False, coordinates: Union["UnitType", "Unit"] = "norm", distance: Optional[float] = 50, inches: Optional[float] = None, clear_after_flip: bool = True, screen: Optional[Union["pyglet.canvas.Screen", int]] = None, **kwargs, ): """Creates an instance of the Window class.""" if isinstance(screen, int): display = pyglet.canvas.get_display() screen = display.get_screens()[screen] super().__init__( caption=caption, fullscreen=fullscreen, visible=visible, screen=screen, **kwargs, ) self.distance = distance self.inches = inches self.clear_after_flip = clear_after_flip self._coordinates = None self._background_color = None self._flip_callbacks = [] if not self.fullscreen and height is not None: self.height = height if not self.fullscreen and width is not None: self.width = width self.coordinates = coordinates self.background_color = background_color self.set_mouse_visible(mouse_visible) self.dispatch_events()
@PygletWindow.width.setter def width(self, value: Optional[Union[str, int, float]]) -> None: """Set the width of the window.""" value = parse_width(value, window=self) super().width = value @PygletWindow.height.setter def height(self, value: Optional[Union[str, int, float]]) -> None: """Set the height of the window.""" value = parse_height(value, window=self) super().height = value @property def coordinates(self) -> "Unit": """Get the unit system used by the window.""" return self._coordinates @coordinates.setter def coordinates(self, value: Union["UnitType", "Unit"]) -> None: """Set the unit system used by the window.""" self._coordinates = Unit.from_name(value, window=self) @property def background_color(self) -> Optional[Tuple[float, float, float, float]]: """Get the background color of the window (r, g, b, a).""" return self._background_color @background_color.setter def background_color(self, color: Optional[Union["ColorType", "Color"]]) -> None: """ Set the background color of the window. Parameters ---------- color : Optional[ColorType] The background color as a tuple (r, g, b, a) or a color name, or a Color object. """ self._background_color = Color(color).to_rgba() if self._background_color is not None: pyglet.gl.glClearColor(*self._background_color) self.clear() @property def dpi(self) -> float: """ Get the number of pixels per centimeter in the window, from the monitor's size in inches. Returns ------- float The number of pixels per centimeter. """ if not self.inches: raise ValueError( "The diagonal size in inches must be set to calculate DPI. " "Specify `inches` to the window constructor." ) # Calculate diagonal resolution in pixels diagonal_pixels = (self.screen.width**2 + self.screen.height**2) ** 0.5 # Calculate pixels per inch (DPI) dpi = diagonal_pixels / self.inches # Convert DPI to pixels per centimeter return dpi
[docs] def on_flip(self, func, *args, **kwargs): """ Register a function to be called after flipping the window's frame buffer. This method allows you to register a function to be called after flipping the window's frame buffer. The function will be called with the provided arguments and keyword arguments after the flip operation is completed. If several functions are registered, they will be called in the order they were added. """ self._flip_callbacks.append((func, args, kwargs))
[docs] def flip(self, clear: Optional[bool] = None) -> "Window": """ Flip the window's frame buffer and optionally clear the window after. Parameters ---------- clear : Optional[bool], default=None Whether to clear the window after flipping. Defaults to the value of `self.clear_after_flip`. """ super().flip() clear = clear if clear is not None else self.clear_after_flip if clear: self.clear() if self._flip_callbacks: for func, args, kwargs in self._flip_callbacks: func(*args, **kwargs) self._flip_callbacks.clear() return self
[docs] def wait(self, duration: float = 1, sleep_interval: float = 0.8, hog_period: float = 0.02): """ Wait for a specified duration while dispatching window events. Parameters ---------- duration : float, default=1 The duration to wait in seconds. sleep_interval : float, default=0.8 The interval to sleep between event dispatches. hog_period : float, default=0.02 The period to hog the CPU at the end of the wait. This is do to increase the accuracy of the wait time. """ wait(duration=duration, sleep_interval=sleep_interval, hog_period=hog_period)
[docs] def wait_key( self, keys: Optional[Union[Iterable[Union[str, int]], str, int]] = None, modifiers: Optional[Union[Iterable[Union[str, int]], str, int]] = None, clock: Optional["Clock"] = None, max_wait: Optional[float] = None, event: "Literal['press', 'release']" = "press", clear_events: bool = True, ) -> "KeyEvent": """ Wait for a specific key event (press or release) within the given time frame. This function waits for a key event (either press or release) to occur, and returns the key, modifiers, and the timestamp when the event happened. It supports specifying a set of keys to listen for, or returning the first key event of any kind if no specific keys are provided. The function can also accept a timeout (`max_wait`), after which it will return even if no key event occurs. If `modifiers` are provided, the function will check if all specified modifiers are pressed at the time of the key event. Modifiers can be ignored if `modifiers` is set to `None`, or you can enforce that no modifiers are pressed by passing an empty list. Parameters ---------- keys : Optional[Union[Iterable[Union[str, int]], str, int]] The keys to wait for. It can be one of the following: - A string representing the key's name (e.g., "SPACE", "A", etc.). - An integer representing the Pyglet key ID (e.g., `pyglet.window.key.SPACE`). - An iterable of strings or integers representing multiple keys. If no keys are provided (`keys=None`), the function will return on any key press or release event. modifiers : Optional[Union[Iterable[Union[str, int]], str, int]] The modifiers to check for. It can be one of the following: - A string representing a modifier name (e.g., "CTRL", "SHIFT"). - An integer representing the Pyglet modifier bitmask (e.g., `pyglet.window.key.MOD_SHIFT`). - An iterable of strings or integers representing multiple modifiers. If `None`, the function ignores any modifiers. If an empty list is provided, the function will only return when no modifiers are pressed. clock : Optional["Clock"] An optional clock object for measuring time. If not provided, the function will use `time.time()` from the standard library. The clock object should have a `.time()` method that returns the current time. max_wait : Optional[float] The maximum amount of time to wait for the key event (in seconds). If this value is not provided, the function will wait indefinitely for a key event. If the timeout is reached before a key event occurs, the function returns `None` and the current timestamp. event : Literal["press", "release"], default "press" Specifies whether to wait for a key press event (`"press"`) or a key release event (`"release"`). clear_events : bool, default True Whether to clear any pending events before waiting for the key event. This can be useful to avoid processing old events that occurred before calling this function. Returns ------- KeyEvent A named tuple containing the following: - ``key``: The pressed or released key, returned as a string (e.g., "SPACE"). If `max_wait` is reached without any event, this will be `None`. - ``modifiers``: A string representation of the modifiers (e.g., "CTRL|SHIFT") pressed at the time of the event. If no modifiers were pressed, this will be an empty string. If modifiers are ignored (`modifiers=None`), this will also be empty. - ``timestamp``: The timestamp when the key event occurred, using either the provided clock or `time.time()`. - ``event``: A string representing whether the key event was a "press" or "release". Raises ------ AssertionError If an invalid event type is passed or if the window is not found. Examples -------- Wait for the SPACE key to be pressed or released within 5 seconds: >>> key_event = wait_key(keys="SPACE", max_wait=5) >>> if key_event.key: ... print(f"Key {key_event.key} pressed with {key_event.modifiers}") ... else: ... print(f"No key pressed within 5 seconds, timestamp: {key_event.timestamp}") Wait for any key press event: >>> key_event = wait_key(max_wait=10) >>> print(f"Key {key_event.key} pressed with {key_event.modifiers} ({key_event.timestamp})") """ return wait_key( keys=keys, modifiers=modifiers, clock=clock, max_wait=max_wait, event=event, clear_events=clear_events, window=self, )