1
+ from datetime import datetime
1
2
from pathlib import Path
2
- from typing import Any , List , Optional
3
+ from typing import List , Optional
3
4
4
- from PySide6 .QtCore import Qt , QUrl , Signal , Slot
5
+ from PySide6 .QtCore import Qt , QTimer , QUrl , Signal , Slot
5
6
from PySide6 .QtGui import QCloseEvent , QIcon , QKeyEvent , QScreen
6
7
from PySide6 .QtMultimedia import QMediaPlayer
7
8
from 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
+ )
9
16
10
17
from ..config import Config , PresentationConfig , SlideConfig
11
18
from ..logger import logger
14
21
WINDOW_NAME = "Manim Slides"
15
22
16
23
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
20
60
21
- main_layout = QVBoxLayout ()
22
- labels_layout = QGridLayout ()
23
- notes_layout = QVBoxLayout ()
24
61
self .scene_label = QLabel ()
25
62
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 ()
27
127
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 )
28
135
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 ()
33
137
34
- notes_layout . addWidget ( self . slide_notes )
138
+ widget . setLayout ( layout )
35
139
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 )
38
142
39
143
self .setLayout (main_layout )
40
144
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 )
44
163
45
164
46
165
class Player (QMainWindow ): # type: ignore[misc]
@@ -107,6 +226,7 @@ def __init__(
107
226
self .setWindowIcon (self .icon )
108
227
109
228
self .video_widget = QVideoWidget ()
229
+ self .video_sink = self .video_widget .videoSink ()
110
230
self .video_widget .setAspectRatioMode (aspect_ratio_mode )
111
231
self .setCentralWidget (self .video_widget )
112
232
@@ -117,7 +237,14 @@ def __init__(
117
237
self .presentation_changed .connect (self .presentation_changed_callback )
118
238
self .slide_changed .connect (self .slide_changed_callback )
119
239
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
+ )
121
248
self .hide_info_window = hide_info_window
122
249
123
250
# Connecting key callbacks
@@ -228,6 +355,28 @@ def current_file(self) -> Path:
228
355
def current_file (self , file : Path ) -> None :
229
356
self .__current_file = file
230
357
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
+
231
380
@property
232
381
def playing_reversed_slide (self ) -> bool :
233
382
return self .__playing_reversed_slide
@@ -286,6 +435,7 @@ def load_previous_slide(self) -> None:
286
435
def load_next_slide (self ) -> None :
287
436
if self .playing_reversed_slide :
288
437
self .playing_reversed_slide = False
438
+ self .preview_next_slide () # Slide number did not change, but next did
289
439
elif self .current_slide_index < self .current_slides_count - 1 :
290
440
self .current_slide_index += 1
291
441
elif self .current_presentation_index < self .presentations_count - 1 :
@@ -321,6 +471,13 @@ def slide_changed_callback(self) -> None:
321
471
count = self .current_slides_count
322
472
self .info .slide_label .setText (f"{ index + 1 :4d} /{ count :4<d} " )
323
473
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 ()
324
481
325
482
def show (self ) -> None :
326
483
super ().show ()
@@ -331,6 +488,7 @@ def show(self) -> None:
331
488
@Slot ()
332
489
def close (self ) -> None :
333
490
logger .info ("Closing gracefully..." )
491
+ self .info .close ()
334
492
super ().close ()
335
493
336
494
@Slot ()
@@ -353,6 +511,7 @@ def previous(self) -> None:
353
511
@Slot ()
354
512
def reverse (self ) -> None :
355
513
self .load_reversed_slide ()
514
+ self .preview_next_slide ()
356
515
357
516
@Slot ()
358
517
def replay (self ) -> None :
@@ -381,9 +540,11 @@ def hide_mouse(self) -> None:
381
540
else :
382
541
self .setCursor (Qt .BlankCursor )
383
542
543
+ @Slot ()
384
544
def closeEvent (self , event : QCloseEvent ) -> None : # noqa: N802
385
545
self .close ()
386
546
547
+ @Slot ()
387
548
def keyPressEvent (self , event : QKeyEvent ) -> None : # noqa: N802
388
549
key = event .key ()
389
550
self .dispatch (key )
0 commit comments