"""
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 List, Type, Optional
import moderngl
from moderngl_window.context.base import WindowConfig, BaseWindow
from moderngl_window.timers.clock import Timer
from moderngl_window.conf import settings
from moderngl_window.utils.module_loading import import_string
from moderngl_window.utils.keymaps import KeyMapFactory, KeyMap, QWERTY, AZERTY # noqa
__version__ = "2.4.5"
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):
"""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)
class ContextRefs:
"""Namespace for window/context references"""
WINDOW: Optional[BaseWindow] = None
CONTEXT: Optional[moderngl.Context] = None
[docs]
def activate_context(window: BaseWindow = None, ctx: moderngl.Context = 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 not ctx:
ContextRefs.CONTEXT = window.ctx
[docs]
def window():
"""Obtain the active window"""
if ContextRefs.WINDOW:
return ContextRefs.WINDOW
raise ValueError("No active window and context. Call activate_window.")
[docs]
def ctx():
"""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 = None) -> 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)
return import_string(window)
[docs]
def get_local_window_cls(window: 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 not window:
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)
activate_context(window=window)
return window
# --- The simple window config system ---
[docs]
def run_window_config(config_cls: WindowConfig, timer=None, args=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,
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)
timer = timer or 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():
"""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(
"-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=None, parser=None):
"""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 ---
def valid_bool(value):
"""Validator for bool values"""
value = value.lower()
if value is None:
return None
if value in OPTIONS_TRUE:
return True
if value in OPTIONS_FALSE:
return False
raise argparse.ArgumentTypeError(
"Boolean value expected. Options: {}".format(OPTIONS_ALL)
)
def valid_window_size(value):
"""
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'",
)
def valid_window_size_multiplier(value):
"""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",)