diff --git a/doc/export.rst b/doc/export.rst index b2094e387..2e8e0e9f3 100644 --- a/doc/export.rst +++ b/doc/export.rst @@ -65,13 +65,15 @@ Advanced Options .. table:: :widths: 10 30 - ================== ============ - Advanced Setting Description - ================== ============ - Export To Export both `video & audio`, `only audio`, `only video`, or an `image sequence` - Start Frame The first frame to export (default is 1) - End Frame The final frame to export (default is the last frame in your project to contain a clip) - ================== ============ + ======================= ============ + Advanced Setting Description + ======================= ============ + Export To Export both `video & audio`, `only audio`, `only video`, or an `image sequence` + Start Frame The first frame to export (default is 1) + End Frame The final frame to export (default is the last frame in your project to contain a clip) + Start at First Clip This checkbox will toggle the **Start Frame** between `0.0` and the `start` of the first clip/transition position. + End at Last Clip This checkbox will toggle the **End Frame** between the `end` of the furthest clip/transition and the full `project duration`. The project duration can be adjusted by dragging the right edge of any track. You will need to zoom out (:guilabel:`Ctrl+Scroll Wheel`) of the timeline before you can drag the right edge of a track. + ======================= ============ Profile ^^^^^^^ diff --git a/doc/main_window.rst b/doc/main_window.rst index 2ce820bdc..30e5678be 100644 --- a/doc/main_window.rst +++ b/doc/main_window.rst @@ -103,7 +103,7 @@ Timeline Toolbar Previous Marker Jump to the previous marker. This moves the playhead to the left, seeking to the next marker or important position (i.e. start / end positions of clips). Next Marker Jump to the next marker. This moves the playhead to the right, seeking to the next marker or important position (i.e. start / end positions of clips). Center Timeline on Playhead This centers the timeline on the playhead position. This can be useful if the playhead is not visible and you want to quickly scroll the timeline to that position. - Zoom Slider This controls the visible portion of the timeline. Adjusting the left/right handles will zoom in/out of your timeline, keeping a specific section of your project in view. + Zoom Slider This controls the visible portion of the timeline. Adjusting the left/right handles will zoom in/out of your timeline, keeping a specific section of your project in view. Double click to zoom to your entire timeline. =========================== ============ .. _keyboard_shortcut_ref: @@ -116,91 +116,95 @@ configure these shortcuts in the Preferences window, which is opened by selectin (On macOS, choose :guilabel:`OpenShot Video Editor→Preferences`.) Learning a few of these shortcuts can save you a bunch of time! -=================================== ======================= ========================== ==================== -Action Shortcut 1 Shortcut 2 Shortcut 3 -=================================== ======================= ========================== ==================== -About OpenShot :kbd:`Ctrl+H` -Add Marker :kbd:`M` -Add Track :kbd:`Ctrl+Y` -Add to Timeline :kbd:`Ctrl+Alt+A` -Advanced View :kbd:`Alt+Shift+1` -Animated Title :kbd:`Ctrl+Shift+T` -Ask a Question... :kbd:`F4` -Center on Playhead :kbd:`Shift+C` :kbd:`Alt+Up` -Choose Profile :kbd:`Ctrl+Alt+P` -Clear All Cache :kbd:`Ctrl+Shift+ESC` -Clear History :kbd:`Ctrl+Shift+H` -Clear Waveform Display Data :kbd:`Ctrl+Shift+W` -Copy :kbd:`Ctrl+C` -Cut :kbd:`Ctrl+X` -Delete Item :kbd:`Delete` :kbd:`Backspace` -Delete Item (Ripple) :kbd:`Shift+Delete` -Details View :kbd:`Ctrl+Page Up` -Donate :kbd:`F7` -Duplicate :kbd:`Ctrl+Shift+/` -Edit Title :kbd:`Alt+T` -Export Selected Files :kbd:`Ctrl+Shift+E` -Export Video / Media :kbd:`Ctrl+E` :kbd:`Ctrl+M` -Fast Forward :kbd:`L` -File Properties :kbd:`Alt+I` :kbd:`Ctrl+Double Click` -Freeze View :kbd:`Ctrl+F` -Fullscreen :kbd:`F11` -Import Files... :kbd:`Ctrl+I` -Insert Keyframe :kbd:`Alt+Shift+K` -Join our Community... :kbd:`F5` -Jump To End :kbd:`End` -Jump To Start :kbd:`Home` -Launch Tutorial :kbd:`F2` -New Project :kbd:`Ctrl+N` -Next Frame :kbd:`Right` :kbd:`.` -Next Marker :kbd:`Shift+M` :kbd:`Alt+Right` -Nudge left (1 Frame) :kbd:`Ctrl+Left` -Nudge left (5 Frames) :kbd:`Shift+Ctrl+Left` -Nudge right (1 Frame) :kbd:`Ctrl+Right` -Nudge right (5 Frames) :kbd:`Shift+Ctrl+Right` -Open Help Contents :kbd:`F1` -Open Project... :kbd:`Ctrl+O` -Paste :kbd:`Ctrl+V` -Play/Pause Toggle :kbd:`Space` :kbd:`Up` :kbd:`Down` -Preferences :kbd:`Ctrl+P` -Preview File :kbd:`Alt+P` :kbd:`Double Click` -Previous Frame :kbd:`Left` :kbd:`,` -Previous Marker :kbd:`Ctrl+Shift+M` :kbd:`Alt+Left` -Properties :kbd:`U` -Quit :kbd:`Ctrl+Q` -Razor Toggle :kbd:`C` :kbd:`B` :kbd:`R` -Redo :kbd:`Ctrl+Shift+Z` -Report a Bug... :kbd:`F3` -Rewind :kbd:`J` -Save Current Frame :kbd:`Ctrl+Shift+Y` -Save Current Frame :kbd:`Ctrl+Shift+Y` -Save Project :kbd:`Ctrl+S` -Save Project As... :kbd:`Ctrl+Shift+S` -Select All :kbd:`Ctrl+A` -Select Item (Ripple) :kbd:`Alt+A` :kbd:`Alt+Click` -Select None :kbd:`Ctrl+Shift+A` -Show All Docks :kbd:`Ctrl+Shift+D` -Simple View :kbd:`Alt+Shift+0` -Slice All: Keep Both Sides :kbd:`Ctrl+Shift+K` -Slice All: Keep Left Side :kbd:`Ctrl+Shift+J` -Slice All: Keep Right Side :kbd:`Ctrl+Shift+L` -Slice Selected: Keep Both Sides :kbd:`Ctrl+K` -Slice Selected: Keep Left Side :kbd:`Ctrl+J` -Slice Selected: Keep Right Side :kbd:`Ctrl+L` -Slice Selected: Keep Left (Ripple) :kbd:`W` -Slice Selected: Keep Right (Ripple) :kbd:`Q` -Snapping Toggle :kbd:`S` -Split File :kbd:`Alt+S` :kbd:`Shift+Double Click` -Thumbnail View :kbd:`Ctrl+Page Down` -Title :kbd:`Ctrl+T` -Transform :kbd:`Ctrl+Alt+T` -Translate this Application... :kbd:`F6` -Un-Freeze View :kbd:`Ctrl+Shift+F` -Undo :kbd:`Ctrl+Z` -View Toolbar :kbd:`Ctrl+Shift+B` -Zoom In :kbd:`=` :kbd:`Ctrl+=` -Zoom Out :kbd:`-` :kbd:`Ctrl+-` -=================================== ======================= ========================== ==================== +.. table:: + :widths: 35 20 20 20 + + =================================== ======================= ========================== ==================== + Action Shortcut 1 Shortcut 2 Shortcut 3 + =================================== ======================= ========================== ==================== + About OpenShot :kbd:`Ctrl+H` + Add Marker :kbd:`M` + Add Track :kbd:`Ctrl+Y` + Add to Timeline :kbd:`Ctrl+Alt+A` + Advanced View :kbd:`Alt+Shift+1` + Animated Title :kbd:`Ctrl+Shift+T` + Ask a Question... :kbd:`F4` + Center on Playhead :kbd:`Shift+C` :kbd:`Alt+Up` + Choose Profile :kbd:`Ctrl+Alt+P` + Clear All Cache :kbd:`Ctrl+Shift+ESC` + Clear History :kbd:`Ctrl+Shift+H` + Clear Waveform Display Data :kbd:`Ctrl+Shift+W` + Copy :kbd:`Ctrl+C` + Cut :kbd:`Ctrl+X` + Delete Item :kbd:`Delete` :kbd:`Backspace` + Delete Item (Ripple) :kbd:`Shift+Delete` + Details View :kbd:`Ctrl+Page Up` + Donate :kbd:`F7` + Duplicate :kbd:`Ctrl+Shift+/` + Edit Title :kbd:`Alt+T` + Export Selected Files :kbd:`Ctrl+Shift+E` + Export Video / Media :kbd:`Ctrl+E` :kbd:`Ctrl+M` + Fast Forward :kbd:`L` + File Properties :kbd:`Alt+I` :kbd:`Ctrl+Double Click` + Freeze View :kbd:`Ctrl+F` + Fullscreen :kbd:`F11` + Import Files... :kbd:`Ctrl+I` + Insert Keyframe :kbd:`Alt+Shift+K` + Join our Community... :kbd:`F5` + Jump To End :kbd:`End` + Jump To Start :kbd:`Home` + Launch Tutorial :kbd:`F2` + New Project :kbd:`Ctrl+N` + Next Frame :kbd:`Right` :kbd:`.` + Next Marker :kbd:`Shift+M` :kbd:`Alt+Right` + Nudge left (1 Frame) :kbd:`Ctrl+Left` + Nudge left (5 Frames) :kbd:`Shift+Ctrl+Left` + Nudge right (1 Frame) :kbd:`Ctrl+Right` + Nudge right (5 Frames) :kbd:`Shift+Ctrl+Right` + Open Help Contents :kbd:`F1` + Open Project... :kbd:`Ctrl+O` + Paste :kbd:`Ctrl+V` + Play/Pause Toggle :kbd:`Space` :kbd:`Up` :kbd:`Down` + Preferences :kbd:`Ctrl+P` + Preview File :kbd:`Alt+P` :kbd:`Double Click` + Previous Frame :kbd:`Left` :kbd:`,` + Previous Marker :kbd:`Ctrl+Shift+M` :kbd:`Alt+Left` + Properties :kbd:`U` + Quit :kbd:`Ctrl+Q` + Razor Toggle :kbd:`C` :kbd:`B` :kbd:`R` + Redo :kbd:`Ctrl+Shift+Z` + Report a Bug... :kbd:`F3` + Rewind :kbd:`J` + Save Current Frame :kbd:`Ctrl+Shift+Y` + Save Current Frame :kbd:`Ctrl+Shift+Y` + Save Project :kbd:`Ctrl+S` + Save Project As... :kbd:`Ctrl+Shift+S` + Select All :kbd:`Ctrl+A` + Select Item (Ripple) :kbd:`Alt+A` :kbd:`Alt+Click` + Select None :kbd:`Ctrl+Shift+A` + Show All Docks :kbd:`Ctrl+Shift+D` + Simple View :kbd:`Alt+Shift+0` + Slice All: Keep Both Sides :kbd:`Ctrl+Shift+K` + Slice All: Keep Left Side :kbd:`Ctrl+Shift+J` + Slice All: Keep Right Side :kbd:`Ctrl+Shift+L` + Slice Selected: Keep Both Sides :kbd:`Ctrl+K` + Slice Selected: Keep Left Side :kbd:`Ctrl+J` + Slice Selected: Keep Right Side :kbd:`Ctrl+L` + Slice Selected: Keep Left (Ripple) :kbd:`W` + Slice Selected: Keep Right (Ripple) :kbd:`Q` + Snapping Toggle :kbd:`S` + Split File :kbd:`Alt+S` :kbd:`Shift+Double Click` + Thumbnail View :kbd:`Ctrl+Page Down` + Title :kbd:`Ctrl+T` + Transform :kbd:`Ctrl+Alt+T` + Translate this Application... :kbd:`F6` + Un-Freeze View :kbd:`Ctrl+Shift+F` + Undo :kbd:`Ctrl+Z` + View Toolbar :kbd:`Ctrl+Shift+B` + Zoom In :kbd:`=` :kbd:`Ctrl+=` + Zoom Out :kbd:`-` :kbd:`Ctrl+-` + Zoom to Timeline :kbd:`\\` :kbd:`Shift+\\` :kbd:`Double Click` + =================================== ======================= ========================== ==================== Menu ---- diff --git a/src/classes/timeline.py b/src/classes/timeline.py index aaf8267cd..6f2b9ce66 100644 --- a/src/classes/timeline.py +++ b/src/classes/timeline.py @@ -74,8 +74,7 @@ def changed(self, action): """ This method is invoked by the UpdateManager each time a change happens (i.e UpdateInterface) """ # Ignore changes that don't affect libopenshot - if action and len(action.key) >= 1 and action.key[0].lower() in ["files", "history", "markers", - "layers", "scale", "profile"]: + if action and len(action.key) >= 1 and action.key[0].lower() in ["files", "history", "markers", "layers", "scale", "profile", "export_settings"]: return # Disable video caching temporarily diff --git a/src/language/OpenShot/OpenShot.pot b/src/language/OpenShot/OpenShot.pot index 716e5ff90..0b3224f40 100644 --- a/src/language/OpenShot/OpenShot.pot +++ b/src/language/OpenShot/OpenShot.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: OpenShot Video Editor (version: 3.2.1-dev)\n" "Report-Msgid-Bugs-To: Jonathan Thomas \n" -"POT-Creation-Date: 2024-09-17 12:16:02.880382\n" +"POT-Creation-Date: 2024-09-30 16:30:18.199948\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Jonathan Thomas \n" "Language-Team: https://translations.launchpad.net/+groups/launchpad-translators\n" @@ -48,9 +48,9 @@ msgid "Reset Zoom" msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/export.py:91 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:850 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:861 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1141 Settings for +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:889 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:900 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1243 Settings for #: actionExportVideo #: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:20 msgid "Export Video" @@ -62,11 +62,11 @@ msgstr "" msgid "Done" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:144 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:147 msgid "Project Data Error" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:145 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:148 #, python-format msgid "" "Sorry, an error was encountered while parsing your project data: \n" @@ -75,139 +75,139 @@ msgid "" "Please save your project and inspect in a JSON editor to repair." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:161 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:164 #: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:658 #: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:756 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2559 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2562 #: /home/jonathan/apps/openshot-qt/src/classes/exporters/edl.py:56 #: /home/jonathan/apps/openshot-qt/src/classes/exporters/final_cut_pro.py:90 msgid "Untitled Project" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:173 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:515 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:858 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:944 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:958 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:176 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:552 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:897 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:983 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:997 msgid "Video & Audio" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:173 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:515 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:858 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:944 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:176 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:552 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:897 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:983 msgid "Video Only" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:173 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:516 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:858 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:958 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:972 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:176 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:553 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:897 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:997 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1011 msgid "Audio Only" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:173 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:516 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:818 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:894 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:944 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:176 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:553 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:857 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:933 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:983 msgid "Image Sequence" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:180 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:183 #: /home/jonathan/apps/openshot-qt/src/windows/file_properties.py:143 Settings #: for default-channellayout msgid "Mono (1 Channel)" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:181 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:184 #: /home/jonathan/apps/openshot-qt/src/windows/file_properties.py:144 Settings #: for default-channellayout msgid "Stereo (2 Channel)" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:182 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:185 #: /home/jonathan/apps/openshot-qt/src/windows/file_properties.py:145 Settings #: for default-channellayout msgid "Surround (3 Channel)" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:183 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:186 #: /home/jonathan/apps/openshot-qt/src/windows/file_properties.py:146 Settings #: for default-channellayout msgid "Surround (5.1 Channel)" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:184 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:187 #: /home/jonathan/apps/openshot-qt/src/windows/file_properties.py:147 Settings #: for default-channellayout msgid "Surround (7.1 Channel)" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:259 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:264 #: /home/jonathan/apps/openshot-qt/src/presets/format_mp4_x264_hw.xml msgid "All Formats" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:422 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:459 #: /home/jonathan/apps/openshot-qt/src/presets/format_mp4_x264.xml msgid "MP4 (h.264)" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:452 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:489 #: /home/jonathan/apps/openshot-qt/src/windows/file_properties.py:161 #: libopenshot (Clip Properties) msgid "No" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:453 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:490 msgid "Yes Top field first" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:454 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:491 msgid "Yes Bottom field first" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:529 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:537 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:605 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:566 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:574 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:642 msgid "Low" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:529 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:537 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:607 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:566 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:574 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:644 msgid "Med" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:529 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:537 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:609 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:566 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:574 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:646 msgid "High" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:671 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:708 msgid "Choose a Folder..." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:798 -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1098 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:837 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1137 msgid "Export Error" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:799 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:838 msgid "Sorry, please select a valid range of frames to export" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:851 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:890 #, python-format msgid "" "%s is an input file.\n" "Please choose a different name." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:862 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:901 #: /home/jonathan/apps/openshot-qt/src/windows/title_editor.py:716 #, python-format msgid "" @@ -215,18 +215,18 @@ msgid "" "Do you want to replace it?" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1026 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1065 msgid "Finalizing video export, please wait..." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1099 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1138 #, python-format msgid "" "Sorry, there was an error exporting your video: \n" "%s" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1142 +#: /home/jonathan/apps/openshot-qt/src/windows/export.py:1244 msgid "Are you sure you want to cancel the export?" msgstr "" @@ -1061,13 +1061,13 @@ msgid "" msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/preferences.py:266 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:1004 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:1053 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:117 msgid "Browse..." msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/preferences.py:305 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:179 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:203 msgid "Search Profiles" msgstr "" @@ -1089,7 +1089,7 @@ msgid "Select executable file" msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/preferences.py:721 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/preferences.ui:48 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:48 msgid "Restore Defaults" msgstr "" @@ -1253,51 +1253,51 @@ msgstr "" msgid "Track Name:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2318 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2321 msgid "Docks" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2512 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2515 msgid "Enter caption text..." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2730 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2733 msgid "Recent Projects" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2739 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2742 msgid "No Recent Projects" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2795 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2811 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2829 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2839 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3407 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2798 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2814 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2832 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2842 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3410 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:432 msgid "Filter" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2854 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2857 msgid "Caption Toolbar" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2926 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2929 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1452 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1455 msgid "Update Available" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2927 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2930 #, python-format msgid "Update Available: %s" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3364 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3367 msgid "Error starting local HTTP server" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3365 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3368 msgid "Failed multiple attempts to start server:" msgstr "" @@ -1315,7 +1315,7 @@ msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/models/titles_model.py:91 #: /home/jonathan/apps/openshot-qt/src/windows/models/emoji_model.py:77 #: /home/jonathan/apps/openshot-qt/src/windows/models/effects_model.py:102 -#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:177 +#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:184 msgid "Name" msgstr "" @@ -1435,16 +1435,16 @@ msgstr "" msgid "SAR" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:177 +#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:184 msgid "Tags" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:366 +#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:377 #, python-format msgid "Importing %(count)d / %(total)d" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:389 +#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:410 #, python-format msgid "Imported %(count)d files" msgstr "" @@ -1548,11 +1548,11 @@ msgstr "" msgid "Please choose a region at the beginning of the clip" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/preview_thread.py:95 +#: /home/jonathan/apps/openshot-qt/src/windows/preview_thread.py:94 msgid "Audio Error" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/preview_thread.py:95 +#: /home/jonathan/apps/openshot-qt/src/windows/preview_thread.py:94 #, python-format msgid "" "Please fix the following error and restart OpenShot\n" @@ -1560,7 +1560,7 @@ msgid "" msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/title_editor.py:332 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:970 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:1019 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:43 msgid "File Name:" msgstr "" @@ -3013,6 +3013,10 @@ msgstr "" msgid "Undo" msgstr "" +#: Settings for actionZoomToTimeline +msgid "Zoom to Timeline" +msgstr "" + #: Settings for seekPreviousFrame msgid "Previous Frame" msgstr "" @@ -3386,19 +3390,19 @@ msgid "YourAnimation" msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/ui/animation.ui:165 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:606 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:647 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:386 msgid "Frame Rate:" msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/ui/animation.ui:235 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:442 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:483 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:468 msgid "Width:" msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/ui/animation.ui:273 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:475 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:516 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:222 msgid "Height:" msgstr "" @@ -3470,135 +3474,143 @@ msgstr "" msgid "Play" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:69 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:93 msgid "Simple" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:83 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:107 msgid "Select a Profile to start:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:107 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:415 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:131 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:456 msgid "Profile:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:128 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:152 msgid "Select from the following options:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:156 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:180 msgid "Target:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:201 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:225 msgid "Video Profile:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:218 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:242 msgid "Quality:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:251 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:275 msgid "Advanced" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:284 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:308 msgid "Advanced Options" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:301 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:325 msgid "Export To:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:328 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:352 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:876 msgid "Start Frame:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:361 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:375 +msgid "Start at First Clip" +msgstr "" + +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:392 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:837 msgid "End Frame:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:401 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:415 +msgid "End at Last Clip" +msgstr "" + +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:442 msgid "Profile" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:508 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:549 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:264 msgid "Aspect Ratio:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:557 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:598 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:325 msgid "Pixel Ratio:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:658 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:699 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:438 msgid "Interlaced:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:686 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:727 msgid "Image Sequence Settings" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:700 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:741 msgid "Image Format:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:721 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:762 msgid "Video Settings" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:735 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:776 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:525 msgid "Video Format:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:755 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:796 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:555 msgid "Video Codec:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:778 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:921 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:819 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:970 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:588 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:711 msgid "Bit Rate / Quality:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:791 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:840 msgid "Audio Settings" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:805 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:854 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:681 msgid "Audio Codec:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:825 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:874 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:639 msgid "Sample Rate:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:858 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:907 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:747 msgid "# of Channels:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:894 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:943 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:792 msgid "Channel Layout:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:977 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:1026 #: /home/jonathan/apps/openshot-qt/src/windows/ui/file-properties.ui:50 msgid "YourVideoName" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:994 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/export.ui:1043 msgid "Folder Path:" msgstr "" diff --git a/src/settings/_default.project b/src/settings/_default.project index dbdb60cc4..72f288a7d 100644 --- a/src/settings/_default.project +++ b/src/settings/_default.project @@ -26,6 +26,7 @@ "tick_pixels": 100, "playhead_position": 0, "profile": "HD 720p 30 fps", + "export_settings": null, "layers": [ { "id": "L1", diff --git a/src/settings/_default.settings b/src/settings/_default.settings index 654da10c1..3e7f1b6ae 100644 --- a/src/settings/_default.settings +++ b/src/settings/_default.settings @@ -814,6 +814,14 @@ "value": "- | Ctrl+-", "type": "text" }, + { + "category": "Keyboard", + "title": "Zoom to Timeline", + "restart": false, + "setting": "actionZoomToTimeline", + "value": "\\ | Shift+\\", + "type": "text" + }, { "category": "Keyboard", "title": "Previous Frame", diff --git a/src/themes/cosmic/theme.py b/src/themes/cosmic/theme.py index a333435f7..cd3aa5a99 100644 --- a/src/themes/cosmic/theme.py +++ b/src/themes/cosmic/theme.py @@ -188,7 +188,7 @@ def __init__(self, app): } QPushButton:hover { - background-color: #283241 + background-color: #283241; } QWidget#settingsContainer { @@ -574,6 +574,17 @@ def apply_theme(self): border-radius: 0px; height: 48px; } + .track-resize-handle { + background-color: #1B222CFF; + border-top: 1px solid #1B222CFF; + border-bottom: 1px solid #1B222CFF; + border-right: 1px solid #1B222CFF; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + } + .track-resize-handle:hover { + background-color: #333F51FF; + } .transition { height: 48px; min-height: 48px; diff --git a/src/themes/humanity/theme.py b/src/themes/humanity/theme.py index 3904152bb..211f3e485 100644 --- a/src/themes/humanity/theme.py +++ b/src/themes/humanity/theme.py @@ -32,10 +32,10 @@ class HumanityDarkTheme(BaseTheme): def __init__(self, app): super().__init__(app) self.style_sheet = """ -QToolTip { - color: #ffffff; - background-color: #2a82da; - border: 0px solid white; +QToolTip { + color: #ffffff; + background-color: #2a82da; + border: 0px solid white; } QComboBox::item { @@ -133,6 +133,12 @@ def apply_theme(self): background: #e5e7ea; box-shadow: none; } + .track-resize-handle { + background-color: #BEBFC1; + } + .track-resize-handle:hover { + background-color: #F7F8FA; + } .transition_top { background: none; border-radius: 0px; diff --git a/src/timeline/index.html b/src/timeline/index.html index 749aa882c..532587440 100644 --- a/src/timeline/index.html +++ b/src/timeline/index.html @@ -26,8 +26,8 @@ - + @@ -45,14 +45,14 @@
-
{{playheadTime.hour}}:{{playheadTime.min}}:{{playheadTime.sec}},{{playheadTime.frame}}
+
{{playheadTime.hour}}:{{playheadTime.min}}:{{playheadTime.sec}},{{playheadTime.frame}}
-
+
@@ -78,10 +78,11 @@
-
+
-
+
+
diff --git a/src/timeline/js/controllers.js b/src/timeline/js/controllers.js index b66c48657..4d9739390 100644 --- a/src/timeline/js/controllers.js +++ b/src/timeline/js/controllers.js @@ -347,10 +347,19 @@ App.controller("TimelineCtrl", function ($scope) { var scrolling_tracks = $("#scrolling_tracks"); var scrollingTracksWidth = scrolling_tracks.width(); - // Calculate the position to scroll the timeline to to center on the requested time + // Get the total width of the timeline (the entire scrollable content width) + var totalTimelineWidth = $scope.getTimelineWidth(0); + + // Calculate the position to scroll the timeline to center on the requested time var pixelToCenterOn = parseFloat(centerTime) * $scope.pixelsPerSecond; var scrollPosition = Math.max(pixelToCenterOn - (scrollingTracksWidth / 2.0), 0); + // Condition: Check if we are zoomed into the very right edge of the timeline + if (scrollPosition + scrollingTracksWidth >= totalTimelineWidth) { + // We are near the right edge, so align the right edge with the right of the screen + scrollPosition = totalTimelineWidth - scrollingTracksWidth; + } + // Scroll the timeline using JQuery scrolling_tracks.scrollLeft(Math.floor(scrollPosition + 0.5)); }; @@ -400,12 +409,6 @@ App.controller("TimelineCtrl", function ($scope) { $scope.setSnappingMode = function (enable_snapping) { $scope.$apply(function () { $scope.enable_snapping = enable_snapping; - if (enable_snapping) { - $(".droppable").draggable("option", "snapTolerance", 20); - } - else { - $(".droppable").draggable("option", "snapTolerance", 0); - } }); }; @@ -583,6 +586,11 @@ App.controller("TimelineCtrl", function ($scope) { // Select item (either clip or transition) $scope.selectItem = function (item_id, item_type, clear_selections, event, force_ripple) { + if ($scope.dragging) { + timeline.qt_log("DEBUG", "Skip selection due to dragging..."); + return; + } + // Trim item_id var id = item_id.replace(`${item_type}_`, ""); @@ -801,10 +809,14 @@ App.controller("TimelineCtrl", function ($scope) { } } // Resize timeline - if (furthest_right_edge > $scope.project.duration - min_timeline_padding || furthest_right_edge < $scope.project.duration - max_timeline_padding) { + if (furthest_right_edge > $scope.project.duration) { if ($scope.Qt) { let new_timeline_length = Math.max(min_timeline_length, furthest_right_edge + min_timeline_padding); timeline.resizeTimeline(new_timeline_length); + // Apply the new duration to the scope + $scope.$apply(function () { + $scope.project.duration = new_timeline_length; + }); } } }; @@ -939,6 +951,20 @@ App.controller("TimelineCtrl", function ($scope) { return Math.max(min_value, $scope.project.duration * $scope.pixelsPerSecond); }; + // Seek to the beginning of the timeline + $scope.rulerTimeClick = function () { + $scope.movePlayhead(0.0); + $scope.previewFrame(0.0); + + // Force a scroll event (from 1 to 0, to send the geometry to zoom slider) + $("#scrolling_tracks").scrollLeft(1); + + // Scroll to top/left when loading a project + $("#scrolling_tracks").animate({ + scrollTop: 0, + scrollLeft: 0 + }, "slow"); + }; // Get Position of item (used by Qt), both the position and track number. /** @@ -1390,6 +1416,11 @@ $scope.updateLayerIndex = function () { } } + // Add end of timeline position + var end_of_track = $scope.project.duration * $scope.pixelsPerSecond; + var end_of_track_diff = position - end_of_track; + diffs.push({"diff": end_of_track_diff, "position": end_of_track}); + // Loop through diffs (and find the smallest one) for (var diff_index = 0; diff_index < diffs.length; diff_index++) { var diff = diffs[diff_index].diff; @@ -1614,8 +1645,6 @@ $scope.updateLayerIndex = function () { delete previous_object[current_key]; } } - // Resize timeline if it's too small to contain all clips - $scope.resizeTimeline(); // Re-sort clips and transitions array $scope.sortItems(); diff --git a/src/timeline/js/directives/clip.js b/src/timeline/js/directives/clip.js index bf4c193d3..605d7e20b 100644 --- a/src/timeline/js/directives/clip.js +++ b/src/timeline/js/directives/clip.js @@ -27,7 +27,7 @@ */ -/*global setSelections, setBoundingBox, moveBoundingBox, bounding_box, drawAudio */ +/*global setSelections, setBoundingBox, moveBoundingBox, bounding_box, drawAudio, updateDraggables */ // Init variables var dragging = false; var resize_disabled = false; @@ -54,11 +54,13 @@ App.directive("tlClip", function ($timeout) { minWidth: 1, maxWidth: scope.clip.length * scope.pixelsPerSecond, start: function (e, ui) { - scope.setDragging(true); - // Set selections setSelections(scope, element, $(this).attr("id")); + // Set dragging mode + scope.setDragging(true); + resize_disabled = false; + // Set bounding box setBoundingBox(scope, $(this), "trimming"); @@ -276,12 +278,12 @@ App.directive("tlClip", function ($timeout) { distance: 5, cancel: ".effect-container,.clip_menu,.point", start: function (event, ui) { - previous_drag_position = null; - scope.setDragging(true); - // Set selections setSelections(scope, element, $(this).attr("id")); + previous_drag_position = null; + scope.setDragging(true); + // Store initial cursor vs draggable offset var elementOffset = $(this).offset(); var cursorOffset = { @@ -323,9 +325,11 @@ App.directive("tlClip", function ($timeout) { // Hide snapline (if any) scope.hideSnapline(); + // Call the shared function for drag stop + updateDraggables(scope, ui, "clip"); + // Clear previous drag position previous_drag_position = null; - scope.setDragging(false); }, drag: function (e, ui) { // Retrieve the initial cursor offset @@ -368,18 +372,6 @@ App.directive("tlClip", function ($timeout) { $(this).css("top", newY); } }); - }, - revert: function (valid) { - if (!valid) { - //the drop spot was invalid, so we're going to move all clips to their original position - $(".ui-selected").each(function () { - var oldY = start_clips[$(this).attr("id")]["top"]; - var oldX = start_clips[$(this).attr("id")]["left"]; - - $(this).css("left", oldX); - $(this).css("top", oldY); - }); - } } }); } @@ -402,7 +394,7 @@ App.directive("tlMultiSelectable", function () { element.selectable({ filter: ".droppable", distance: 0, - cancel: ".effect-container,.transition_menu,.clip_menu,.point", + cancel: ".effect-container,.transition_menu,.clip_menu,.point,.track-resize-handle", selected: function (event, ui) { // Identify the selected ID and TYPE var id = ui.selected.id; diff --git a/src/timeline/js/directives/playhead.js b/src/timeline/js/directives/playhead.js index bb9282517..2aa4bc3ba 100644 --- a/src/timeline/js/directives/playhead.js +++ b/src/timeline/js/directives/playhead.js @@ -37,26 +37,24 @@ App.directive("tlPlayhead", function () { link: function (scope, element, attrs) { // get the default top position so we can lock it in place vertically playhead_y_max = element.position().top; + var isDragging = false; element.on("mousedown", function (e) { // Set bounding box for the playhead setBoundingBox(scope, $("#playhead"), "playhead"); - if (scope.Qt) { - // Disable caching thread during scrubbing - timeline.DisableCacheThread(); - } - }); - element.on("contextmenu", function (e) { + // Start dragging + isDragging = true; + if (scope.Qt) { - // Enable caching thread after scrubbing - timeline.EnableCacheThread(); + // Disable caching thread during scrubbing + timeline.DisableCacheThread(); } }); - // Move playhead to new position (if it's not currently being animated) - element.on("mousemove", function (e) { - if (e.which === 1 && !scope.playhead_animating && !scope.getDragging()) { // left button + // Global mousemove listener + $(document).on("mousemove", function (e) { + if (isDragging && e.which === 1 && !scope.playhead_animating && !scope.getDragging()) { // left button is held // Calculate the playhead bounding box movement and apply snapping rules let cursor_position = e.pageX - $("#ruler").offset().left; let results = moveBoundingBox(scope, bounding_box.left, bounding_box.top, @@ -71,11 +69,23 @@ App.directive("tlPlayhead", function () { // Move playhead let playhead_seconds = snapToFPSGridTime(scope, pixelToTime(scope, new_position)); + playhead_seconds = Math.min(Math.max(0.0, playhead_seconds), scope.project.duration); scope.movePlayhead(playhead_seconds); scope.previewFrame(playhead_seconds); } }); + // Global mouseup listener to stop dragging + $(document).on("mouseup", function (e) { + if (isDragging) { + isDragging = false; + + if (scope.Qt) { + // Enable caching thread after scrubbing + timeline.EnableCacheThread(); + } + } + }); } }; }); diff --git a/src/timeline/js/directives/ruler.js b/src/timeline/js/directives/ruler.js index 28bf0bfc0..01231ec7c 100644 --- a/src/timeline/js/directives/ruler.js +++ b/src/timeline/js/directives/ruler.js @@ -27,7 +27,7 @@ */ -/*global setSelections, setBoundingBox, moveBoundingBox, bounding_box */ +/*global App, timeline, secondsToTime, setSelections, setBoundingBox, moveBoundingBox, bounding_box */ // Variables for panning by middle click var is_scrolling = false; var starting_scrollbar = {x: 0, y: 0}; @@ -39,7 +39,6 @@ var scroll_left_pixels = 0; // This container allows for tracks to be scrolled (with synced ruler) // and allows for panning of the timeline with the middle mouse button -/*global App, timeline, secondsToTime*/ App.directive("tlScrollableTracks", function () { return { restrict: "A", @@ -70,40 +69,42 @@ App.directive("tlScrollableTracks", function () { // Sync ruler to track scrolling element.on("scroll", function () { - //set amount scrolled - scroll_left_pixels = element.scrollLeft(); + var scrollLeft = element.scrollLeft(); + var timelineWidth = scope.getTimelineWidth(0); // Full width of the timeline + var maxScrollLeft = timelineWidth - element.width(); // Max horizontal scroll + // Clamp to right edge + element.scrollLeft(Math.min(scrollLeft, maxScrollLeft)); + + // Sync the ruler and other components $("#track_controls").scrollTop(element.scrollTop()); - $("#scrolling_ruler").scrollLeft(element.scrollLeft()); - $("#progress_container").scrollLeft(element.scrollLeft()); + $("#scrolling_ruler, #progress_container").scrollLeft(scrollLeft); - // Send scrollbar position to Qt + // Send scrollbar position to Qt if available if (scope.Qt) { - // Calculate scrollbar positions (left and right edge of scrollbar) - var timeline_length = scope.getTimelineWidth(0); - var left_scrollbar_edge = scroll_left_pixels / timeline_length; - var right_scrollbar_edge = (scroll_left_pixels + element.width()) / timeline_length; + // Create variables first and pass them as arguments + var leftScrollbarEdge = scrollLeft / timelineWidth; // Use the full timeline width + var rightScrollbarEdge = (scrollLeft + element.width()) / timelineWidth; // Use the full timeline width - // Send normalized scrollbar positions to Qt - timeline.ScrollbarChanged([left_scrollbar_edge, right_scrollbar_edge, timeline_length, element.width()]); + // Pass the variables as a JavaScript array (interpreted as a PyQt list) + timeline.ScrollbarChanged([leftScrollbarEdge, rightScrollbarEdge, timelineWidth, element.width()]); } - scope.$apply( () => { - scope.scrollLeft = element[0].scrollLeft; - }) - + // Update scrollLeft in scope + scope.$apply(() => scope.scrollLeft = scrollLeft); }); - // Pans the timeline (on middle mouse clip and drag) + // Pans the timeline (on middle mouse click and drag) element.on("mousemove", function (e) { if (is_scrolling) { - // Calculate difference from last position var difference = {x: starting_mouse_position.x - e.pageX, y: starting_mouse_position.y - e.pageY}; - var newPos = { x: starting_scrollbar.x + difference.x, y: starting_scrollbar.y + difference.y}; + var newPos = { + x: Math.max(0, Math.min(starting_scrollbar.x + difference.x, scope.getTimelineWidth(0) - element.width())), + y: Math.max(0, Math.min(starting_scrollbar.y + difference.y, $("#scrolling_tracks")[0].scrollHeight - element.height())) + }; // Scroll the tracks div - element.scrollLeft(newPos.x); - element.scrollTop(newPos.y); + element.scrollLeft(newPos.x).scrollTop(newPos.y); } }); @@ -154,11 +155,24 @@ App.directive("tlRuler", function ($timeout) { return { restrict: "A", link: function (scope, element, attrs) { - //on click of the ruler canvas, jump playhead to the clicked spot + var isDragging = false; + + // Start dragging when mousedown on the ruler element.on("mousedown", function (e) { + // Set bounding box for the playhead position + setBoundingBox(scope, $("#playhead"), "playhead"); + isDragging = true; + + if (scope.Qt) { + // Disable caching thread during scrubbing + timeline.DisableCacheThread(); + } + // Get playhead position var playhead_left = e.pageX - element.offset().left; var playhead_seconds = snapToFPSGridTime(scope, pixelToTime(scope, playhead_left)); + playhead_seconds = Math.min(Math.max(0.0, playhead_seconds), scope.project.duration); + var playhead_snapped_target = playhead_seconds * scope.pixelsPerSecond; // Immediately preview frame (don't wait for animated playhead) scope.previewFrame(playhead_seconds); @@ -170,8 +184,8 @@ App.directive("tlRuler", function ($timeout) { // Animate to new position (and then update scope) scope.playhead_animating = true; - $(".playhead-line").animate({left: playhead_left}, 200); - $(".playhead-top").animate({left: playhead_left}, 200, function () { + $(".playhead-line").animate({left: playhead_snapped_target}, 150); + $(".playhead-top").animate({left: playhead_snapped_target}, 150, function () { // Update playhead scope.movePlayhead(playhead_seconds); @@ -182,30 +196,14 @@ App.directive("tlRuler", function ($timeout) { }); }); - element.on("mousedown", function (e) { - // Set bounding box for the playhead position - setBoundingBox(scope, $("#playhead"), "playhead"); - if (scope.Qt) { - // Disable caching thread during scrubbing - timeline.DisableCacheThread(); - } - }); - - element.on("contextmenu", function (e) { - if (scope.Qt) { - // Enable caching thread after scrubbing - timeline.EnableCacheThread(); - } - }); - - // Move playhead to new position (if it's not currently being animated) - element.on("mousemove", function (e) { - if (e.which === 1 && !scope.playhead_animating && !scope.getDragging()) { // left button + // Global mousemove listener + $(document).on("mousemove", function (e) { + if (isDragging && e.which === 1 && !scope.playhead_animating && !scope.getDragging()) { // left button is held // Calculate the playhead bounding box movement let cursor_position = e.pageX - $("#ruler").offset().left; let new_position = cursor_position; if (e.shiftKey) { - // Only apply playhead shapping when SHIFT is pressed + // Only apply playhead snapping when SHIFT is pressed let results = moveBoundingBox(scope, bounding_box.left, bounding_box.top, cursor_position - bounding_box.left, cursor_position - bounding_box.top, cursor_position, cursor_position, "playhead"); @@ -216,11 +214,24 @@ App.directive("tlRuler", function ($timeout) { // Move playhead let playhead_seconds = new_position / scope.pixelsPerSecond; + playhead_seconds = Math.min(Math.max(0.0, playhead_seconds), scope.project.duration); scope.movePlayhead(playhead_seconds); scope.previewFrame(playhead_seconds); } }); + // Global mouseup listener to stop dragging + $(document).on("mouseup", function (e) { + if (isDragging) { + isDragging = false; + + if (scope.Qt) { + // Enable caching thread after scrubbing + timeline.EnableCacheThread(); + } + } + }); + /** * Draw frame precision alternating banding on each track (when zoomed in) */ diff --git a/src/timeline/js/directives/track.js b/src/timeline/js/directives/track.js index b4d847cce..35ac4f341 100644 --- a/src/timeline/js/directives/track.js +++ b/src/timeline/js/directives/track.js @@ -1,12 +1,11 @@ /** * @file - * @brief Track directives (droppable functionality, etc...) + * @brief Track directives (resizable functionality) * @author Jonathan Thomas - * @author Cody Parker * * @section LICENSE * - * Copyright (c) 2008-2018 OpenShot Studios, LLC + * Copyright (c) 2008-2024 OpenShot Studios, LLC * . This file is part of * OpenShot Video Editor, an open-source project dedicated to * delivering high quality video editing and animation solutions to the @@ -26,164 +25,68 @@ * along with OpenShot Library. If not, see . */ - -// Treats element as a track -// 1: allows clips, transitions, and effects to be dropped -/*global App, timeline, findTrackAtLocation*/ -App.directive("tlTrack", function ($timeout) { - return { - // A = attribute, E = Element, C = Class and M = HTML Comment - restrict: "A", - link: function (scope, element, attrs) { - - scope.$watch("project.layers.length", function (val) { - if (val) { - $timeout(function () { - // Update track indexes if tracks change - scope.updateLayerIndex(); - scope.playhead_height = $("#track-container").height(); - $(".playhead-line").height(scope.playhead_height); - }, 0); - - } - }); - - //make it accept drops - element.droppable({ - accept: ".droppable", - drop: function (event, ui) { - - // Disabling sorting (until all the updates are completed) - scope.enable_sorting = false; - - var scrolling_tracks = $("#scrolling_tracks"); - var vert_scroll_offset = scrolling_tracks.scrollTop(); - var horz_scroll_offset = scrolling_tracks.scrollLeft(); - - // Keep track of each dropped clip (to check for missing transitions below, after they have been dropped) - var dropped_clips = []; - var position_diff = 0; // the time diff to apply to multiple selections (if any) - var ui_selected = $(".ui-selected"); - var selected_item_count = ui_selected.length; - - // Arrays to collect updates for batch processing - var clip_updates = []; - var transition_updates = []; - - // Get uuid to group all these updates as a single transaction - var tid = uuidv4(); - var drop_track_num = -1; - - // with each dragged clip, find out which track they landed on - // Loop through each selected item, and remove the selection if multiple items are selected - // If only 1 item is selected, leave it selected - ui_selected.each(function (index) { - var item = $(this); - - // Determine type of item - var item_type = null; - if (item.hasClass("clip")) { - item_type = "clip"; - } else if (item.hasClass("transition")) { - item_type = "transition"; - } else { - // Unknown drop type - return; - } - - // get the item properties we need - var item_id = item.attr("id"); - var item_num = item_id.substr(item_id.indexOf("_") + 1); - var item_left = item.position().left; - - // Adjust top and left coordinates for scrollbars - item_left = parseFloat(item_left + horz_scroll_offset); - var item_top = parseFloat(item.position().top + vert_scroll_offset); - - // make sure the item isn't dropped off too far to the left - if (item_left < 0) { - item_left = 0; +/* global App, timeline, snapToFPSGridTime pixelToTime */ +App.directive("tlTrack", function () { + return { + restrict: "A", + link: function (scope, element) { + var startX, startWidth, isResizing = false, newDuration, minimumWidth; + + // Function to calculate the furthest right edge of any clip + var getFurthestRightEdge = function() { + return scope.project.clips.reduce((max, clip) => + Math.max(max, clip.position + (clip.end - clip.start)), 0); + }; + + // Delegate the mousedown event to the parent element for dynamically created resize-handle + element.on("mousedown", ".track-resize-handle", function(event) { + // Start resizing logic + isResizing = true; + startX = event.pageX; + startWidth = element.width(); + + // Calculate the minimum width based on the furthest right edge of clips + minimumWidth = getFurthestRightEdge() * scope.pixelsPerSecond; + + // Attach document-wide mousemove and mouseup events + $(document).on("mousemove", resizeTrack); + $(document).on("mouseup", stopResizing); + event.preventDefault(); + }); + + // Function to handle resizing as mouse moves + function resizeTrack(event) { + if (!isResizing) return; + + // Calculate the new width (ensure it doesn't go below the minimum width) + var newWidth = Math.max(startWidth + (event.pageX - startX), minimumWidth); + + // Update the track's new duration based on the resized width + newDuration = snapToFPSGridTime(scope, pixelToTime(scope, newWidth)); + + // Update the element's width dynamically + element.width(newWidth); + + // Apply the new duration to the scope + scope.$apply(function () { + scope.project.duration = newDuration; + }); } - // get track the item was dropped on - let drop_track = findTrackAtLocation(scope, parseInt(item_top, 10)); - if (drop_track != null) { - // find the item in the json data - let item_data = null; - if (item_type === "clip") { - item_data = findElement(scope.project.clips, "id", item_num); - } else if (item_type === "transition") { - item_data = findElement(scope.project.effects, "id", item_num); - } - - // set time diff (if not already determined) - if (position_diff === 0.0) { - // once calculated, we want to apply the exact same time diff to each clip/trans - position_diff = (item_left / scope.pixelsPerSecond) - item_data.position; - } + // Function to stop resizing when the mouse button is released + function stopResizing() { + if (!isResizing) {return;} + isResizing = false; - scope.$apply(function () { - //set track and position - item_data.layer = drop_track.number; - item_data.position += position_diff; - }); - scope.$apply(function () { - // Snap to FPS grid (must be done separately so Angular will actually refresh - // when extreme zooms are used (i.e. you drag a clip a partial frame, this causes it - // to jump back to it's original position correctly). - item_data.position = snapToFPSGridTime(scope, item_data.position); - }); + // Clean up the document-wide event listeners + $(document).off("mousemove", resizeTrack); + $(document).off("mouseup", stopResizing); - // Resize timeline if it's too small to contain all clips - scope.resizeTimeline(); - - // Keep track of dropped clips (we'll check for missing transitions in a sec) - dropped_clips.push(item_data); - - // Collect updates for later batch processing - if (item_type === "clip") { - clip_updates.push(item_data); - } else if (item_type === "transition") { - transition_updates.push(item_data); - } + // Finalize the new duration on the timeline (if valid) + if (newDuration !== null) { + timeline.resizeTimeline(newDuration); + } } - }); - - // Now fire all the Qt updates after all scope.$apply calls - // Update clips in Qt - clip_updates.forEach(function(item_data, index) { - var needs_refresh = (index === clip_updates.length - 1); - timeline.update_clip_data(JSON.stringify(item_data), true, true, !needs_refresh, tid); - }); - - // Update transitions in Qt - transition_updates.forEach(function(item_data, index) { - var needs_refresh = (index === transition_updates.length - 1); - timeline.update_transition_data(JSON.stringify(item_data), true, !needs_refresh, tid); - }); - - // Add missing transitions (if any) - if (dropped_clips.length === 1) { - // Hack to only add missing transitions if a single clip is being dropped - for (var clip_index = 0; clip_index < dropped_clips.length; clip_index++) { - var item_data = dropped_clips[clip_index]; - - // Check again for missing transitions - var missing_transition_details = scope.getMissingTransitions(item_data); - if (scope.Qt && missing_transition_details !== null) { - timeline.add_missing_transition(JSON.stringify(missing_transition_details)); - } - } - } - - // Clear dropped clips - dropped_clips = []; - - // Re-sort clips - scope.enable_sorting = true; - scope.sortItems(); } - }); - } - }; + }; }); diff --git a/src/timeline/js/directives/transition.js b/src/timeline/js/directives/transition.js index fe36b22cb..8c46cdde0 100644 --- a/src/timeline/js/directives/transition.js +++ b/src/timeline/js/directives/transition.js @@ -27,7 +27,7 @@ */ -/*global setSelections, setBoundingBox, moveBoundingBox, bounding_box */ +/*global setSelections, setBoundingBox, moveBoundingBox, bounding_box, updateDraggables */ // Init Variables var resize_disabled = false; var previous_drag_position = null; @@ -52,12 +52,13 @@ App.directive("tlTransition", function () { handles: "e, w", minWidth: 1, start: function (e, ui) { - scope.setDragging(true); - resize_disabled = false; - // Set selections setSelections(scope, element, $(this).attr("id")); + // Set dragging mode + scope.setDragging(true); + resize_disabled = false; + // Set bounding box setBoundingBox(scope, $(this), "trimming"); @@ -220,12 +221,13 @@ App.directive("tlTransition", function () { distance: 5, cancel: ".transition_menu, .point", start: function (event, ui) { - previous_drag_position = null; - scope.setDragging(true); - // Set selections setSelections(scope, element, $(this).attr("id")); + // Set dragging mode + previous_drag_position = null; + scope.setDragging(true); + // Store initial cursor vs draggable offset var elementOffset = $(this).offset(); var cursorOffset = { @@ -266,10 +268,11 @@ App.directive("tlTransition", function () { // Hide snapline (if any) scope.hideSnapline(); + // Call the shared function for drag stop + updateDraggables(scope, ui, "transition"); + // Clear previous drag position previous_drag_position = null; - scope.setDragging(false); - }, drag: function (e, ui) { // Retrieve the initial cursor offset @@ -313,18 +316,6 @@ App.directive("tlTransition", function () { } }); - }, - revert: function (valid) { - if (!valid) { - // The drop spot was invalid, so we're going to move all transitions to their original position - $(".ui-selected").each(function () { - var oldY = start_transitions[$(this).attr("id")]["top"]; - var oldX = start_transitions[$(this).attr("id")]["left"]; - - $(this).css("left", oldX); - $(this).css("top", oldY); - }); - } } }); diff --git a/src/timeline/js/functions.js b/src/timeline/js/functions.js index 0ef6306e4..094b8eb09 100644 --- a/src/timeline/js/functions.js +++ b/src/timeline/js/functions.js @@ -26,7 +26,7 @@ * along with OpenShot Library. If not, see . */ -/*global bounding_box, global_primes*/ +/*global bounding_box, global_primes, timeline*/ // Generate a UUID function uuidv4() { @@ -538,3 +538,130 @@ function forceDrawRuler() { document.querySelector("#scrolling_tracks").scrollLeft = 10; document.querySelector("#scrolling_tracks").scrollLeft = scroll; } + +// Update the clip/transition data on Draggable stop (replaces the Track droppable) +function updateDraggables(scope, ui, itemType) { + scope.enable_sorting = false; + + var scrolling_tracks = $("#scrolling_tracks"); + var vert_scroll_offset = scrolling_tracks.scrollTop(); + var horz_scroll_offset = scrolling_tracks.scrollLeft(); + + // Track each dropped clip or transition + var dropped_clips = []; + var position_diff = 0; // The time difference for multiple selections (if any) + var ui_selected = $(".ui-selected"); + + // Arrays to collect updates for batch processing + var clip_updates = []; + var transition_updates = []; + + // UUID to group these updates as a single transaction + var tid = uuidv4(); + + // Loop through each selected item and remove the selection if multiple items are selected + ui_selected.each(function (index) { + var item = $(this); + + // Determine the type of item (clip or transition) + var item_type = itemType || null; + if (item.hasClass("clip")) { + item_type = "clip"; + } else if (item.hasClass("transition")) { + item_type = "transition"; + } else { + // Unknown drop type, skip it + return; + } + + // Get the item properties + var item_id = item.attr("id"); + var item_num = item_id.substr(item_id.indexOf("_") + 1); + var item_left = item.position().left; + + // Adjust for scrollbars + item_left = parseFloat(item_left + horz_scroll_offset); + var item_top = parseFloat(item.position().top + vert_scroll_offset); + + // Prevent items from being dropped too far to the left + if (item_left < 0) { + item_left = 0; + } + + // Get the track where the item was dropped + let drop_track = findTrackAtLocation(scope, parseInt(item_top, 10)); + if (drop_track != null) { + // Find the item in the project JSON data + let item_data = null; + if (item_type === "clip") { + item_data = findElement(scope.project.clips, "id", item_num); + } else if (item_type === "transition") { + item_data = findElement(scope.project.effects, "id", item_num); + } + + // Set the time difference (if not already calculated) + if (position_diff === 0.0) { + position_diff = (item_left / scope.pixelsPerSecond) - item_data.position; + } + + scope.$apply(function () { + // Set track and position + item_data.layer = drop_track.number; + item_data.position += position_diff; + }); + + scope.$apply(function () { + // Snap to FPS grid (if necessary) + item_data.position = snapToFPSGridTime(scope, item_data.position); + }); + + // Keep track of dropped clips/transitions + dropped_clips.push(item_data); + + // Collect updates for batch processing + if (item_type === "clip") { + clip_updates.push(item_data); + } else if (item_type === "transition") { + transition_updates.push(item_data); + } + } + }); + + // Batch process updates + clip_updates.forEach(function(item_data, index) { + var needs_refresh = (index === clip_updates.length - 1); + timeline.update_clip_data(JSON.stringify(item_data), true, true, !needs_refresh, tid); + }); + + transition_updates.forEach(function(item_data, index) { + var needs_refresh = (index === transition_updates.length - 1); + timeline.update_transition_data(JSON.stringify(item_data), true, !needs_refresh, tid); + }); + + // Add missing transitions (if any) + if (dropped_clips.length === 1) { + for (var clip_index = 0; clip_index < dropped_clips.length; clip_index++) { + var item_data = dropped_clips[clip_index]; + + // Check for missing transitions + var missing_transition_details = scope.getMissingTransitions(item_data); + if (scope.Qt && missing_transition_details !== null) { + timeline.add_missing_transition(JSON.stringify(missing_transition_details)); + } + } + } + + // Clear dropped clips + dropped_clips = []; + + // Re-enable sorting and sort items + scope.enable_sorting = true; + scope.sortItems(); + scope.resizeTimeline(); + + // Delay clearing the dragging variable (to prevent an ng-click race condition + // which causes selections to be randomly cleared after a drag) + setTimeout(function() { + scope.setDragging(false); + }, 100); +} diff --git a/src/timeline/media/css/main.css b/src/timeline/media/css/main.css index b570a56e3..ba3d5587e 100644 --- a/src/timeline/media/css/main.css +++ b/src/timeline/media/css/main.css @@ -70,6 +70,7 @@ img { position: relative; line-height: 4px; height: 43px; + margin-right: 8px; /* Prevent playhead from becoming unaligned due to scrollbars */ } #scrolling_tracks { @@ -90,6 +91,7 @@ img { color: #999; padding-top: 12px; padding-left: 17px; + cursor: default; } #progress { @@ -122,6 +124,7 @@ img { font-size: 0.8em; position: absolute; transform: translate(-50%, 0); + cursor: default; } /* Tracks */ @@ -190,13 +193,30 @@ img { border-top: 1px solid #4b92ad; border-bottom: 1px solid #4b92ad; border-right: 1px solid #4b92ad; - border-top-right-radius: 8px; - border-bottom-right-radius: 8px; box-shadow: 0 0 10px #000; position: relative; z-index: 1; } +.track-resize-handle { + position: absolute; + right: -8px; /* To the right of the track right edge */ + top: -1px; + width: 8px; + height: 100%; + cursor: ew-resize; + background-color: #2c2c2c; + border-top: 1px solid #4b92ad; + border-bottom: 1px solid #4b92ad; + border-right: 1px solid #4b92ad; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; +} + +.track-resize-handle:hover { + background-color: #4C4C4CFF; +} + .banding-overlay { position: absolute; top: 0; diff --git a/src/windows/export.py b/src/windows/export.py index 55aeda5b3..b5e2b1c28 100644 --- a/src/windows/export.py +++ b/src/windows/export.py @@ -44,10 +44,10 @@ from PyQt5.QtCore import Qt, QCoreApplication, QTimer, QSize, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QMessageBox, QDialog, QFileDialog, QDialogButtonBox, QPushButton + QMessageBox, QDialog, QFileDialog, QDialogButtonBox, QPushButton, QWidget, QLineEdit, QComboBox, QSpinBox, QCheckBox ) from PyQt5.QtGui import QIcon - +from functools import partial from classes import info from classes import ui_util from classes import openshot_rc # noqa @@ -91,6 +91,9 @@ def __init__(self, *args, **kwargs): self.export_button = QPushButton(_('Export Video')) self.export_button.setObjectName("acceptButton") self.close_button = QPushButton(_('Done')) + self.restoring_defaults = False + self.restore_defaults_button.clicked.connect(self.restore_defaults) + self.buttonBox.addButton(self.close_button, QDialogButtonBox.RejectRole) self.buttonBox.addButton(self.export_button, QDialogButtonBox.AcceptRole) self.buttonBox.addButton(self.cancel_button, QDialogButtonBox.RejectRole) @@ -200,6 +203,8 @@ def __init__(self, *args, **kwargs): self.cboChannelLayout.currentIndexChanged.connect(self.updateChannels) self.ExportFrame.connect(self.updateProgressBar) self.btnBrowseProfiles.clicked.connect(self.btnBrowseProfiles_clicked) + self.checkStartFirstClip.toggled.connect(partial(self.updateFrameRate, True)) + self.checkEndLastClip.toggled.connect(partial(self.updateFrameRate, True)) # ********* Advanced Profile List ********** # Loop through profiles @@ -283,6 +288,27 @@ def __init__(self, *args, **kwargs): # Determine the length of the timeline (in frames) self.updateFrameRate() + # Load previous settings (if any) + self.load_settings() + + def restore_defaults(self): + """ + Restore defaults by closing and reopening the dialog. + """ + # Clear the saved settings + get_app().updates.ignore_history = True + get_app().updates.update(["export_settings"], None) + get_app().updates.ignore_history = False + + log.info("Cleared last-export_settings.") + + # Close the current dialog + self.restoring_defaults = True + self.close() + + # Re-open the export dialog (calling the dialog anew) + QTimer.singleShot(0, get_app().window.actionExportVideo.trigger) + def getProfilePath(self, profile_name): """Get the profile path that matches the name""" for profile, path in self.profile_paths.items(): @@ -344,12 +370,23 @@ def updateFrameRate(self, set_limits=True): self.timeline.info.has_audio = True if set_limits: - # Determine max frame (based on clips) - self.timeline_length_int = self.timeline.GetMaxFrame() + if self.checkEndLastClip.isChecked(): + # Set end frame to last clip (right edge) + timeline_length_int = self.timeline.GetMaxFrame() + else: + # Set end frame to project length (full timeline) + timeline_length_int = self.timeline.info.video_length + + if self.checkStartFirstClip.isChecked(): + # Set the start frame to the first clip position + timeline_start_int = self.timeline.GetMinFrame() + else: + # Set the start frame to the start of the project (0.0) + timeline_start_int = 1 # Set the min and max frame numbers for this project - self.txtStartFrame.setValue(1) - self.txtEndFrame.setValue(self.timeline_length_int) + self.txtStartFrame.setValue(timeline_start_int) + self.txtEndFrame.setValue(timeline_length_int) # Calculate differences between editing/preview FPS and export FPS current_fps = get_app().project.get("fps") @@ -769,6 +806,8 @@ def enableControls(self): def accept(self): """ Start exporting video """ + # Save export settings + self.save_settings() # Build the export window title def titlestring(sec, fps, mess): @@ -1132,7 +1171,70 @@ def titlestring(sec, fps, mess): # Accept dialog super(Export, self).accept() + def save_settings(self): + if self.restoring_defaults: + return # Ignore saving if we are actively restoring defaults + + # Create a list to store the settings in order + settings = [] + + # Iterate over all children in the dialog in the order they are defined + for child in self.findChildren(QWidget): + if child.objectName().startswith("qt_"): + continue + setting = {} + if isinstance(child, QLineEdit): + setting['name'] = child.objectName() + setting['type'] = 'QLineEdit' + setting['value'] = child.text() + elif isinstance(child, QComboBox): + setting['name'] = child.objectName() + setting['type'] = 'QComboBox' + setting['value'] = child.currentIndex() + elif isinstance(child, QSpinBox): + setting['name'] = child.objectName() + setting['type'] = 'QSpinBox' + setting['value'] = child.value() + elif isinstance(child, QCheckBox): + setting['name'] = child.objectName() + setting['type'] = 'QCheckBox' + setting['value'] = child.isChecked() + # Append the setting to the list + if setting: + settings.append(setting) + + # Save all settings as a JSON string + get_app().updates.ignore_history = True + get_app().updates.update(["export_settings"], settings) + get_app().updates.ignore_history = False + + log.info("Export settings saved: %s", settings) + + def load_settings(self): + # Load the JSON string from settings + settings = get_app().project.get("export_settings") + + if not settings: + log.info("No saved settings found.") + return + + # Iterate over the list of settings and apply them in order + for setting in settings: + widget = self.findChild(QWidget, setting['name']) + if widget: + if setting['type'] == 'QLineEdit': + widget.setText(setting.get('value', '')) + elif setting['type'] == 'QComboBox': + widget.setCurrentIndex(setting.get('value', 0)) + elif setting['type'] in ['QSpinBox', 'QDoubleSpinBox']: + widget.setValue(setting.get('value', widget.minimum())) + elif setting['type'] == 'QCheckBox': + widget.setChecked(setting.get('value', False)) + log.info("Export settings loaded: %s", settings) + def reject(self): + self.save_settings() + if self.exporting and not self.close_button.isVisible(): # Show confirmation dialog _ = get_app()._tr diff --git a/src/windows/main_window.py b/src/windows/main_window.py index f82ab40af..986dbff0d 100644 --- a/src/windows/main_window.py +++ b/src/windows/main_window.py @@ -2161,6 +2161,9 @@ def actionRemoveMarker_trigger(self): # Remove track m.delete() + def actionZoomToTimeline(self): + self.sliderZoomWidget.zoomToTimeline() + def actionTimelineZoomIn_trigger(self): self.sliderZoomWidget.zoomIn() diff --git a/src/windows/preview_thread.py b/src/windows/preview_thread.py index 3988e7432..d7823af80 100644 --- a/src/windows/preview_thread.py +++ b/src/windows/preview_thread.py @@ -45,8 +45,7 @@ def changed(self, action): """ This method is invoked by the UpdateManager each time a change happens (i.e UpdateInterface) """ # Ignore changes that don't affect libopenshot - if action and len(action.key) >= 1 and action.key[0].lower() in ["files", "history", "markers", "layers", - "scale", "profile", "sample_rate"]: + if action and len(action.key) >= 1 and action.key[0].lower() in ["files", "history", "markers", "layers", "scale", "profile", "sample_rate", "export_settings"]: return try: diff --git a/src/windows/ui/export.ui b/src/windows/ui/export.ui index 086efc1c1..996116f61 100644 --- a/src/windows/ui/export.ui +++ b/src/windows/ui/export.ui @@ -40,12 +40,36 @@ - - - - QDialogButtonBox::NoButton - - + + + + + + Restore Defaults + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + QDialogButtonBox::NoButton + + + + @@ -277,7 +301,7 @@ 0 0 480 - 122 + 119 @@ -345,6 +369,13 @@ + + + + Start at First Clip + + + @@ -378,6 +409,16 @@ + + + + End at Last Clip + + + true + + + @@ -387,8 +428,8 @@ 0 0 - 466 - 269 + 263 + 262 @@ -678,8 +719,8 @@ 0 0 - 480 - 68 + 151 + 47 @@ -713,8 +754,8 @@ 0 0 - 480 - 120 + 151 + 117 @@ -787,6 +828,14 @@ + + + 0 + 0 + 212 + 189 + + Audio Settings diff --git a/src/windows/views/timeline.py b/src/windows/views/timeline.py index 7fadd0cee..a00739318 100644 --- a/src/windows/views/timeline.py +++ b/src/windows/views/timeline.py @@ -3135,13 +3135,8 @@ def ScrollbarChanged(self, new_positions): def resizeTimeline(self, new_duration): """Resize the duration of the timeline""" log.debug(f"Changing timeline to length: {new_duration}") - duration_diff = abs(get_app().project.get('duration') - new_duration) - if (duration_diff > 1.0): - log.debug("Updating duration") - get_app().updates.update_untracked(["duration"], new_duration) - get_app().window.TimelineResize.emit() - else: - log.debug("Duration unchanged. Not updating") + get_app().updates.update_untracked(["duration"], new_duration) + get_app().window.TimelineResize.emit() # Add Transition def addTransition(self, file_path, position, track): diff --git a/src/windows/views/zoom_slider.py b/src/windows/views/zoom_slider.py index 333367a3d..8a607e703 100644 --- a/src/windows/views/zoom_slider.py +++ b/src/windows/views/zoom_slider.py @@ -24,6 +24,8 @@ You should have received a copy of the GNU General Public License along with OpenShot Library. If not, see . """ +import copy +import math from PyQt5.QtCore import ( Qt, QCoreApplication, QRectF, QTimer @@ -157,12 +159,10 @@ def paintEvent(self, event, *args): layers = Track.filter() # Wait for timeline object and valid scrollbar positions - # TODO: Fix commented out logic - if get_app().window.timeline: # and self.scrollbar_position[2] != 0.0: + if get_app().window.timeline: # Get max width of timeline project_duration = get_app().project.get("duration") pixels_per_second = event.rect().width() / project_duration - project_pixel_width = max(0, project_duration * pixels_per_second) scroll_width = (self.scrollbar_position[1] - self.scrollbar_position[0]) * event.rect().width() # Get FPS info @@ -217,7 +217,8 @@ def paintEvent(self, event, *args): painter.fillPath(left_handle_path, handle_color) # right handle - right_handle_x = (self.scrollbar_position[1] * event.rect().width()) - (handle_width/2.0) + right_handle_x = self.scroll_bar_rect.right() - (handle_width/2.0) + right_handle_x = min(right_handle_x, event.rect().width() - (handle_width/2.0)) self.right_handle_rect = QRectF(right_handle_x, event.rect().height() / 4.0, handle_width, event.rect().height() / 2.0) right_handle_path = QPainterPath() right_handle_path.addRoundedRect(self.right_handle_rect, handle_width, handle_width) @@ -231,6 +232,25 @@ def paintEvent(self, event, *args): # End painter painter.end() + def zoomToTimeline(self): + """Toggle between zooming to the entire timeline and the previous zoom""" + # Are we already zoomed complete out? + if math.isclose(self.scrollbar_position[0], 0.0, abs_tol=1e-9) and math.isclose(self.scrollbar_position[1], 1.0, abs_tol=1e-9): + # Restore previous zoom + self.scrollbar_position[0] = self.scrollbar_zoom_previous[0] + self.scrollbar_position[1] = self.scrollbar_zoom_previous[1] + else: + # Zoom out to reveal the entire timeline + self.scrollbar_zoom_previous = copy.deepcopy(self.scrollbar_position) + self.scrollbar_position[0] = 0.0 + self.scrollbar_position[1] = 1.0 + self.delayed_resize_callback() + + def mouseDoubleClickEvent(self, event): + self.zoomToTimeline() + self.mouse_dragging = True # Prevent mouseReleaseEvent from moving selection + event.accept() + def mousePressEvent(self, event): """Capture mouse press event""" event.accept() @@ -249,10 +269,27 @@ def mouseReleaseEvent(self, event): click_pos = event.pos().x() / self.width() selection_width = self.scrollbar_position[1] - self.scrollbar_position[0] half_width = selection_width / 2 - new_left_pos = max(0.0, click_pos - half_width) - new_right_pos = min(1.0, click_pos + half_width) - self.scrollbar_position = [new_left_pos, new_right_pos, self.scrollbar_position[2], - self.scrollbar_position[3]] + + # Calculate new left / right handles + new_left_pos = click_pos - half_width + new_right_pos = click_pos + half_width + + # If the new left position is less than 0, adjust both sides to fit within bounds + if new_left_pos < 0.0: + diff = -new_left_pos + new_left_pos = 0.0 + new_right_pos = min(1.0, new_right_pos + diff) + + # If the new right position is greater than 1, adjust both sides to fit within bounds + if new_right_pos > 1.0: + diff = new_right_pos - 1.0 + new_right_pos = 1.0 + new_left_pos = max(0.0, new_left_pos - diff) + + # Update the scrollbar position to the newly calculated values + self.scrollbar_position = [new_left_pos, new_right_pos, self.scrollbar_position[2], self.scrollbar_position[3]] + + # Trigger the resize and update self.delayed_resize_timer.start() self.update() @@ -412,6 +449,19 @@ def resizeEvent(self, event): self.delayed_size = self.size() self.delayed_resize_timer.start() + def get_scroll_width(self): + """Calculate the width of the scrollbar handle (i.e. selection width)""" + # Get max width of timeline + project_duration = get_app().project.get("duration") + + # Calculate scroll bar / selection width + timeline_pixels_per_second = 100.0 / get_app().project.get("scale") + timeline_project_width = project_duration * timeline_pixels_per_second + scroll_ratio = self.scrollbar_position[3] / timeline_project_width + scroll_width = scroll_ratio * self.width() + scroll_width = min(scroll_width, self.width()) + return scroll_width, scroll_ratio + def delayed_resize_callback(self): """Callback for resize event timer (to delay the resize event, and prevent lots of similar resize events)""" # Get max width of timeline @@ -437,14 +487,23 @@ def wheelEvent(self, event): # Repaint widget on zoom self.repaint() - def setZoomFactor(self, zoom_factor): + def setZoomFactor(self, zoom_factor, center=False): """Set the current zoom factor""" # Force recalculation of clips self.zoom_factor = zoom_factor # Emit zoom signal get_app().window.TimelineZoom.emit(self.zoom_factor) - get_app().window.TimelineCenter.emit() + if center: + get_app().window.TimelineCenter.emit() + + # Prevent scrollbars from exceeding 100% of zoomslider + # This is caused by the scrollbars stopping events once zoomed out too much + scroll_width, scroll_ratio = self.get_scroll_width() + if scroll_ratio >= 1.0 and self.scrollbar_position: + # Set to left/right edge - max + self.scrollbar_position[0] = 0.0 + self.scrollbar_position[1] = 1.0 # Force re-paint self.repaint() @@ -459,7 +518,7 @@ def zoomIn(self): new_factor = self.zoom_factor * 0.8 # Emit zoom signal - self.setZoomFactor(new_factor) + self.setZoomFactor(new_factor, center=True) def zoomOut(self): """Zoom out of timeline""" @@ -472,7 +531,7 @@ def zoomOut(self): new_factor = min(self.zoom_factor * 1.25, 4.0) # Emit zoom signal - self.setZoomFactor(new_factor) + self.setZoomFactor(new_factor, center=True) def update_scrollbars(self, new_positions): """Consume the current scroll bar positions from the webview timeline""" @@ -496,6 +555,11 @@ def handle_selection(self): self.changed(None) self.repaint() + def timeline_resized(self): + # Force recalculation of clips and repaint + self.repaint() + self.delayed_resize_timer.start() + def update_playhead_pos(self, currentFrame): """Callback when position is changed""" self.current_frame = currentFrame @@ -538,6 +602,7 @@ def __init__(self, *args): self.zoom_factor = 15.0 self.scrollbar_position = [0.0, 0.0, 0.0, 0.0] self.scrollbar_position_previous = [0.0, 0.0, 0.0, 0.0] + self.scrollbar_zoom_previous = [0.0, 0.2, 0.0, 0.0] self.left_handle_rect = QRectF() self.left_handle_dragging = False self.right_handle_rect = QRectF() @@ -573,7 +638,7 @@ def __init__(self, *args): # Connect zoom functionality self.win.TimelineScrolled.connect(self.update_scrollbars) - self.win.TimelineResize.connect(self.delayed_resize_callback) + self.win.TimelineResize.connect(self.timeline_resized) self.win.IgnoreUpdates.connect(self.ignore_updates_callback) # Connect Selection signals