mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-19 11:36:37 +08:00

* wip: rewrite player * wip(cli): new player * wip(player): allow to close * Auto stash before merge of "rewrite-player" and "origin/rewrite-player" * feat(cli): new player * chore(docs): document changes * feat(cli): add info window
347 lines
10 KiB
Python
347 lines
10 KiB
Python
from pathlib import Path
|
|
from typing import Any, List, Optional
|
|
|
|
from PySide6.QtCore import Qt, QUrl, Signal, Slot
|
|
from PySide6.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
|
|
from PySide6.QtMultimedia import QMediaPlayer
|
|
from PySide6.QtMultimediaWidgets import QVideoWidget
|
|
from PySide6.QtWidgets import QDialog, QGridLayout, QLabel, QMainWindow
|
|
|
|
from ..config import Config, PresentationConfig, SlideConfig
|
|
from ..logger import logger
|
|
from ..resources import * # noqa: F401, F403
|
|
|
|
WINDOW_NAME = "Manim Slides"
|
|
|
|
|
|
class Info(QDialog): # type: ignore[misc]
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
super().__init__(*args, **kwargs)
|
|
|
|
layout = QGridLayout()
|
|
self.scene_label = QLabel()
|
|
self.slide_label = QLabel()
|
|
|
|
layout.addWidget(QLabel("Scene:"), 1, 1)
|
|
layout.addWidget(QLabel("Slide:"), 2, 1)
|
|
layout.addWidget(self.scene_label, 1, 2)
|
|
layout.addWidget(self.slide_label, 2, 2)
|
|
self.setLayout(layout)
|
|
self.setFixedWidth(150)
|
|
self.setFixedHeight(80)
|
|
|
|
if parent := self.parent():
|
|
self.closeEvent = parent.closeEvent
|
|
self.keyPressEvent = parent.keyPressEvent
|
|
|
|
|
|
class Player(QMainWindow): # type: ignore[misc]
|
|
presentation_changed: Signal = Signal()
|
|
slide_changed: Signal = Signal()
|
|
|
|
def __init__(
|
|
self,
|
|
config: Config,
|
|
presentation_configs: List[PresentationConfig],
|
|
*,
|
|
start_paused: bool = False,
|
|
full_screen: bool = False,
|
|
skip_all: bool = False,
|
|
exit_after_last_slide: bool = False,
|
|
hide_mouse: bool = False,
|
|
aspect_ratio_mode: Qt.AspectRatioMode = Qt.KeepAspectRatio,
|
|
presentation_index: int = 0,
|
|
slide_index: int = 0,
|
|
screen: Optional[QScreen] = None,
|
|
):
|
|
super().__init__()
|
|
|
|
# Wizard's config
|
|
|
|
self.config = config
|
|
|
|
# Presentation configs
|
|
|
|
self.presentation_configs = presentation_configs
|
|
self.__current_presentation_index = 0
|
|
self.__current_slide_index = 0
|
|
self.__current_file: Path = self.current_slide_config.file
|
|
|
|
self.current_presentation_index = presentation_index
|
|
self.current_slide_index = slide_index
|
|
|
|
self.__playing_reversed_slide = False
|
|
|
|
# Widgets
|
|
|
|
if screen:
|
|
self.setScreen(screen)
|
|
self.move(screen.geometry().topLeft())
|
|
|
|
if full_screen:
|
|
self.setWindowState(Qt.WindowFullScreen)
|
|
else:
|
|
w, h = self.current_presentation_config.resolution
|
|
geometry = self.geometry()
|
|
geometry.setWidth(w)
|
|
geometry.setHeight(h)
|
|
self.setGeometry(geometry)
|
|
|
|
if hide_mouse:
|
|
self.setCursor(Qt.BlankCursor)
|
|
|
|
self.setWindowTitle(WINDOW_NAME)
|
|
self.icon = QIcon(":/icon.png")
|
|
self.setWindowIcon(self.icon)
|
|
|
|
self.video_widget = QVideoWidget()
|
|
self.video_widget.setAspectRatioMode(aspect_ratio_mode)
|
|
self.setCentralWidget(self.video_widget)
|
|
|
|
self.media_player = QMediaPlayer(self)
|
|
self.media_player.setVideoOutput(self.video_widget)
|
|
|
|
self.presentation_changed.connect(self.presentation_changed_callback)
|
|
self.slide_changed.connect(self.slide_changed_callback)
|
|
|
|
self.info = Info(parent=self)
|
|
|
|
# Connecting key callbacks
|
|
|
|
self.config.keys.QUIT.connect(self.quit)
|
|
self.config.keys.PLAY_PAUSE.connect(self.play_pause)
|
|
self.config.keys.NEXT.connect(self.next)
|
|
self.config.keys.PREVIOUS.connect(self.previous)
|
|
self.config.keys.REVERSE.connect(self.reverse)
|
|
self.config.keys.REPLAY.connect(self.replay)
|
|
self.config.keys.FULL_SCREEN.connect(self.full_screen)
|
|
self.config.keys.HIDE_MOUSE.connect(self.hide_mouse)
|
|
|
|
self.dispatch = self.config.keys.dispatch_key_function()
|
|
|
|
# Misc
|
|
|
|
self.exit_after_last_slide = exit_after_last_slide
|
|
|
|
# Setting-up everything
|
|
|
|
if skip_all:
|
|
|
|
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
|
|
self.media_player.setLoops(1) # Otherwise looping slides never end
|
|
if status == QMediaPlayer.EndOfMedia:
|
|
self.load_next_slide()
|
|
|
|
self.media_player.mediaStatusChanged.connect(media_status_changed)
|
|
|
|
if self.current_slide_config.loop:
|
|
self.media_player.setLoops(-1)
|
|
|
|
self.load_current_media(start_paused=start_paused)
|
|
|
|
self.presentation_changed.emit()
|
|
self.slide_changed.emit()
|
|
|
|
"""
|
|
Properties
|
|
"""
|
|
|
|
@property
|
|
def presentations_count(self) -> int:
|
|
return len(self.presentation_configs)
|
|
|
|
@property
|
|
def current_presentation_index(self) -> int:
|
|
return self.__current_presentation_index
|
|
|
|
@current_presentation_index.setter
|
|
def current_presentation_index(self, index: int) -> None:
|
|
if 0 <= index < self.presentations_count:
|
|
self.__current_presentation_index = index
|
|
elif -self.presentations_count <= index < 0:
|
|
self.__current_presentation_index = index + self.presentations_count
|
|
else:
|
|
logger.warn(f"Could not set presentation index to {index}")
|
|
return
|
|
|
|
self.presentation_changed.emit()
|
|
|
|
@property
|
|
def current_presentation_config(self) -> PresentationConfig:
|
|
return self.presentation_configs[self.current_presentation_index]
|
|
|
|
@property
|
|
def current_slides_count(self) -> int:
|
|
return len(self.current_presentation_config.slides)
|
|
|
|
@property
|
|
def current_slide_index(self) -> int:
|
|
return self.__current_slide_index
|
|
|
|
@current_slide_index.setter
|
|
def current_slide_index(self, index: int) -> None:
|
|
if 0 <= index < self.current_slides_count:
|
|
self.__current_slide_index = index
|
|
elif -self.current_slides_count <= index < 0:
|
|
self.__current_slide_index = index + self.current_slides_count
|
|
else:
|
|
logger.warn(f"Could not set slide index to {index}")
|
|
return
|
|
|
|
self.slide_changed.emit()
|
|
|
|
@property
|
|
def current_slide_config(self) -> SlideConfig:
|
|
return self.current_presentation_config.slides[self.current_slide_index]
|
|
|
|
@property
|
|
def current_file(self) -> Path:
|
|
return self.__current_file
|
|
|
|
@current_file.setter
|
|
def current_file(self, file: Path) -> None:
|
|
self.__current_file = file
|
|
|
|
@property
|
|
def playing_reversed_slide(self) -> bool:
|
|
return self.__playing_reversed_slide
|
|
|
|
@playing_reversed_slide.setter
|
|
def playing_reversed_slide(self, playing_reversed_slide: bool) -> None:
|
|
self.__playing_reversed_slide = playing_reversed_slide
|
|
|
|
"""
|
|
Loading slides
|
|
"""
|
|
|
|
def load_current_media(self, start_paused: bool = False) -> None:
|
|
url = QUrl.fromLocalFile(self.current_file)
|
|
self.media_player.setSource(url)
|
|
|
|
if start_paused:
|
|
self.media_player.pause()
|
|
else:
|
|
self.media_player.play()
|
|
|
|
def load_current_slide(self) -> None:
|
|
slide_config = self.current_slide_config
|
|
self.current_file = slide_config.file
|
|
|
|
if slide_config.loop:
|
|
self.media_player.setLoops(-1)
|
|
else:
|
|
self.media_player.setLoops(1)
|
|
|
|
self.load_current_media()
|
|
|
|
def load_previous_slide(self) -> None:
|
|
self.playing_reversed_slide = False
|
|
|
|
if self.current_slide_index > 0:
|
|
self.current_slide_index -= 1
|
|
elif self.current_presentation_index > 0:
|
|
self.current_presentation_index -= 1
|
|
self.current_slide_index = self.current_slides_count - 1
|
|
else:
|
|
logger.info("No previous slide.")
|
|
return
|
|
|
|
self.load_current_slide()
|
|
|
|
def load_next_slide(self) -> None:
|
|
if self.playing_reversed_slide:
|
|
self.playing_reversed_slide = False
|
|
elif self.current_slide_index < self.current_slides_count - 1:
|
|
self.current_slide_index += 1
|
|
elif self.current_presentation_index < self.presentations_count - 1:
|
|
self.current_presentation_index += 1
|
|
self.current_slide_index = 0
|
|
elif self.exit_after_last_slide:
|
|
self.quit()
|
|
else:
|
|
logger.info("No more slide to play.")
|
|
return
|
|
|
|
self.load_current_slide()
|
|
|
|
def load_reversed_slide(self) -> None:
|
|
self.playing_reversed_slide = True
|
|
self.current_file = self.current_slide_config.rev_file
|
|
self.load_current_media()
|
|
|
|
"""
|
|
Key callbacks and slots
|
|
"""
|
|
|
|
@Slot()
|
|
def presentation_changed_callback(self) -> None:
|
|
index = self.current_presentation_index
|
|
count = self.presentations_count
|
|
self.info.scene_label.setText(f"{index+1:4d}/{count:4<d}")
|
|
|
|
@Slot()
|
|
def slide_changed_callback(self) -> None:
|
|
index = self.current_slide_index
|
|
count = self.current_slides_count
|
|
self.info.slide_label.setText(f"{index+1:4d}/{count:4<d}")
|
|
|
|
def show(self) -> None:
|
|
super().show()
|
|
self.info.show()
|
|
|
|
@Slot()
|
|
def quit(self) -> None:
|
|
logger.info("Closing gracefully...")
|
|
self.info.deleteLater()
|
|
self.deleteLater()
|
|
|
|
@Slot()
|
|
def next(self) -> None:
|
|
if self.media_player.playbackState() == QMediaPlayer.PausedState:
|
|
self.media_player.play()
|
|
else:
|
|
self.load_next_slide()
|
|
|
|
@Slot()
|
|
def previous(self) -> None:
|
|
self.load_previous_slide()
|
|
|
|
@Slot()
|
|
def reverse(self) -> None:
|
|
self.load_reversed_slide()
|
|
|
|
@Slot()
|
|
def replay(self) -> None:
|
|
self.media_player.setPosition(0)
|
|
self.media_player.play()
|
|
|
|
@Slot()
|
|
def play_pause(self) -> None:
|
|
state = self.media_player.playbackState()
|
|
if state == QMediaPlayer.PausedState:
|
|
self.media_player.play()
|
|
elif state == QMediaPlayer.PlayingState:
|
|
self.media_player.pause()
|
|
|
|
@Slot()
|
|
def full_screen(self) -> None:
|
|
if self.windowState() == Qt.WindowFullScreen:
|
|
self.setWindowState(Qt.WindowNoState)
|
|
else:
|
|
self.setWindowState(Qt.WindowFullScreen)
|
|
|
|
@Slot()
|
|
def hide_mouse(self) -> None:
|
|
if self.cursor().shape() == Qt.BlankCursor:
|
|
self.setCursor(Qt.ArrowCursor)
|
|
else:
|
|
self.setCursor(Qt.BlankCursor)
|
|
|
|
def closeEvent(self, event: QCloseEvent) -> None:
|
|
self.quit()
|
|
|
|
def keyPressEvent(self, event: QKeyEvent) -> None:
|
|
key = event.key()
|
|
self.dispatch(key)
|
|
event.accept()
|