mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-05-20 20:16:30 +08:00
feat(cli): rewrite presentation player (#243)
* 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
This commit is contained in:
346
manim_slides/present/player.py
Normal file
346
manim_slides/present/player.py
Normal file
@ -0,0 +1,346 @@
|
||||
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()
|
Reference in New Issue
Block a user