Files
Jérome Eertmans ae8d5b6aab chore(lib): re-organizing the module
This PR refactors a lot of the code in order to have a better organized codebase. If will that I needed to create a second level of submodules, to better distinguish the different parts of this project.
2024-08-20 11:31:58 +02:00

295 lines
7.9 KiB
Python

import signal
import sys
from pathlib import Path
from typing import Optional
import click
from click import Context, Parameter
from pydantic import ValidationError
from ...core.config import Config, PresentationConfig, list_presentation_configs
from ...core.logger import logger
from ..commons import (
config_path_option,
folder_path_option,
scenes_argument,
verbosity_option,
)
PREFERRED_QT_VERSIONS = ("6.5.1", "6.5.2")
def warn_if_non_desirable_pyside6_version() -> None:
from qtpy import API, QT_VERSION
if sys.version_info < (3, 12) and (
API != "pyside6" or QT_VERSION not in PREFERRED_QT_VERSIONS
):
logger.warn(
f"You are using {API = }, {QT_VERSION = }, "
"but we recommend installing 'PySide6==6.5.2', mainly to avoid "
"flashing screens between slides, "
"see issue https://github.com/jeertmans/manim-slides/issues/293. "
"You can do so with `pip install 'manim-slides[pyside6]'`."
)
@click.command()
@folder_path_option
@click.help_option("-h", "--help")
@verbosity_option
def list_scenes(folder: Path) -> None:
"""List available scenes."""
scene_names = [path.stem for path in list_presentation_configs(folder)]
num_digits = len(str(len(scene_names)))
for i, scene_name in enumerate(scene_names, start=1):
click.secho(f"{i:{num_digits}d}: {scene_name}", fg="green")
def start_at_callback(
ctx: Context, param: Parameter, values: str
) -> tuple[Optional[int], ...]:
if values == "(None, None)":
return (None, None)
def str_to_int_or_none(value: str) -> Optional[int]:
if value.lower().strip() == "":
return None
else:
try:
return int(value)
except ValueError:
raise click.BadParameter(
f"start index can only be an integer or an empty string, not `{value}`",
ctx=ctx,
param=param,
) from None
values_tuple = values.split(",")
n_values = len(values_tuple)
if n_values == 2:
return tuple(map(str_to_int_or_none, values_tuple))
raise click.BadParameter(
f"exactly 2 arguments are expected but you gave {n_values}, "
"please use commas to separate them",
ctx=ctx,
param=param,
)
@click.command()
@scenes_argument
@config_path_option
@folder_path_option
@click.option("--start-paused", is_flag=True, help="Start paused.")
@click.option(
"-F",
"--full-screen",
"--fullscreen",
"full_screen",
is_flag=True,
help="Toggle full screen mode.",
)
@click.option(
"-s",
"--skip-all",
is_flag=True,
help="Skip all slides, useful the test if slides are working. "
"Automatically sets ``--exit-after-last-slide`` to True.",
)
@click.option(
"--exit-after-last-slide",
is_flag=True,
help="At the end of last slide, the application will be exited.",
)
@click.option(
"-H",
"--hide-mouse",
is_flag=True,
help="Hide mouse cursor.",
)
@click.option(
"--aspect-ratio",
type=click.Choice(["keep", "ignore"], case_sensitive=False),
default="keep",
help="Set the aspect ratio mode to be used when rescaling the video.",
show_default=True,
)
@click.option(
"--sa",
"--start-at",
"start_at",
metavar="<SCENE,SLIDE>",
type=str,
callback=start_at_callback,
default=(None, None),
help="Start presenting at (x, y), equivalent to ``--sacn x --sasn y``, "
"and overrides values if not None.",
)
@click.option(
"--sacn",
"--start-at-scene-number",
"start_at_scene_number",
metavar="INDEX",
type=int,
default=0,
help="Start presenting at a given scene number (0 is first, -1 is last).",
)
@click.option(
"--sasn",
"--start-at-slide-number",
"start_at_slide_number",
metavar="INDEX",
type=int,
default=0,
help="Start presenting at a given slide number (0 is first, -1 is last).",
)
@click.option(
"-S",
"--screen",
"screen_number",
metavar="NUMBER",
type=int,
default=None,
help="Present content on the given screen (a.k.a. display).",
)
@click.option(
"--playback-rate",
metavar="RATE",
type=float,
default=1.0,
help="Playback rate of the video slides, see PySide6 docs for details. "
" The playback rate of each slide is defined as the product of its default "
" playback rate and the provided value.",
)
@click.option(
"--next-terminates-loop",
"next_terminates_loop",
is_flag=True,
help="If set, pressing next will turn any looping slide into a play slide.",
)
@click.option(
"--hide-info-window",
is_flag=True,
help="Hide info window.",
)
@click.option(
"--info-window-screen",
"info_window_screen_number",
metavar="NUMBER",
type=int,
default=None,
help="Put info window on the given screen (a.k.a. display).",
)
@click.help_option("-h", "--help")
@verbosity_option
def present(
scenes: list[Path],
config_path: Path,
folder: Path,
start_paused: bool,
full_screen: bool,
skip_all: bool,
exit_after_last_slide: bool,
hide_mouse: bool,
aspect_ratio: str,
start_at: tuple[Optional[int], Optional[int], Optional[int]],
start_at_scene_number: int,
start_at_slide_number: int,
screen_number: Optional[int],
playback_rate: float,
next_terminates_loop: bool,
hide_info_window: bool,
info_window_screen_number: Optional[int],
) -> None:
"""
Present SCENE(s), one at a time, in order.
Each SCENE parameter must be the name of a Manim scene,
with existing SCENE.json config file.
You can present the same SCENE multiple times by repeating the parameter.
Use ``manim-slide list-scenes`` to list all available
scenes in a given folder.
"""
if skip_all:
exit_after_last_slide = True
presentation_configs = [PresentationConfig.from_file(path) for path in scenes]
if config_path.exists():
try:
config = Config.from_file(config_path)
except ValidationError as e:
raise click.UsageError(str(e)) from None
else:
logger.debug("No configuration file found, using default configuration.")
config = Config()
if start_at[0]:
start_at_scene_number = start_at[0]
if start_at[1]:
start_at_slide_number = start_at[1]
warn_if_non_desirable_pyside6_version()
from qtpy.QtCore import Qt
from qtpy.QtGui import QScreen
from ..qt_utils import qapp
from .player import Player
app = qapp()
app.setApplicationName("Manim Slides")
def get_screen(number: int) -> Optional[QScreen]:
try:
return app.screens()[number]
except IndexError:
logger.error(
f"Invalid screen number {number}, "
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
)
return None
if screen_number is not None:
screen = get_screen(screen_number)
else:
screen = None
if info_window_screen_number is not None:
info_window_screen = get_screen(info_window_screen_number)
else:
info_window_screen = None
aspect_ratio_modes = {
"keep": Qt.KeepAspectRatio,
"ignore": Qt.IgnoreAspectRatio,
}
player = Player(
config,
presentation_configs,
start_paused=start_paused,
full_screen=full_screen,
skip_all=skip_all,
exit_after_last_slide=exit_after_last_slide,
hide_mouse=hide_mouse,
aspect_ratio_mode=aspect_ratio_modes[aspect_ratio],
presentation_index=start_at_scene_number,
slide_index=start_at_slide_number,
screen=screen,
playback_rate=playback_rate,
next_terminates_loop=next_terminates_loop,
hide_info_window=hide_info_window,
info_window_screen=info_window_screen,
)
player.show()
signal.signal(signal.SIGINT, signal.SIG_DFL)
sys.exit(app.exec())