|
28 | 28 | """
|
29 | 29 |
|
30 | 30 | import os
|
31 |
| -import sys |
32 | 31 | import platform
|
33 | 32 | import shutil
|
| 33 | +import sys |
34 | 34 | import webbrowser
|
| 35 | +from copy import deepcopy |
35 | 36 | from time import sleep
|
36 | 37 | from uuid import uuid4
|
37 |
| -from copy import deepcopy |
38 | 38 |
|
| 39 | +import openshot # Python module for libopenshot (required video editing module installed separately) |
39 | 40 | from PyQt5.QtCore import (
|
40 | 41 | Qt, pyqtSignal, QCoreApplication, PYQT_VERSION_STR,
|
41 | 42 | QTimer, QDateTime, QFileInfo, QUrl,
|
42 | 43 | )
|
43 |
| -from PyQt5.QtGui import QIcon, QCursor, QKeySequence |
| 44 | +from PyQt5.QtGui import QIcon, QCursor, QKeySequence, QTextCursor |
44 | 45 | from PyQt5.QtWidgets import (
|
45 | 46 | QMainWindow, QWidget, QDockWidget,
|
46 | 47 | QMessageBox, QDialog, QFileDialog, QInputDialog,
|
47 | 48 | QAction, QActionGroup, QSizePolicy,
|
48 | 49 | QStatusBar, QToolBar, QToolButton,
|
49 |
| - QLineEdit, QSlider, QLabel, QComboBox, |
50 |
| - ) |
51 |
| -import openshot # Python module for libopenshot (required video editing module installed separately) |
| 50 | + QLineEdit, QSlider, QLabel, QComboBox, QTextEdit |
| 51 | +) |
52 | 52 |
|
53 |
| -from windows.views.timeline_webview import TimelineWebView |
54 | 53 | from classes import info, ui_util, settings, qt_types, updates
|
55 |
| -from classes import openshot_rc # noqa |
56 | 54 | from classes.app import get_app
|
| 55 | +from classes.conversion import zoomToSeconds, secondsToZoom |
| 56 | +from classes.exporters.edl import export_edl |
| 57 | +from classes.exporters.final_cut_pro import export_xml |
| 58 | +from classes.importers.edl import import_edl |
| 59 | +from classes.importers.final_cut_pro import import_xml |
57 | 60 | from classes.logger import log
|
58 |
| -from classes.timeline import TimelineSync |
59 |
| -from classes.query import Clip, Transition, Marker, Track |
60 | 61 | from classes.metrics import (
|
61 | 62 | track_metric_session, track_metric_screen,
|
62 | 63 | track_metric_error, track_exception_stacktrace,
|
63 | 64 | )
|
64 |
| -from classes.version import get_current_Version |
65 |
| -from classes.conversion import zoomToSeconds, secondsToZoom |
| 65 | +from classes.query import Clip, Transition, Marker, Track |
66 | 66 | from classes.thumbnail import httpThumbnailServerThread
|
| 67 | +from classes.time_parts import secondsToTimecode |
| 68 | +from classes.timeline import TimelineSync |
| 69 | +from classes.version import get_current_Version |
| 70 | +from windows.models.effects_model import EffectsModel |
| 71 | +from windows.models.emoji_model import EmojisModel |
67 | 72 | from windows.models.files_model import FilesModel
|
68 |
| -from windows.views.files_treeview import FilesTreeView |
69 |
| -from windows.views.files_listview import FilesListView |
70 | 73 | from windows.models.transition_model import TransitionsModel
|
71 |
| -from windows.views.transitions_treeview import TransitionsTreeView |
72 |
| -from windows.views.transitions_listview import TransitionsListView |
73 |
| -from windows.models.emoji_model import EmojisModel |
74 |
| -from windows.views.emojis_listview import EmojisListView |
75 |
| -from windows.models.effects_model import EffectsModel |
76 |
| -from windows.views.effects_treeview import EffectsTreeView |
| 74 | +from windows.preview_thread import PreviewParent |
| 75 | +from windows.video_widget import VideoWidget |
77 | 76 | from windows.views.effects_listview import EffectsListView
|
| 77 | +from windows.views.effects_treeview import EffectsTreeView |
| 78 | +from windows.views.emojis_listview import EmojisListView |
| 79 | +from windows.views.files_listview import FilesListView |
| 80 | +from windows.views.files_treeview import FilesTreeView |
78 | 81 | from windows.views.properties_tableview import PropertiesTableView, SelectionLabel
|
| 82 | +from windows.views.timeline_webview import TimelineWebView |
| 83 | +from windows.views.transitions_listview import TransitionsListView |
| 84 | +from windows.views.transitions_treeview import TransitionsTreeView |
79 | 85 | from windows.views.tutorial import TutorialManager
|
80 |
| -from windows.video_widget import VideoWidget |
81 |
| -from windows.preview_thread import PreviewParent |
82 |
| -from classes.exporters.edl import export_edl |
83 |
| -from classes.exporters.final_cut_pro import export_xml |
84 |
| -from classes.importers.edl import import_edl |
85 |
| -from classes.importers.final_cut_pro import import_xml |
86 | 86 |
|
87 | 87 |
|
88 | 88 | class MainWindow(QMainWindow, updates.UpdateWatcher):
|
@@ -113,6 +113,8 @@ class MainWindow(QMainWindow, updates.UpdateWatcher):
|
113 | 113 | OpenProjectSignal = pyqtSignal(str)
|
114 | 114 | ThumbnailUpdated = pyqtSignal(str)
|
115 | 115 | FileUpdated = pyqtSignal(str)
|
| 116 | + CaptionTextUpdated = pyqtSignal(str, object) |
| 117 | + CaptionTextLoaded = pyqtSignal(str, object) |
116 | 118 |
|
117 | 119 | # Docks are closable, movable and floatable
|
118 | 120 | docks_frozen = False
|
@@ -2169,21 +2171,20 @@ def actionSimple_View_trigger(self):
|
2169 | 2171 |
|
2170 | 2172 | # Set initial size of docks
|
2171 | 2173 | simple_state = "".join([
|
2172 |
| - "AAAA/wAAAAD9AAAAAwAAAAAAAAEnAAAC3/wCAAAAAvwAAAJeAAAApwAAAAAA////", |
2173 |
| - "+gAAAAACAAAAAfsAAAAYAGQAbwBjAGsASwBlAHkAZgByAGEAbQBlAAAAAAD/////", |
2174 |
| - "AAAAAAAAAAD7AAAAHABkAG8AYwBrAFAAcgBvAHAAZQByAHQAaQBlAHMAAAAAJwAAAt8AAACfAP///", |
2175 |
| - "wAAAAEAAAEcAAABQPwCAAAAAfsAAAAYAGQAbwBjAGsASwBlAHkAZgByAGEAbQBlAQAAAVg", |
2176 |
| - "AAAAVAAAAAAAAAAAAAAACAAAFEgAAAvP8AQAAAAH8AAAAAAAABRIAAAD6AP////", |
2177 |
| - "wCAAAAAvwAAAAnAAAB0QAAAK0A/////AEAAAAC/AAAAAAAAAHaAAAAewD////", |
2178 |
| - "6AAAAAAIAAAAE+wAAABIAZABvAGMAawBGAGkAbABlAHMBAAAAAP////8AAACRAP////", |
2179 |
| - "sAAAAeAGQAbwBjAGsAVAByAGEAbgBzAGkAdABpAG8AbgBzAQAAAAD/////AAAAkQD////", |
2180 |
| - "7AAAAFgBkAG8AYwBrAEUAZgBmAGUAYwB0AHMBAAAAAP////8AAACRAP////", |
2181 |
| - "sAAAAUAGQAbwBjAGsARQBtAG8AagBpAHMBAAAAJwAAAdEAAACRAP////", |
2182 |
| - "sAAAASAGQAbwBjAGsAVgBpAGQAZQBvAQAAAeAAAAMyAAAARwD////", |
2183 |
| - "7AAAAGABkAG8AYwBrAFQAaQBtAGUAbABpAG4AZQEAAAH+AAABHAAAAJYA////", |
2184 |
| - "AAAFEgAAAAEAAAABAAAAAgAAAAEAAAAC/", |
2185 |
| - "AAAAAEAAAACAAAAAQAAAA4AdABvAG8AbABCAGEAcgEAAAAA/////wAAAAAAAAAA" |
2186 |
| - ]) |
| 2174 | + "AAAA/wAAAAD9AAAAAwAAAAAAAAEnAAAC3/wCAAAAA/wAAAJeAAAApwAAAAAA////+gAAAAACAAAAAfsAAAA" |
| 2175 | + "YAGQAbwBjAGsASwBlAHkAZgByAGEAbQBlAAAAAAD/////AAAAAAAAAAD7AAAAHABkAG8AYwBrAFAAcgBvAH" |
| 2176 | + "AAZQByAHQAaQBlAHMAAAAAJwAAAt8AAAChAP////sAAAAYAGQAbwBjAGsAVAB1AHQAbwByAGkAYQBsAgAAA" |
| 2177 | + "AAAAAAAAAAAyAAAAGQAAAABAAABHAAAAUD8AgAAAAH7AAAAGABkAG8AYwBrAEsAZQB5AGYAcgBhAG0AZQEA" |
| 2178 | + "AAFYAAAAFQAAAAAAAAAAAAAAAgAABEYAAALY/AEAAAAC/AAAAAAAAANnAAAA+gD////8AgAAAAL8AAAAJwA" |
| 2179 | + "AAcAAAACvAP////wBAAAAAvwAAAAAAAABFQAAAHsA////+gAAAAACAAAAA/sAAAASAGQAbwBjAGsARgBpAG" |
| 2180 | + "wAZQBzAQAAAAD/////AAAAkgD////7AAAAHgBkAG8AYwBrAFQAcgBhAG4AcwBpAHQAaQBvAG4AcwEAAAAA/" |
| 2181 | + "////wAAAJIA////+wAAABYAZABvAGMAawBFAGYAZgBlAGMAdABzAQAAAAD/////AAAAkgD////7AAAAEgBk" |
| 2182 | + "AG8AYwBrAFYAaQBkAGUAbwEAAAEbAAACTAAAAEcA////+wAAABgAZABvAGMAawBUAGkAbQBlAGwAaQBuAGU" |
| 2183 | + "BAAAB7QAAARIAAACWAP////wAAANtAAAA2QAAAIIA////+gAAAAECAAAAAvsAAAAiAGQAbwBjAGsAQwBhAH" |
| 2184 | + "AAdABpAG8AbgBFAGQAaQB0AG8AcgAAAAAA/////wAAAJgA////+wAAABQAZABvAGMAawBFAG0AbwBqAGkAc" |
| 2185 | + "wEAAADFAAACOgAAAJIA////AAAERgAAAAEAAAABAAAAAgAAAAEAAAAC/AAAAAEAAAACAAAAAQAAAA4AdABv" |
| 2186 | + "AG8AbABCAGEAcgEAAAAA/////wAAAAAAAAAA" |
| 2187 | + ]) |
2187 | 2188 | self.restoreState(qt_types.str_to_bytes(simple_state))
|
2188 | 2189 | QCoreApplication.processEvents()
|
2189 | 2190 |
|
@@ -2213,21 +2214,18 @@ def actionAdvanced_View_trigger(self):
|
2213 | 2214 |
|
2214 | 2215 | # Set initial size of docks
|
2215 | 2216 | advanced_state = "".join([
|
2216 |
| - "AAAA/wAAAAD9AAAAAwAAAAAAAADxAAAC+vwCAAAAAvsAAAAcAGQAbwBjAGsAUAB", |
2217 |
| - "yAG8AcABlAHIAdABpAGUAcwEAAAAnAAAC+gAAAJ8A/////AAAAl4AAACnAAAAAAD////", |
2218 |
| - "6AAAAAAIAAAAB+wAAABgAZABvAGMAawBLAGUAeQBmAHIAYQBtAGUAAAAAAP////", |
2219 |
| - "8AAAAAAAAAAAAAAAEAAACZAAAC+vwCAAAAAvsAAAAYAGQAbwBjAGsASwBlAHkAZg", |
2220 |
| - "ByAGEAbQBlAQAAAVgAAAAVAAAAAAAAAAD7AAAAFgBkAG8AYwBrAEUAZgBmAGUAY", |
2221 |
| - "wB0AHMBAAAAJwAAAvoAAACRAP///wAAAAIAAAN8AAAC8/wBAAAAAfwAAAD3AAAD", |
2222 |
| - "fAAAAPoA/////AIAAAAC/AAAACcAAAHZAAABRAD////8AQAAAAL8AAAA9wAAAVsAAAB7AP////", |
2223 |
| - "wCAAAAAvsAAAASAGQAbwBjAGsARgBpAGwAZQBzAQAAACcAAADkAAAAkQD////", |
2224 |
| - "8AAABEQAAAO8AAACtAQAAG/oAAAAAAQAAAAL7AAAAHgBkAG8AYwBrAFQAcg", |
2225 |
| - "BhAG4AcwBpAHQAaQBvAG4AcwEAAAAA/////wAAAGwA////", |
2226 |
| - "+wAAABQAZABvAGMAawBFAG0AbwBqAGkAcwEAAAD3AAABHQAAAFgA////", |
2227 |
| - "+wAAABIAZABvAGMAawBWAGkAZABlAG8BAAACWAAAAhsAAABHAP////", |
2228 |
| - "sAAAAYAGQAbwBjAGsAVABpAG0AZQBsAGkAbgBlAQAAAgYAAAEUAAAAlgD///", |
2229 |
| - "8AAAN8AAAAAQAAAAEAAAACAAAAAQAAAAL8AAAAAQAAAAIAAAABAAAADgB0", |
2230 |
| - "AG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA=" |
| 2217 | + "AAAA/wAAAAD9AAAAAwAAAAAAAADxAAAC3/wCAAAAAvsAAAAcAGQAbwBjAGsAUAByAG8AcABlAHIAdABpAGUAcw" |
| 2218 | + "EAAAAnAAAC3wAAAKEA/////AAAAl4AAACnAAAAAAD////6AAAAAAIAAAAB+wAAABgAZABvAGMAawBLAGUAeQBm" |
| 2219 | + "AHIAYQBtAGUAAAAAAP////8AAAAAAAAAAAAAAAEAAACZAAAC3/wCAAAAAvsAAAAYAGQAbwBjAGsASwBlAHkAZg" |
| 2220 | + "ByAGEAbQBlAQAAAVgAAAAVAAAAAAAAAAD8AAAAJwAAAt8AAAC1AQAAHPoAAAAAAQAAAAL7AAAAFgBkAG8AYwBr" |
| 2221 | + "AEUAZgBmAGUAYwB0AHMBAAADrQAAAJkAAABYAP////sAAAAiAGQAbwBjAGsAQwBhAHAAdABpAG8AbgBFAGQAaQ" |
| 2222 | + "B0AG8AcgEAAAAA/////wAAAFgA////AAAAAgAAArAAAALY/AEAAAAB/AAAAPcAAAKwAAAA+gD////8AgAAAAL8" |
| 2223 | + "AAAAJwAAAcgAAAFHAP////wBAAAAAvwAAAD3AAAArgAAAIIA/////AIAAAAC+wAAABIAZABvAGMAawBGAGkAbA" |
| 2224 | + "BlAHMBAAAAJwAAAOQAAACSAP////wAAAERAAAA3gAAAK8BAAAc+gAAAAABAAAAAvsAAAAeAGQAbwBjAGsAVABy" |
| 2225 | + "AGEAbgBzAGkAdABpAG8AbgBzAQAAAAD/////AAAAbAD////7AAAAFABkAG8AYwBrAEUAbQBvAGoAaQBzAQAAAP" |
| 2226 | + "cAAAEdAAAAggD////7AAAAEgBkAG8AYwBrAFYAaQBkAGUAbwEAAAGrAAAB/AAAAEcA////+wAAABgAZABvAGMA" |
| 2227 | + "awBUAGkAbQBlAGwAaQBuAGUBAAAB9QAAAQoAAACWAP///wAAArAAAAABAAAAAQAAAAIAAAABAAAAAvwAAAABAA" |
| 2228 | + "AAAgAAAAEAAAAOAHQAbwBvAGwAQgBhAHIBAAAAAP////8AAAAAAAAAAA==" |
2231 | 2229 | ])
|
2232 | 2230 | self.restoreState(qt_types.str_to_bytes(advanced_state))
|
2233 | 2231 | QCoreApplication.processEvents()
|
@@ -2263,6 +2261,57 @@ def actionTutorial_trigger(self):
|
2263 | 2261 | self.tutorial_manager.exit_manager()
|
2264 | 2262 | self.tutorial_manager = TutorialManager(self)
|
2265 | 2263 |
|
| 2264 | + def actionInsertTimestamp_trigger(self, event): |
| 2265 | + """Insert the current timestamp into the caption editor |
| 2266 | + In the format: 00:00:23,000 --> 00:00:24,500. first click to set the initial timestamp, |
| 2267 | + move the playehad, second click to set the end timestamp. |
| 2268 | + """ |
| 2269 | + # Get translation function |
| 2270 | + app = get_app() |
| 2271 | + _ = app._tr |
| 2272 | + |
| 2273 | + if self.captionTextEdit.isReadOnly(): |
| 2274 | + return |
| 2275 | + |
| 2276 | + # Calculate fps / current seconds |
| 2277 | + fps = get_app().project.get("fps") |
| 2278 | + fps_float = float(fps["num"]) / float(fps["den"]) |
| 2279 | + current_position = (self.preview_thread.current_frame - 1) / fps_float |
| 2280 | + |
| 2281 | + # Get cursor / current line of text (where cursor is located) |
| 2282 | + cursor = self.captionTextEdit.textCursor() |
| 2283 | + self.captionTextEdit.moveCursor(QTextCursor.StartOfLine) |
| 2284 | + line_text = cursor.block().text() |
| 2285 | + self.captionTextEdit.moveCursor(QTextCursor.EndOfLine) |
| 2286 | + |
| 2287 | + # Insert text at cursor position |
| 2288 | + current_timestamp = secondsToTimecode(current_position, fps["num"], fps["den"], use_milliseconds=True) |
| 2289 | + if "-->" in line_text: |
| 2290 | + self.captionTextEdit.insertPlainText("%s\n%s" % (current_timestamp, _("Enter caption text..."))) |
| 2291 | + else: |
| 2292 | + self.captionTextEdit.insertPlainText("%s --> " % (current_timestamp)) |
| 2293 | + |
| 2294 | + def captionTextEdit_TextChanged(self): |
| 2295 | + """Caption text was edited, start the save timer (to prevent spamming saves)""" |
| 2296 | + self.caption_save_timer.start() |
| 2297 | + |
| 2298 | + def caption_editor_save(self): |
| 2299 | + """Emit the CaptionTextUpdated signal (and if that property is active/selected, it will be saved)""" |
| 2300 | + self.CaptionTextUpdated.emit(self.captionTextEdit.toPlainText(), self.caption_model_row) |
| 2301 | + |
| 2302 | + def caption_editor_load(self, new_caption_text, caption_model_row): |
| 2303 | + """Load the caption editor with text, or disable it if empty string detected""" |
| 2304 | + self.caption_model_row = caption_model_row |
| 2305 | + self.captionTextEdit.setPlainText(new_caption_text.strip()) |
| 2306 | + if not caption_model_row: |
| 2307 | + self.captionTextEdit.setReadOnly(True) |
| 2308 | + else: |
| 2309 | + self.captionTextEdit.setReadOnly(False) |
| 2310 | + |
| 2311 | + # Show this dock |
| 2312 | + self.dockCaptionEditor.show() |
| 2313 | + self.dockCaptionEditor.raise_() |
| 2314 | + |
2266 | 2315 | def SetWindowTitle(self, profile=None):
|
2267 | 2316 | """ Set the window title based on a variety of factors """
|
2268 | 2317 |
|
@@ -2332,6 +2381,9 @@ def addSelection(self, item_id, item_type, clear_existing=False):
|
2332 | 2381 | elif item_type == "effect":
|
2333 | 2382 | self.selected_effects.clear()
|
2334 | 2383 |
|
| 2384 | + # Clear caption editor (if nothing is selected) |
| 2385 | + get_app().window.CaptionTextLoaded.emit("", None) |
| 2386 | + |
2335 | 2387 | if item_id:
|
2336 | 2388 | # If item_id is not blank, store it
|
2337 | 2389 | if item_type == "clip" and item_id not in self.selected_clips:
|
@@ -2363,6 +2415,9 @@ def removeSelection(self, item_id, item_type):
|
2363 | 2415 | # Clear transform (if no other clips are selected)
|
2364 | 2416 | self.TransformSignal.emit("")
|
2365 | 2417 |
|
| 2418 | + # Clear caption editor (if nothing is selected) |
| 2419 | + get_app().window.CaptionTextLoaded.emit("", None) |
| 2420 | + |
2366 | 2421 | # Move selection to next selected clip (if any)
|
2367 | 2422 | self.show_property_id = ""
|
2368 | 2423 | self.show_property_type = ""
|
@@ -2594,6 +2649,27 @@ def setup_toolbars(self):
|
2594 | 2649 | self.timelineToolbar.addAction(self.actionCenterOnPlayhead)
|
2595 | 2650 | self.timelineToolbar.addSeparator()
|
2596 | 2651 |
|
| 2652 | + # Add Video Preview toolbar |
| 2653 | + self.captionToolbar = QToolBar(_("Caption Toolbar")) |
| 2654 | + |
| 2655 | + # Add Caption text editor widget |
| 2656 | + self.captionTextEdit = QTextEdit() |
| 2657 | + self.captionTextEdit.setReadOnly(True) |
| 2658 | + |
| 2659 | + # Playback controls (centered) |
| 2660 | + self.captionToolbar.addAction(self.actionInsertTimestamp) |
| 2661 | + self.tabCaptions.layout().addWidget(self.captionToolbar) |
| 2662 | + self.tabCaptions.layout().addWidget(self.captionTextEdit) |
| 2663 | + |
| 2664 | + # Hook up caption editor signal |
| 2665 | + self.captionTextEdit.textChanged.connect(self.captionTextEdit_TextChanged) |
| 2666 | + self.caption_save_timer = QTimer() |
| 2667 | + self.caption_save_timer.setInterval(100) |
| 2668 | + self.caption_save_timer.setSingleShot(True) |
| 2669 | + self.caption_save_timer.timeout.connect(self.caption_editor_save) |
| 2670 | + self.CaptionTextLoaded.connect(self.caption_editor_load) |
| 2671 | + self.caption_model_row = None |
| 2672 | + |
2597 | 2673 | # Get project's initial zoom value
|
2598 | 2674 | initial_scale = get_app().project.get("scale") or 15
|
2599 | 2675 | # Round non-exponential scale down to next lowest power of 2
|
|
0 commit comments