File: __init__.py

package info (click to toggle)
python-moderngl-window 2.4.6-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 69,220 kB
  • sloc: python: 11,387; makefile: 21
file content (372 lines) | stat: -rw-r--r-- 10,828 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
"""
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.6"

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__)


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


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


def window():
    """Obtain the active window"""
    if ContextRefs.WINDOW:
        return ContextRefs.WINDOW

    raise ValueError("No active window and context. Call activate_window.")


def ctx():
    """Obtain the active context"""
    if ContextRefs.CONTEXT:
        return ContextRefs.CONTEXT

    raise ValueError("No active window and context. Call activate_window.")


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)


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))


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


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 ---


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,
        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)
    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
            )
        )


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(
        "-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


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",)