"""
General helper functions aiding in the boostrapping of this library.
"""
# pylint: disable = redefined-outer-name, too-few-public-methods
import argparse
import logging
import os
import sys
import weakref
from pathlib import Path
from typing import Any, Optional
import moderngl
from moderngl_window.conf import settings
from moderngl_window.context.base import BaseWindow, WindowConfig
from moderngl_window.timers.clock import Timer
from moderngl_window.utils.keymaps import AZERTY, QWERTY, KeyMap, KeyMapFactory # noqa
from moderngl_window.utils.module_loading import import_string
__version__ = "3.0.0"
IGNORE_DIRS = [
"__pycache__",
"base",
]
# Add new windows classes here to be recognized by the command line option --window
WINDOW_CLASSES = [
"glfw",
"headless",
"pygame2",
"pyglet",
"pyqt5",
"pyside2",
"sdl2",
"tk",
]
OPTIONS_TRUE = ["yes", "on", "true", "t", "y", "1"]
OPTIONS_FALSE = ["no", "off", "false", "f", "n", "0"]
OPTIONS_ALL = OPTIONS_TRUE + OPTIONS_FALSE
# Quick and dirty debug logging setup by default
# See: https://docs.python.org/3/howto/logging.html#logging-advanced-tutorial
logger = logging.getLogger(__name__)
[docs]
def setup_basic_logging(level: int) -> None:
"""Set up basic logging
Args:
level (int): The log level
"""
if level is None:
return
# Do not add a new handler if we already have one
if not logger.handlers:
logger.propagate = False
logger.setLevel(level)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
logger.addHandler(ch)
[docs]
class ContextRefs:
"""Namespace for window/context references"""
WINDOW: Optional[BaseWindow] = None
CONTEXT: Optional[moderngl.Context] = None
[docs]
def activate_context(
window: Optional[BaseWindow] = None, ctx: Optional[moderngl.Context] = None
) -> None:
"""
Register the active window and context.
If only a window is supplied the context is taken from the window.
Only a context can also be passed in.
Keyword Args:
window (window): The window to activate
ctx (moderngl.Context): The moderngl context to activate
"""
ContextRefs.WINDOW = window
ContextRefs.CONTEXT = ctx
if ctx is None:
ContextRefs.CONTEXT = window.ctx
[docs]
def window() -> BaseWindow:
"""Obtain the active window"""
if ContextRefs.WINDOW:
return ContextRefs.WINDOW
raise ValueError("No active window and context. Call activate_window.")
[docs]
def ctx() -> moderngl.Context:
"""Obtain the active context"""
if ContextRefs.CONTEXT:
return ContextRefs.CONTEXT
raise ValueError("No active window and context. Call activate_window.")
[docs]
def get_window_cls(window: str = "") -> type[BaseWindow]:
"""
Attempt to obtain a window class using the full dotted
python path. This can be used to import custom or modified
window classes.
Args:
window (str): Name of the window
Returns:
A reference to the requested window class. Raises exception if not found.
"""
logger.info("Attempting to load window class: %s", window)
win = import_string(window)
# assert issubclass(
# win, BaseWindow
# ), f"{win} is not derived from moderngl_window.context.base.BaseWindow"
return win
[docs]
def get_local_window_cls(window: Optional[str] = None) -> type[BaseWindow]:
"""
Attempt to obtain a window class in the moderngl_window package
using short window names such as ``pyglet`` or ``glfw``.
Args:
window (str): Name of the window
Returns:
A reference to the requested window class. Raises exception if not found.
"""
window = os.environ.get("MODERNGL_WINDOW") or window
if window is None:
window = "pyglet"
return get_window_cls("moderngl_window.context.{}.Window".format(window))
[docs]
def find_window_classes() -> list[str]:
"""
Find available window packages
Returns:
A list of available window packages
"""
# In some environments we cannot rely on introspection
# and instead return a hardcoded list
try:
return [
path.parts[-1]
for path in Path(__file__).parent.joinpath("context").iterdir()
if path.is_dir() and path.parts[-1] not in IGNORE_DIRS
]
except Exception:
return WINDOW_CLASSES
[docs]
def create_window_from_settings() -> BaseWindow:
"""
Creates a window using configured values in :py:attr:`moderngl_window.conf.Settings.WINDOW`.
This will also activate the window/context.
Returns:
The Window instance
"""
window_cls = import_string(settings.WINDOW["class"])
window = window_cls(**settings.WINDOW)
assert isinstance(
window, BaseWindow
), f"{type(window)} is not derived from moderngl_window.context.base.BaseWindow"
activate_context(window=window)
return window
# --- The simple window config system ---
[docs]
def run_window_config(
config_cls: type[WindowConfig], timer: Optional[Timer] = None, args: Any = None
) -> None:
"""
Run an WindowConfig entering a blocking main loop
Args:
config_cls: The WindowConfig class to render
Keyword Args:
timer: A custom timer instance
args: Override sys.args
"""
setup_basic_logging(config_cls.log_level)
parser = create_parser()
config_cls.add_arguments(parser)
values = parse_args(args=args, parser=parser)
config_cls.argv = values
window_cls = get_local_window_cls(values.window)
# Calculate window size
size = values.size or config_cls.window_size
size = int(size[0] * values.size_mult), int(size[1] * values.size_mult)
# Resolve cursor
show_cursor = values.cursor
if show_cursor is None:
show_cursor = config_cls.cursor
window = window_cls(
title=config_cls.title,
size=size,
fullscreen=config_cls.fullscreen or values.fullscreen,
resizable=(values.resizable if values.resizable is not None else config_cls.resizable),
visible=config_cls.visible,
gl_version=config_cls.gl_version,
aspect_ratio=config_cls.aspect_ratio,
vsync=values.vsync if values.vsync is not None else config_cls.vsync,
samples=values.samples if values.samples is not None else config_cls.samples,
cursor=show_cursor if show_cursor is not None else True,
backend=values.backend,
)
window.print_context_info()
activate_context(window=window)
if timer is None:
timer = Timer()
config = config_cls(ctx=window.ctx, wnd=window, timer=timer)
# Avoid the event assigning in the property setter for now
# We want the even assigning to happen in WindowConfig.__init__
# so users are free to assign them in their own __init__.
window._config = weakref.ref(config)
# Swap buffers once before staring the main loop.
# This can trigged additional resize events reporting
# a more accurate buffer size
window.swap_buffers()
window.set_default_viewport()
timer.start()
while not window.is_closing:
current_time, delta = timer.next_frame()
if config.clear_color is not None:
window.clear(*config.clear_color)
# Always bind the window framebuffer before calling render
window.use()
window.render(current_time, delta)
if not window.is_closing:
window.swap_buffers()
_, duration = timer.stop()
window.destroy()
if duration > 0:
logger.info("Duration: {0:.2f}s @ {1:.2f} FPS".format(duration, window.frames / duration))
[docs]
def create_parser() -> argparse.ArgumentParser:
"""Create an argparser parsing the standard arguments for WindowConfig"""
parser = argparse.ArgumentParser()
parser.add_argument(
"-wnd",
"--window",
choices=find_window_classes(),
help="Name for the window type to use",
)
parser.add_argument(
"-fs",
"--fullscreen",
action="store_true",
help="Open the window in fullscreen mode",
)
parser.add_argument(
"-vs",
"--vsync",
type=valid_bool,
help="Enable or disable vsync",
)
parser.add_argument(
"-r",
"--resizable",
type=valid_bool,
default=None,
help="Enable/disable window resize",
)
parser.add_argument(
"-hd",
"--hidden",
type=valid_bool,
default=False,
help="Start the window in hidden mode",
)
parser.add_argument(
"-s",
"--samples",
type=int,
help="Specify the desired number of samples to use for multisampling",
)
parser.add_argument(
"-c",
"--cursor",
type=valid_bool,
help="Enable or disable displaying the mouse cursor",
)
parser.add_argument(
"--size",
type=valid_window_size,
help="Window size",
)
parser.add_argument(
"--size_mult",
type=valid_window_size_multiplier,
default=1.0,
help="Multiplier for the window size making it easy scale the window",
)
parser.add_argument(
"--backend",
help="Specify context backend. This is mostly used to enable EGL in headless mode",
)
return parser
[docs]
def parse_args(
args: Optional[Any] = None, parser: Optional[argparse.ArgumentParser] = None
) -> argparse.Namespace:
"""Parse arguments from sys.argv
Passing in your own argparser can be user to extend the parser.
Keyword Args:
args: override for sys.argv
parser: Supply your own argparser instance
"""
parser = parser or create_parser()
return parser.parse_args(args or sys.argv[1:])
# --- Validators ---
[docs]
def valid_bool(value: Optional[str]) -> Optional[bool]:
"""Validator for bool values"""
if value is None:
return None
value = value.lower()
if value in OPTIONS_TRUE:
return True
if value in OPTIONS_FALSE:
return False
raise argparse.ArgumentTypeError(f"Boolean value expected. Options: {OPTIONS_ALL}")
[docs]
def valid_window_size(value: str) -> tuple[int, int]:
"""
Validator for window size parameter.
Valid format is "[int]x[int]". For example "1920x1080".
"""
try:
width, height = value.split("x")
return int(width), int(height)
except ValueError:
pass
raise argparse.ArgumentTypeError(
"Valid size format: int]x[int]. Example '1920x1080'",
)
[docs]
def valid_window_size_multiplier(value: str) -> float:
"""Validates window size multiplier
Must be an integer or float greater than 0
"""
try:
val = float(value)
if val > 0:
return val
except ValueError:
pass
raise argparse.ArgumentTypeError(
"Must be a positive int or float",
)