1+ from datetime import datetime
12from pathlib import Path
2- from typing import Any , List , Optional
3+ from typing import List , Optional
34
4- from PySide6 .QtCore import Qt , QUrl , Signal , Slot
5+ from PySide6 .QtCore import Qt , QTimer , QUrl , Signal , Slot
56from PySide6 .QtGui import QCloseEvent , QIcon , QKeyEvent , QScreen
67from PySide6 .QtMultimedia import QMediaPlayer
78from PySide6 .QtMultimediaWidgets import QVideoWidget
8- from PySide6 .QtWidgets import QDialog , QGridLayout , QLabel , QMainWindow , QVBoxLayout
9+ from PySide6 .QtWidgets import (
10+ QHBoxLayout ,
11+ QLabel ,
12+ QMainWindow ,
13+ QVBoxLayout ,
14+ QWidget ,
15+ )
916
1017from ..config import Config , PresentationConfig , SlideConfig
1118from ..logger import logger
1421WINDOW_NAME = "Manim Slides"
1522
1623
17- class Info (QDialog ): # type: ignore[misc]
18- def __init__ (self , * args : Any , ** kwargs : Any ) -> None :
19- super ().__init__ (* args , ** kwargs )
24+ class Info (QWidget ): # type: ignore[misc]
25+ key_press_event : Signal = Signal (QKeyEvent )
26+ close_event : Signal = Signal (QCloseEvent )
27+
28+ def __init__ (
29+ self ,
30+ * ,
31+ full_screen : bool ,
32+ aspect_ratio_mode : Qt .AspectRatioMode ,
33+ screen : Optional [QScreen ],
34+ ) -> None :
35+ super ().__init__ ()
36+
37+ if screen :
38+ self .setScreen (screen )
39+ self .move (screen .geometry ().topLeft ())
40+
41+ if full_screen :
42+ self .setWindowState (Qt .WindowFullScreen )
43+
44+ layout = QHBoxLayout ()
45+
46+ # Current slide view
47+
48+ left_layout = QVBoxLayout ()
49+ left_layout .addWidget (
50+ QLabel ("Current slide" ),
51+ alignment = Qt .AlignmentFlag .AlignBottom | Qt .AlignmentFlag .AlignHCenter ,
52+ )
53+ main_video_widget = QVideoWidget ()
54+ main_video_widget .setAspectRatioMode (aspect_ratio_mode )
55+ main_video_widget .setFixedSize (720 , 480 )
56+ self .video_sink = main_video_widget .videoSink ()
57+ left_layout .addWidget (main_video_widget )
58+
59+ # Current slide informations
2060
21- main_layout = QVBoxLayout ()
22- labels_layout = QGridLayout ()
23- notes_layout = QVBoxLayout ()
2461 self .scene_label = QLabel ()
2562 self .slide_label = QLabel ()
26- self .slide_notes = QLabel ("" )
63+ self .start_time = datetime .now ()
64+ self .time_label = QLabel ()
65+ self .elapsed_label = QLabel ("00h00m00s" )
66+ self .timer = QTimer ()
67+ self .timer .start (1000 ) # every second
68+ self .timer .timeout .connect (self .update_time )
69+
70+ bottom_left_layout = QHBoxLayout ()
71+ bottom_left_layout .addWidget (
72+ QLabel ("Scene:" ),
73+ alignment = Qt .AlignmentFlag .AlignTop | Qt .AlignmentFlag .AlignRight ,
74+ )
75+ bottom_left_layout .addWidget (
76+ self .scene_label ,
77+ alignment = Qt .AlignmentFlag .AlignTop | Qt .AlignmentFlag .AlignLeft ,
78+ )
79+ bottom_left_layout .addWidget (
80+ QLabel ("Slide:" ),
81+ alignment = Qt .AlignmentFlag .AlignTop | Qt .AlignmentFlag .AlignRight ,
82+ )
83+ bottom_left_layout .addWidget (
84+ self .slide_label ,
85+ alignment = Qt .AlignmentFlag .AlignTop | Qt .AlignmentFlag .AlignLeft ,
86+ )
87+ bottom_left_layout .addWidget (
88+ QLabel ("Time:" ),
89+ alignment = Qt .AlignmentFlag .AlignTop | Qt .AlignmentFlag .AlignRight ,
90+ )
91+ bottom_left_layout .addWidget (
92+ self .time_label ,
93+ alignment = Qt .AlignmentFlag .AlignTop | Qt .AlignmentFlag .AlignLeft ,
94+ )
95+ bottom_left_layout .addWidget (
96+ QLabel ("Elapsed:" ),
97+ alignment = Qt .AlignmentFlag .AlignTop | Qt .AlignmentFlag .AlignRight ,
98+ )
99+ bottom_left_layout .addWidget (
100+ self .elapsed_label ,
101+ alignment = Qt .AlignmentFlag .AlignTop | Qt .AlignmentFlag .AlignLeft ,
102+ )
103+ left_layout .addLayout (bottom_left_layout )
104+ layout .addLayout (left_layout )
105+
106+ layout .addSpacing (20 )
107+
108+ # Next slide preview
109+
110+ right_layout = QVBoxLayout ()
111+ right_layout .addWidget (
112+ QLabel ("Next slide" ),
113+ alignment = Qt .AlignmentFlag .AlignBottom | Qt .AlignmentFlag .AlignHCenter ,
114+ )
115+ next_video_widget = QVideoWidget ()
116+ next_video_widget .setAspectRatioMode (aspect_ratio_mode )
117+ next_video_widget .setFixedSize (360 , 240 )
118+ self .next_media_player = QMediaPlayer ()
119+ self .next_media_player .setVideoOutput (next_video_widget )
120+ self .next_media_player .setLoops (- 1 )
121+
122+ right_layout .addWidget (next_video_widget )
123+
124+ # Notes
125+
126+ self .slide_notes = QLabel ()
27127 self .slide_notes .setWordWrap (True )
128+ self .slide_notes .setTextFormat (Qt .TextFormat .MarkdownText )
129+ self .slide_notes .setFixedWidth (360 )
130+ right_layout .addWidget (
131+ self .slide_notes ,
132+ alignment = Qt .AlignmentFlag .AlignTop | Qt .AlignmentFlag .AlignLeft ,
133+ )
134+ layout .addLayout (right_layout )
28135
29- labels_layout .addWidget (QLabel ("Scene:" ), 1 , 1 )
30- labels_layout .addWidget (QLabel ("Slide:" ), 2 , 1 )
31- labels_layout .addWidget (self .scene_label , 1 , 2 )
32- labels_layout .addWidget (self .slide_label , 2 , 2 )
136+ widget = QWidget ()
33137
34- notes_layout . addWidget ( self . slide_notes )
138+ widget . setLayout ( layout )
35139
36- main_layout . addLayout ( labels_layout )
37- main_layout .addLayout ( notes_layout )
140+ main_layout = QVBoxLayout ( )
141+ main_layout .addWidget ( widget , alignment = Qt . AlignmentFlag . AlignCenter )
38142
39143 self .setLayout (main_layout )
40144
41- if parent := self .parent ():
42- self .closeEvent = parent .closeEvent
43- self .keyPressEvent = parent .keyPressEvent
145+ @Slot ()
146+ def update_time (self ) -> None :
147+ now = datetime .now ()
148+ seconds = (now - self .start_time ).total_seconds ()
149+ hours , seconds = divmod (seconds , 3600 )
150+ minutes , seconds = divmod (seconds , 60 )
151+ self .time_label .setText (now .strftime ("%Y/%m/%d %H:%M:%S" ))
152+ self .elapsed_label .setText (
153+ f"{ int (hours ):02d} h{ int (minutes ):02d} m{ int (seconds ):02d} s"
154+ )
155+
156+ @Slot ()
157+ def closeEvent (self , event : QCloseEvent ) -> None : # noqa: N802
158+ self .close_event .emit (event )
159+
160+ @Slot ()
161+ def keyPressEvent (self , event : QKeyEvent ) -> None : # noqa: N802
162+ self .key_press_event .emit (event )
44163
45164
46165class Player (QMainWindow ): # type: ignore[misc]
@@ -107,6 +226,7 @@ def __init__(
107226 self .setWindowIcon (self .icon )
108227
109228 self .video_widget = QVideoWidget ()
229+ self .video_sink = self .video_widget .videoSink ()
110230 self .video_widget .setAspectRatioMode (aspect_ratio_mode )
111231 self .setCentralWidget (self .video_widget )
112232
@@ -117,7 +237,14 @@ def __init__(
117237 self .presentation_changed .connect (self .presentation_changed_callback )
118238 self .slide_changed .connect (self .slide_changed_callback )
119239
120- self .info = Info (parent = self )
240+ self .info = Info (
241+ full_screen = full_screen , aspect_ratio_mode = aspect_ratio_mode , screen = screen
242+ )
243+ self .info .close_event .connect (self .closeEvent )
244+ self .info .key_press_event .connect (self .keyPressEvent )
245+ self .video_sink .videoFrameChanged .connect (
246+ lambda frame : self .info .video_sink .setVideoFrame (frame )
247+ )
121248 self .hide_info_window = hide_info_window
122249
123250 # Connecting key callbacks
@@ -228,6 +355,28 @@ def current_file(self) -> Path:
228355 def current_file (self , file : Path ) -> None :
229356 self .__current_file = file
230357
358+ @property
359+ def next_slide_config (self ) -> Optional [SlideConfig ]:
360+ if self .playing_reversed_slide :
361+ return self .current_slide_config
362+ elif self .current_slide_index < self .current_slides_count - 1 :
363+ return self .presentation_configs [self .current_presentation_index ].slides [
364+ self .current_slide_index + 1
365+ ]
366+ elif self .current_presentation_index < self .presentations_count - 1 :
367+ return self .presentation_configs [
368+ self .current_presentation_index + 1
369+ ].slides [0 ]
370+ else :
371+ return None
372+
373+ @property
374+ def next_file (self ) -> Optional [Path ]:
375+ if slide_config := self .next_slide_config :
376+ return slide_config .file # type: ignore[no-any-return]
377+
378+ return None
379+
231380 @property
232381 def playing_reversed_slide (self ) -> bool :
233382 return self .__playing_reversed_slide
@@ -286,6 +435,7 @@ def load_previous_slide(self) -> None:
286435 def load_next_slide (self ) -> None :
287436 if self .playing_reversed_slide :
288437 self .playing_reversed_slide = False
438+ self .preview_next_slide () # Slide number did not change, but next did
289439 elif self .current_slide_index < self .current_slides_count - 1 :
290440 self .current_slide_index += 1
291441 elif self .current_presentation_index < self .presentations_count - 1 :
@@ -321,6 +471,13 @@ def slide_changed_callback(self) -> None:
321471 count = self .current_slides_count
322472 self .info .slide_label .setText (f"{ index + 1 :4d} /{ count :4<d} " )
323473 self .info .slide_notes .setText (self .current_slide_config .notes )
474+ self .preview_next_slide ()
475+
476+ def preview_next_slide (self ) -> None :
477+ if slide_config := self .next_slide_config :
478+ url = QUrl .fromLocalFile (slide_config .file )
479+ self .info .next_media_player .setSource (url )
480+ self .info .next_media_player .play ()
324481
325482 def show (self ) -> None :
326483 super ().show ()
@@ -331,6 +488,7 @@ def show(self) -> None:
331488 @Slot ()
332489 def close (self ) -> None :
333490 logger .info ("Closing gracefully..." )
491+ self .info .close ()
334492 super ().close ()
335493
336494 @Slot ()
@@ -353,6 +511,7 @@ def previous(self) -> None:
353511 @Slot ()
354512 def reverse (self ) -> None :
355513 self .load_reversed_slide ()
514+ self .preview_next_slide ()
356515
357516 @Slot ()
358517 def replay (self ) -> None :
@@ -381,9 +540,11 @@ def hide_mouse(self) -> None:
381540 else :
382541 self .setCursor (Qt .BlankCursor )
383542
543+ @Slot ()
384544 def closeEvent (self , event : QCloseEvent ) -> None : # noqa: N802
385545 self .close ()
386546
547+ @Slot ()
387548 def keyPressEvent (self , event : QKeyEvent ) -> None : # noqa: N802
388549 key = event .key ()
389550 self .dispatch (key )
0 commit comments