import platform
import pyglet
# On OS X we need to disable the shadow context
# because the 2.1 shadow context cannot be upgrade to a 3.3+ core
if platform.system() == "Darwin":
pyglet.options["shadow_window"] = False
pyglet.options["debug_gl"] = False
from pathlib import Path # noqa
from typing import Any, Union # noqa
from moderngl_window.context.base import BaseWindow # noqa: E402
from moderngl_window.context.pyglet.keys import Keys # noqa: E402
[docs]
class Window(BaseWindow):
"""
Window based on Pyglet 1.4.x
"""
#: Name of the window
name = "pyglet"
#: Pyglet specific key constants
keys = Keys
# pyglet button id -> universal button id
_mouse_button_map = {
1: 1,
4: 2,
2: 3,
}
def __init__(self, **kwargs: Any):
super().__init__(**kwargs)
config = pyglet.gl.Config(
major_version=self.gl_version[0],
minor_version=self.gl_version[1],
forward_compatible=True,
red_size=8,
green_size=8,
blue_size=8,
alpha_size=8,
stencil_size=8,
depth_size=24,
double_buffer=True,
sample_buffers=1 if self.samples > 1 else 0,
samples=self.samples,
)
if self.fullscreen:
display = pyglet.canvas.get_display()
screen = display.get_default_screen()
self._width, self._height = screen.width, screen.height
self._window = PygletWrapper(
width=self._width,
height=self._height,
caption=self._title,
resizable=self._resizable,
visible=self._visible,
vsync=self._vsync,
fullscreen=self._fullscreen,
config=config,
file_drops=True and platform.system() != "Darwin",
)
self.cursor = self._cursor
self._window.event(self.on_key_press)
self._window.event(self.on_key_release)
self._window.event(self.on_mouse_motion)
self._window.event(self.on_mouse_drag)
self._window.event(self.on_resize)
self._window.event(self.on_close)
self._window.event(self.on_mouse_press)
self._window.event(self.on_mouse_release)
self._window.event(self.on_mouse_scroll)
self._window.event(self.on_text)
self._window.event(self.on_show)
self._window.event(self.on_hide)
self._window.event(self.on_file_drop)
self.init_mgl_context()
self._buffer_width, self._buffer_height = self._window.get_framebuffer_size()
self.set_default_viewport()
def _set_fullscreen(self, value: bool) -> None:
self._window.set_fullscreen(value)
@property
def size(self) -> tuple[int, int]:
"""tuple[int, int]: current window size.
This property also support assignment::
# Resize the window to 1000 x 1000
window.size = 1000, 1000
"""
return self._width, self._height
@size.setter
def size(self, value: tuple[int, int]) -> None:
self._window.set_size(value[0], value[1])
@property
def position(self) -> tuple[int, int]:
"""tuple[int, int]: The current window position.
This property can also be set to move the window::
# Move window to 100, 100
window.position = 100, 100
"""
return self._window.get_location()
@position.setter
def position(self, value: tuple[int, int]) -> None:
self._window.set_location(value[0], value[1])
@property
def visible(self) -> bool:
"""bool: Is the window visible?
This property can also be set::
# Hide or show the window
window.visible = False
"""
return self._visible
@visible.setter
def visible(self, value: bool) -> None:
self._visible = value
self._window.set_visible(value)
@property
def cursor(self) -> bool:
"""bool: Should the mouse cursor be visible inside the window?
This property can also be assigned to::
# Disable cursor
window.cursor = False
"""
return self._cursor
@cursor.setter
def cursor(self, value: bool) -> None:
self._window.set_mouse_visible(value)
self._cursor = value
@property
def mouse_exclusivity(self) -> bool:
"""bool: If mouse exclusivity is enabled.
When you enable mouse-exclusive mode, the mouse cursor is no longer
available. It is not merely hidden – no amount of mouse movement
will make it leave your application. This is for example useful
when you don't want the mouse leaving the screen when rotating
a 3d scene.
This property can also be set::
window.mouse_exclusivity = True
"""
return self._mouse_exclusivity
@mouse_exclusivity.setter
def mouse_exclusivity(self, value: bool) -> None:
self._window.set_exclusive_mouse(value)
self._mouse_exclusivity = value
@property
def title(self) -> str:
"""str: Window title.
This property can also be set::
window.title = "New Title"
"""
return self._title
@title.setter
def title(self, value: str) -> None:
self._window.set_caption(value)
self._title = value
@property
def is_closing(self) -> bool:
"""Check pyglet's internal exit state"""
return self._window.has_exit or super().is_closing
@is_closing.setter
def is_closing(self, value: bool) -> None:
self._close = value
[docs]
def close(self) -> None:
"""Close the pyglet window directly"""
self.is_closing = True
self._window.close()
super().close()
[docs]
def swap_buffers(self) -> None:
"""Swap buffers, increment frame counter and pull events"""
self._window.flip()
self._frames += 1
self._window.dispatch_events()
def _handle_modifiers(self, mods: int) -> None:
"""Update key modifier states"""
self._modifiers.shift = mods & 1 == 1
self._modifiers.ctrl = mods & 2 == 2
self._modifiers.alt = mods & 4 == 4
def _set_icon(self, icon_path: Path) -> None:
icon = pyglet.image.load(icon_path)
self._window.set_icon(icon)
def _set_vsync(self, value: bool) -> None:
self._window.set_vsync(value)
[docs]
def on_key_press(self, symbol: int, modifiers: int) -> bool:
"""Pyglet specific key press callback.
Forwards and translates the events to the standard methods.
Args:
symbol: The symbol of the pressed key
modifiers: Modifier state (shift, ctrl etc.)
"""
if self._exit_key is not None and symbol == self._exit_key:
self.close()
if self._fs_key is not None and symbol == self._fs_key:
self.fullscreen = not self.fullscreen
self._key_pressed_map[symbol] = True
self._handle_modifiers(modifiers)
self._key_event_func(symbol, self.keys.ACTION_PRESS, self._modifiers)
return pyglet.event.EVENT_HANDLED
[docs]
def on_text(self, text: str) -> None:
"""Pyglet specific text input callback
Forwards and translates the events to the standard methods.
Args:
text (str): The unicode character entered
"""
self._unicode_char_entered_func(text)
[docs]
def on_key_release(self, symbol: int, modifiers: int) -> None:
"""Pyglet specific key release callback.
Forwards and translates the events to standard methods.
Args:
symbol: The symbol of the pressed key
modifiers: Modifier state (shift, ctrl etc.)
"""
self._key_pressed_map[symbol] = False
self._handle_modifiers(modifiers)
self._key_event_func(symbol, self.keys.ACTION_RELEASE, self._modifiers)
[docs]
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
"""Pyglet specific mouse motion callback.
Forwards and translates the event to the standard methods.
Args:
x: x position of the mouse
y: y position of the mouse
dx: delta x position
dy: delta y position of the mouse
"""
# NOTE: Screen coordinates relative to the lower-left corner
# so we have to flip the y axis to make this consistent with
# other window libraries
self._mouse_position_event_func(x, self._height - y, dx, -dy)
[docs]
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None:
"""Pyglet specific mouse drag event.
When a mouse button is pressed this is the only way
to capture mouse position events
"""
self._handle_modifiers(modifiers)
self._mouse_drag_event_func(x, self._height - y, dx, -dy)
[docs]
def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None:
"""Handle mouse press events and forward to standard methods
Args:
x: x position of the mouse when pressed
y: y position of the mouse when pressed
button: The pressed button
mods: Modifiers
"""
self._handle_modifiers(mods)
button = self._mouse_button_map.get(button, -1)
if button != -1:
self._handle_mouse_button_state_change(button, True)
self._mouse_press_event_func(
x,
self._height - y,
button,
)
[docs]
def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None:
"""Handle mouse release events and forward to standard methods
Args:
x: x position when mouse button was released
y: y position when mouse button was released
button: The button pressed
mods: Modifiers
"""
button = self._mouse_button_map.get(button, -1)
if button != -1:
self._handle_mouse_button_state_change(button, False)
self._mouse_release_event_func(
x,
self._height - y,
button,
)
[docs]
def on_resize(self, width: int, height: int) -> None:
"""Pyglet specific callback for window resize events forwarding to standard methods
Args:
width: New window width
height: New window height
"""
self._width, self._height = width, height
self._buffer_width, self._buffer_height = self._window.get_framebuffer_size()
self.set_default_viewport()
super().resize(self._buffer_width, self._buffer_height)
[docs]
def on_close(self) -> None:
"""Pyglet specific window close callback"""
self._close_func()
[docs]
def on_show(self) -> None:
"""Called when window first appear or restored from hidden state"""
self._visible = True
self._iconify_func(False)
[docs]
def on_hide(self) -> None:
"""Called when window is minimized"""
self._visible = False
self._iconify_func(True)
[docs]
def on_file_drop(self, x: int, y: int, paths: list[Union[str, Path]]) -> None:
"""Called when files dropped onto the window
Args:
x (int): X location in window where file was dropped
y (int): Y location in window where file was dropped
paths (list): List of file paths dropped
"""
# pyglet coordinate origin is in the bottom left corner of the window
# mglw coordinate origin is in the top left corner of the window
# convert pyglet coordinates to mglw coordinates:
(x, y) = self.convert_window_coordinates(x, y, y_flipped=True)
self._files_dropped_event_func(x, y, paths)
[docs]
def destroy(self) -> None:
"""Destroy the pyglet window"""
pass
class PygletWrapper(pyglet.window.Window):
"""Block out some window methods so pyglet don't trigger GL errors"""
def on_resize(self, width: int, height: int) -> None:
"""Block out the resize method.
For some reason pyglet calls this triggering errors.
"""
pass
def on_draw(self) -> None:
"""Block out the default draw method to avoid GL errors"""
pass