Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lib): enhance notes support #324

Merged
merged 6 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions manim_slides/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from functools import wraps
from inspect import Parameter, signature
from pathlib import Path
from textwrap import dedent
from typing import Any, Callable, Dict, List, Optional, Set, Tuple

import rtoml
Expand Down Expand Up @@ -145,6 +146,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
playback_rate: float = 1.0
reversed_playback_rate: float = 1.0
notes: str = ""
dedent_notes: bool = True

@classmethod
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
Expand Down Expand Up @@ -188,6 +190,16 @@ def __wrapper__(*args: Any, **kwargs: Any) -> Any: # noqa: N807

return _wrapper_

@model_validator(mode="after")
@classmethod
def apply_dedent_notes(
cls, base_slide_config: "BaseSlideConfig"
) -> "BaseSlideConfig":
if base_slide_config.dedent_notes:
base_slide_config.notes = dedent(base_slide_config.notes)

return base_slide_config


class PreSlideConfig(BaseSlideConfig):
"""Slide config to be used prior to rendering."""
Expand Down
5 changes: 4 additions & 1 deletion manim_slides/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@
def open(self, file: Path) -> None:
return open_with_default(file)

def convert_to(self, dest: Path) -> None:
def convert_to(self, dest: Path) -> None: # noqa: C901
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
prs = pptx.Presentation()
prs.slide_width = self.width * 9525
Expand Down Expand Up @@ -557,6 +557,9 @@
poster_frame_image=poster_frame_image,
mime_type=mime_type,
)
if slide_config.notes != "":
slide.notes_slide.notes_text_frame.text = slide_config.notes

Check warning on line 561 in manim_slides/convert.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/convert.py#L561

Added line #L561 was not covered by tests

if self.auto_play_media:
auto_play_media(movie, loop=slide_config.loop)

Expand Down
203 changes: 182 additions & 21 deletions manim_slides/present/player.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
from datetime import datetime
from pathlib import Path
from typing import Any, List, Optional
from typing import List, Optional

from PySide6.QtCore import Qt, QUrl, Signal, Slot
from PySide6.QtCore import Qt, QTimer, 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, QVBoxLayout
from PySide6.QtWidgets import (
QHBoxLayout,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)

from ..config import Config, PresentationConfig, SlideConfig
from ..logger import logger
Expand All @@ -14,33 +21,145 @@
WINDOW_NAME = "Manim Slides"


class Info(QDialog): # type: ignore[misc]
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
class Info(QWidget): # type: ignore[misc]
key_press_event: Signal = Signal(QKeyEvent)
close_event: Signal = Signal(QCloseEvent)

def __init__(
self,
*,
full_screen: bool,
aspect_ratio_mode: Qt.AspectRatioMode,
screen: Optional[QScreen],
) -> None:
super().__init__()

if screen:
self.setScreen(screen)
self.move(screen.geometry().topLeft())

if full_screen:
self.setWindowState(Qt.WindowFullScreen)

layout = QHBoxLayout()

# Current slide view

left_layout = QVBoxLayout()
left_layout.addWidget(
QLabel("Current slide"),
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
)
main_video_widget = QVideoWidget()
main_video_widget.setAspectRatioMode(aspect_ratio_mode)
main_video_widget.setFixedSize(720, 480)
self.video_sink = main_video_widget.videoSink()
left_layout.addWidget(main_video_widget)

# Current slide informations

main_layout = QVBoxLayout()
labels_layout = QGridLayout()
notes_layout = QVBoxLayout()
self.scene_label = QLabel()
self.slide_label = QLabel()
self.slide_notes = QLabel("")
self.start_time = datetime.now()
self.time_label = QLabel()
self.elapsed_label = QLabel("00h00m00s")
self.timer = QTimer()
self.timer.start(1000) # every second
self.timer.timeout.connect(self.update_time)

bottom_left_layout = QHBoxLayout()
bottom_left_layout.addWidget(
QLabel("Scene:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.scene_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
bottom_left_layout.addWidget(
QLabel("Slide:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.slide_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
bottom_left_layout.addWidget(
QLabel("Time:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.time_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
bottom_left_layout.addWidget(
QLabel("Elapsed:"),
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
)
bottom_left_layout.addWidget(
self.elapsed_label,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
left_layout.addLayout(bottom_left_layout)
layout.addLayout(left_layout)

layout.addSpacing(20)

# Next slide preview

right_layout = QVBoxLayout()
right_layout.addWidget(
QLabel("Next slide"),
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
)
next_video_widget = QVideoWidget()
next_video_widget.setAspectRatioMode(aspect_ratio_mode)
next_video_widget.setFixedSize(360, 240)
self.next_media_player = QMediaPlayer()
self.next_media_player.setVideoOutput(next_video_widget)
self.next_media_player.setLoops(-1)

right_layout.addWidget(next_video_widget)

# Notes

self.slide_notes = QLabel()
self.slide_notes.setWordWrap(True)
self.slide_notes.setTextFormat(Qt.TextFormat.MarkdownText)
self.slide_notes.setFixedWidth(360)
right_layout.addWidget(
self.slide_notes,
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
)
layout.addLayout(right_layout)

labels_layout.addWidget(QLabel("Scene:"), 1, 1)
labels_layout.addWidget(QLabel("Slide:"), 2, 1)
labels_layout.addWidget(self.scene_label, 1, 2)
labels_layout.addWidget(self.slide_label, 2, 2)
widget = QWidget()

notes_layout.addWidget(self.slide_notes)
widget.setLayout(layout)

main_layout.addLayout(labels_layout)
main_layout.addLayout(notes_layout)
main_layout = QVBoxLayout()
main_layout.addWidget(widget, alignment=Qt.AlignmentFlag.AlignCenter)

self.setLayout(main_layout)

if parent := self.parent():
self.closeEvent = parent.closeEvent
self.keyPressEvent = parent.keyPressEvent
@Slot()
def update_time(self) -> None:
now = datetime.now()
seconds = (now - self.start_time).total_seconds()
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
self.time_label.setText(now.strftime("%Y/%m/%d %H:%M:%S"))
self.elapsed_label.setText(

Check warning on line 152 in manim_slides/present/player.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/present/player.py#L147-L152

Added lines #L147 - L152 were not covered by tests
f"{int(hours):02d}h{int(minutes):02d}m{int(seconds):02d}s"
)

@Slot()
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
self.close_event.emit(event)

@Slot()
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
self.key_press_event.emit(event)

Check warning on line 162 in manim_slides/present/player.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/present/player.py#L162

Added line #L162 was not covered by tests


class Player(QMainWindow): # type: ignore[misc]
Expand Down Expand Up @@ -107,6 +226,7 @@
self.setWindowIcon(self.icon)

self.video_widget = QVideoWidget()
self.video_sink = self.video_widget.videoSink()
self.video_widget.setAspectRatioMode(aspect_ratio_mode)
self.setCentralWidget(self.video_widget)

Expand All @@ -117,7 +237,14 @@
self.presentation_changed.connect(self.presentation_changed_callback)
self.slide_changed.connect(self.slide_changed_callback)

self.info = Info(parent=self)
self.info = Info(
full_screen=full_screen, aspect_ratio_mode=aspect_ratio_mode, screen=screen
)
self.info.close_event.connect(self.closeEvent)
self.info.key_press_event.connect(self.keyPressEvent)
self.video_sink.videoFrameChanged.connect(
lambda frame: self.info.video_sink.setVideoFrame(frame)
)
self.hide_info_window = hide_info_window

# Connecting key callbacks
Expand Down Expand Up @@ -228,6 +355,28 @@
def current_file(self, file: Path) -> None:
self.__current_file = file

@property
def next_slide_config(self) -> Optional[SlideConfig]:
if self.playing_reversed_slide:
return self.current_slide_config

Check warning on line 361 in manim_slides/present/player.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/present/player.py#L361

Added line #L361 was not covered by tests
elif self.current_slide_index < self.current_slides_count - 1:
return self.presentation_configs[self.current_presentation_index].slides[
self.current_slide_index + 1
]
elif self.current_presentation_index < self.presentations_count - 1:
return self.presentation_configs[

Check warning on line 367 in manim_slides/present/player.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/present/player.py#L367

Added line #L367 was not covered by tests
self.current_presentation_index + 1
].slides[0]
else:
return None

@property
def next_file(self) -> Optional[Path]:
if slide_config := self.next_slide_config:
return slide_config.file # type: ignore[no-any-return]

Check warning on line 376 in manim_slides/present/player.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/present/player.py#L375-L376

Added lines #L375 - L376 were not covered by tests

return None

Check warning on line 378 in manim_slides/present/player.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/present/player.py#L378

Added line #L378 was not covered by tests

@property
def playing_reversed_slide(self) -> bool:
return self.__playing_reversed_slide
Expand Down Expand Up @@ -286,6 +435,7 @@
def load_next_slide(self) -> None:
if self.playing_reversed_slide:
self.playing_reversed_slide = False
self.preview_next_slide() # Slide number did not change, but next did

Check warning on line 438 in manim_slides/present/player.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/present/player.py#L438

Added line #L438 was not covered by tests
elif self.current_slide_index < self.current_slides_count - 1:
self.current_slide_index += 1
elif self.current_presentation_index < self.presentations_count - 1:
Expand Down Expand Up @@ -321,6 +471,13 @@
count = self.current_slides_count
self.info.slide_label.setText(f"{index+1:4d}/{count:4<d}")
self.info.slide_notes.setText(self.current_slide_config.notes)
self.preview_next_slide()

def preview_next_slide(self) -> None:
if slide_config := self.next_slide_config:
url = QUrl.fromLocalFile(slide_config.file)
self.info.next_media_player.setSource(url)
self.info.next_media_player.play()

def show(self) -> None:
super().show()
Expand All @@ -331,6 +488,7 @@
@Slot()
def close(self) -> None:
logger.info("Closing gracefully...")
self.info.close()
super().close()

@Slot()
Expand All @@ -353,6 +511,7 @@
@Slot()
def reverse(self) -> None:
self.load_reversed_slide()
self.preview_next_slide()

Check warning on line 514 in manim_slides/present/player.py

View check run for this annotation

Codecov / codecov/patch

manim_slides/present/player.py#L514

Added line #L514 was not covered by tests

@Slot()
def replay(self) -> None:
Expand Down Expand Up @@ -381,9 +540,11 @@
else:
self.setCursor(Qt.BlankCursor)

@Slot()
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
self.close()

@Slot()
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
key = event.key()
self.dispatch(key)
Expand Down
2 changes: 1 addition & 1 deletion manim_slides/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def render(ce: bool, gl: bool, args: Tuple[str, ...]) -> None:
"""
Render SCENE(s) from the input FILE, using the specified renderer.
Use 'manim-slides render --help' to see help information for
Use ``manim-slides render --help`` to see help information for
a the specified renderer.
"""
if ce and gl:
Expand Down
8 changes: 6 additions & 2 deletions manim_slides/slide/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,14 @@ def next_slide(
Note that this is only supported by ``manim-slides present``.
:param notes:
Presenter notes, in HTML format.
Presenter notes, in Markdown format.
Note that PowerPoint does not support Markdown.
Note that this is only supported by ``manim-slides present``
and ``manim-slides convert --to=html``.
and ``manim-slides convert --to=html/pptx``.
:param dedent_notes:
If set, apply :func:`textwrap.dedent` to notes.
:param kwargs:
Keyword arguments to be passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
Expand Down
5 changes: 3 additions & 2 deletions manim_slides/templates/revealjs.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
{%- endif -%}>
{% if slide_config.notes != "" -%}
<aside class="notes">{{ slide_config.notes }}</aside>
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
{%- endif %}
</section>
{%- endfor -%}
Expand All @@ -54,14 +54,15 @@
<!-- To include plugins, see: https://revealjs.com/plugins/ -->

{% if has_notes -%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
{%- endif -%}

<!-- <script src="index.js"></script> -->
<script>
Reveal.initialize({
{% if has_notes -%}
plugins: [ RevealNotes ],
plugins: [ RevealMarkdown, RevealNotes ],
{%- endif %}
// The "normal" size of the presentation, aspect ratio will
// be preserved when the presentation is scaled to fit different
Expand Down
Loading