From 68fd550de6f03a7747c711d047bf4410cd52dc91 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Wed, 9 Oct 2024 16:55:23 -0500 Subject: [PATCH] Adding new Profile Editor. New context menu options to Edit, Duplicate, and Delete custom profiles. Updated translations (new strings). Modified "Choose Profile" to apply a new profile, even if it's already applied (i.e. might have been edited now). Added new methods for inserting / updating / removing rows from the Profile model / view. Fixed small bug on the Export screen, to restore start/end frame correctly. --- src/classes/query.py | 2 +- src/language/OpenShot/OpenShot.pot | 200 ++++++++----- src/windows/export.py | 7 + src/windows/main_window.py | 36 ++- src/windows/models/profiles_model.py | 126 +++++--- src/windows/profile.py | 13 +- src/windows/profile_edit.py | 200 +++++++++++++ src/windows/ui/profile-edit.ui | 385 +++++++++++++++++++++++++ src/windows/views/files_listview.py | 6 +- src/windows/views/files_treeview.py | 6 +- src/windows/views/profiles_treeview.py | 37 ++- 11 files changed, 890 insertions(+), 128 deletions(-) create mode 100644 src/windows/profile_edit.py create mode 100644 src/windows/ui/profile-edit.ui diff --git a/src/classes/query.py b/src/classes/query.py index a02335ba67..f5b66bfdfd 100644 --- a/src/classes/query.py +++ b/src/classes/query.py @@ -285,7 +285,7 @@ def profile(self): "num": d.get("fps", {}).get("num", 1), }, "height": d.get("height", 1), - "interlaced_frame": d.get("interlaced_frame", False), + "progressive": not d.get("interlaced_frame", False), "pixel_format": d.get("pixel_format", None), "pixel_ratio": { diff --git a/src/language/OpenShot/OpenShot.pot b/src/language/OpenShot/OpenShot.pot index 0b3224f403..1e184f11f8 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-30 16:30:18.199948\n" +"POT-Creation-Date: 2024-10-09 16:12:14.191928\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" @@ -76,9 +76,9 @@ msgid "" msgstr "" #: /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:2562 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:660 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:758 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2606 #: /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" @@ -157,7 +157,9 @@ msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/export.py:489 #: /home/jonathan/apps/openshot-qt/src/windows/file_properties.py:161 -#: libopenshot (Clip Properties) +#: /home/jonathan/apps/openshot-qt/src/windows/profile_edit.py:54 +#: /home/jonathan/apps/openshot-qt/src/windows/profile_edit.py:68 libopenshot +#: (Clip Properties) msgid "No" msgstr "" @@ -286,7 +288,7 @@ msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/views/properties_tableview.py:700 #: /home/jonathan/apps/openshot-qt/src/windows/views/timeline.py:149 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2141 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2185 #: /home/jonathan/apps/openshot-qt/src/windows/models/properties_model.py:784 #: /home/jonathan/apps/openshot-qt/src/windows/models/properties_model.py:873 #: /home/jonathan/apps/openshot-qt/src/windows/add_to_timeline.py:480 @@ -960,6 +962,36 @@ msgstr "" msgid "Reverse Transition" msgstr "" +#: /home/jonathan/apps/openshot-qt/src/windows/views/files_listview.py:87 +#: /home/jonathan/apps/openshot-qt/src/windows/views/files_treeview.py:90 +#: Settings for actionProfile +#: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1281 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1284 +msgid "Choose Profile" +msgstr "" + +#: /home/jonathan/apps/openshot-qt/src/windows/views/files_listview.py:98 +#: /home/jonathan/apps/openshot-qt/src/windows/views/files_treeview.py:101 +#: /home/jonathan/apps/openshot-qt/src/windows/profile_edit.py:50 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/profile-edit.ui:14 +msgid "Create Profile" +msgstr "" + +#: /home/jonathan/apps/openshot-qt/src/windows/views/profiles_treeview.py:90 +msgid "Edit" +msgstr "" + +#: /home/jonathan/apps/openshot-qt/src/windows/views/profiles_treeview.py:94 +#: Settings for actionDuplicate +#: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1554 +#: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1557 +msgid "Duplicate" +msgstr "" + +#: /home/jonathan/apps/openshot-qt/src/windows/views/profiles_treeview.py:100 +msgid "Delete" +msgstr "" + #: /home/jonathan/apps/openshot-qt/src/windows/views/find_file.py:64 #, python-format msgid "Missing File (%s)" @@ -1106,198 +1138,207 @@ msgstr "" msgid "Please restart OpenShot for all preferences to take effect." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:145 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:308 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:528 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:626 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:147 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:310 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:530 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:628 msgid "Unsaved Changes" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:146 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:148 msgid "Save changes to project before closing?" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:238 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:240 msgid "Backup Recovered" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:239 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:241 msgid "Your most recent unsaved project has been recovered." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:309 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:529 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:627 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:311 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:531 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:629 msgid "Save changes to project first?" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:498 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:500 msgid "Error Saving Project" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:572 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:574 #, python-format msgid "" "Project %s is missing (it may have been moved or deleted). It has been " "removed from the Recent Projects menu." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:585 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:587 msgid "Error Opening Project" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:639 Settings for +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:641 Settings for #: actionOpen /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:534 msgid "Open Project..." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:641 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:664 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:766 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:643 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:666 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:768 msgid "OpenShot Project (*.osp)" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:662 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:664 msgid "Save Project..." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:764 Settings for +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:766 Settings for #: actionSaveAs #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:570 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:573 msgid "Save Project As..." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:787 Settings for +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:789 Settings for #: actionImportFiles #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:595 msgid "Import Files..." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:820 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:822 #, python-format msgid "%s is not a valid video, audio, or image file." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:839 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:841 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:610 msgid "Import Image Sequence" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:840 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:842 #, python-format msgid "Would you like to import %s as an image sequence?" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1184 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1192 msgid "Save Frame..." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1184 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1192 msgid "Image files (*.png)" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1188 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1196 msgid "Save Frame cancelled..." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1226 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1234 #, python-format msgid "Saved Frame to %s" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1228 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1236 #, python-format msgid "Failed to save image to %s" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1435 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1436 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1443 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1444 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:826 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:829 msgid "Disable Snapping" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1438 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1439 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1446 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1447 msgid "Enable Snapping" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1449 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1450 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1457 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1458 msgid "Disable Razor" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1452 -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1453 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1460 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1461 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:808 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:811 msgid "Enable Razor" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2072 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1768 +#: /home/jonathan/apps/openshot-qt/src/windows/profile_edit.py:156 +msgid "Error" +msgstr "" + +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:1768 +msgid "You can not delete the current profile." +msgstr "" + +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2116 msgid "Error Removing Track" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2072 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2116 msgid "You must keep at least 1 track" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2143 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2187 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1440 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1443 msgid "Rename Track" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2143 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2187 msgid "Track Name:" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2321 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2365 msgid "Docks" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2515 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2559 msgid "Enter caption text..." msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2733 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2777 msgid "Recent Projects" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2742 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2786 msgid "No Recent Projects" msgstr "" -#: /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/main_window.py:2858 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2876 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2886 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3514 #: /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:2857 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2901 msgid "Caption Toolbar" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2929 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2973 #: /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:2930 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:2974 #, python-format msgid "Update Available: %s" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3367 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3471 msgid "Error starting local HTTP server" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3368 +#: /home/jonathan/apps/openshot-qt/src/windows/main_window.py:3472 msgid "Failed multiple attempts to start server:" msgstr "" @@ -1315,7 +1356,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:184 +#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:185 msgid "Name" msgstr "" @@ -1435,16 +1476,16 @@ msgstr "" msgid "SAR" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:184 +#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:185 msgid "Tags" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:377 +#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:378 #, python-format msgid "Importing %(count)d / %(total)d" msgstr "" -#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:410 +#: /home/jonathan/apps/openshot-qt/src/windows/models/files_model.py:411 #, python-format msgid "Imported %(count)d files" msgstr "" @@ -1518,7 +1559,10 @@ msgid "Unknown" msgstr "" #: /home/jonathan/apps/openshot-qt/src/windows/file_properties.py:160 -#: libopenshot (Clip Properties) +#: /home/jonathan/apps/openshot-qt/src/windows/profile_edit.py:53 +#: /home/jonathan/apps/openshot-qt/src/windows/profile_edit.py:68 +#: /home/jonathan/apps/openshot-qt/src/windows/profile_edit.py:133 libopenshot +#: (Clip Properties) msgid "Yes" msgstr "" @@ -1678,6 +1722,14 @@ msgstr "" msgid "OpenShot on GitHub" msgstr "" +#: /home/jonathan/apps/openshot-qt/src/windows/profile_edit.py:48 +msgid "Duplicate Profile" +msgstr "" + +#: /home/jonathan/apps/openshot-qt/src/windows/profile_edit.py:156 +msgid "Please enter a description for this profile." +msgstr "" + #: /home/jonathan/apps/openshot-qt/src/classes/exporters/edl.py:59 msgid "Export EDL..." msgstr "" @@ -2943,12 +2995,6 @@ msgstr "" msgid "Animated Title" msgstr "" -#: Settings for actionProfile -#: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1281 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1284 -msgid "Choose Profile" -msgstr "" - #: Settings for actionDetailsView #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1149 #: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1152 @@ -3145,12 +3191,6 @@ msgstr "" msgid "Preview File" msgstr "" -#: Settings for actionDuplicate -#: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1554 -#: /home/jonathan/apps/openshot-qt/src/windows/ui/main-window.ui:1557 -msgid "Duplicate" -msgstr "" - #: Settings for actionClearAllCache msgid "Clear All Cache" msgstr "" @@ -3912,6 +3952,14 @@ msgstr "" msgid "%s: Initialize Effect" msgstr "" +#: /home/jonathan/apps/openshot-qt/src/windows/ui/profile-edit.ui:41 +msgid "*/.openshot_qt/profiles/*" +msgstr "" + +#: /home/jonathan/apps/openshot-qt/src/windows/ui/profile-edit.ui:126 +msgid "Description:" +msgstr "" + #: /home/jonathan/apps/openshot-qt/src/windows/ui/profile.ui:25 msgid "Filter Profiles" msgstr "" diff --git a/src/windows/export.py b/src/windows/export.py index b5e2b1c281..982766c422 100644 --- a/src/windows/export.py +++ b/src/windows/export.py @@ -1230,6 +1230,13 @@ def load_settings(self): widget.setValue(setting.get('value', widget.minimum())) elif setting['type'] == 'QCheckBox': widget.setChecked(setting.get('value', False)) + + # Update start / end frame after loading settings + if self.checkStartFirstClip.isChecked(): + self.updateFrameRate(True) + if self.checkEndLastClip.isChecked(): + self.updateFrameRate(True) + log.info("Export settings loaded: %s", settings) def reject(self): diff --git a/src/windows/main_window.py b/src/windows/main_window.py index e0edb7a6cf..d66772b7a0 100644 --- a/src/windows/main_window.py +++ b/src/windows/main_window.py @@ -1753,6 +1753,38 @@ def initShortcuts(self): # Log shortcut initialization completion log.debug("Shortcuts initialized or updated.") + def actionProfileEdit_trigger(self, profile=None, duplicate=False, delete=False, parent=None): + # Show profile edit dialog + from windows.profile_edit import EditProfileDialog + log.debug("Showing profile edit dialog") + + # get translations + _ = get_app()._tr + + if profile and delete and parent: + # Delete profile (no dialog) + if os.path.exists(profile.path): + if profile.info.description == get_app().project.get(['profile']): + QMessageBox.warning(parent, _("Error"), _("You can not delete the current profile.")) + return + + log.info(f"Removing custom profile: {profile.path}") + os.unlink(profile.path) + parent.profiles_model.remove_row(profile) + else: + # Show edit dialog + win = EditProfileDialog(profile, duplicate) + result = win.exec_() + if result == QDialog.Accepted: + profile = win.profile + if parent: + # Update model and refresh view + parent.profiles_model.update_or_insert_row(profile) + parent.refresh_view(parent.parent.txtProfileFilter.text()) + else: + # Choose the edited profile + self.actionProfile_trigger(profile) + def actionProfile_trigger(self, profile=None): # Disable video caching openshot.Settings.Instance().ENABLE_PLAYBACK_CACHING = False @@ -1770,11 +1802,11 @@ def actionProfile_trigger(self, profile=None): result = win.exec_() profile = win.selected_profile else: - # Profile passed in alraedy + # Profile passed in already result = QDialog.Accepted # Update profile (if changed) - if result == QDialog.Accepted and profile and profile.info.description != get_app().project.get(['profile']): + if result == QDialog.Accepted and profile: proj = get_app().project # Group transactions diff --git a/src/windows/models/profiles_model.py b/src/windows/models/profiles_model.py index 1b24dc41d4..3e1b1ae8d8 100644 --- a/src/windows/models/profiles_model.py +++ b/src/windows/models/profiles_model.py @@ -80,42 +80,96 @@ def update_model(self, filter=None, clear=True): ): continue - row = [] - flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled - - item = QStandardItem(f"{profile.Key()}") - item.setData(profile, Qt.UserRole) - row.append(item) - - item = QStandardItem(f"{profile.info.description}") - item.setFlags(flags) - row.append(item) - - item = QStandardItem(f"{profile.info.width}") - item.setFlags(flags) - row.append(item) - - item = QStandardItem(f"{profile.info.height}") - item.setFlags(flags) - row.append(item) - - fps_string = f"{(profile.info.fps.num / profile.info.fps.den):.2f}" - if profile.info.fps.den == 1: - fps_string = f"{int(profile.info.fps.num / profile.info.fps.den)}" - - item = QStandardItem(fps_string) - item.setFlags(flags) - row.append(item) - - item = QStandardItem(f"{profile.info.display_ratio.num}:{profile.info.display_ratio.den}") - item.setFlags(flags) - row.append(item) - - item = QStandardItem(f"{profile.info.pixel_ratio.num}:{profile.info.pixel_ratio.den}") - item.setFlags(flags) - row.append(item) - - self.model.appendRow(row) + self._insert_row(profile) + + def update_or_insert_row(self, profile): + """ + Updates an existing row if a profile with the same key exists, + otherwise inserts a new row. + """ + # Find if the profile already exists in the model by key + existing_index = None + for row in range(self.model.rowCount()): + index = self.model.index(row, 0) # Assuming key is in column 0 + if index.data(Qt.UserRole) == profile: + existing_index = index + break + + if existing_index: + # Update existing row + self._update_row(existing_index.row(), profile) + else: + # Insert new row + self._insert_row(profile) + + def remove_row(self, profile): + """ + Removes an existing row if a profile with the same key exists. + """ + # Find if the profile already exists in the model by key + for row in range(self.model.rowCount()): + index = self.model.index(row, 0) # Assuming key is in column 0 + if index.data(Qt.UserRole) == profile: + # Remove the row from the model + self.model.removeRow(row) + break + + def _update_row(self, row, profile): + """Update the data in an existing row.""" + self.model.setData(self.model.index(row, 0), profile.Key(), Qt.DisplayRole) + self.model.setData(self.model.index(row, 0), profile, Qt.UserRole) + + self.model.setData(self.model.index(row, 1), profile.info.description, Qt.DisplayRole) + self.model.setData(self.model.index(row, 2), profile.info.width, Qt.DisplayRole) + self.model.setData(self.model.index(row, 3), profile.info.height, Qt.DisplayRole) + + fps_string = f"{(profile.info.fps.num / profile.info.fps.den):.2f}" + if profile.info.fps.den == 1: + fps_string = f"{int(profile.info.fps.num / profile.info.fps.den)}" + self.model.setData(self.model.index(row, 4), fps_string, Qt.DisplayRole) + + self.model.setData(self.model.index(row, 5), f"{profile.info.display_ratio.num}:{profile.info.display_ratio.den}", Qt.DisplayRole) + self.model.setData(self.model.index(row, 6), f"{profile.info.pixel_ratio.num}:{profile.info.pixel_ratio.den}", Qt.DisplayRole) + + def _insert_row(self, profile): + """Insert a new row into the model.""" + row = [] + + flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled + + item = QStandardItem(f"{profile.Key()}") + item.setData(profile, Qt.UserRole) + row.append(item) + + item = QStandardItem(f"{profile.info.description}") + item.setFlags(flags) + row.append(item) + + item = QStandardItem(f"{profile.info.width}") + item.setFlags(flags) + row.append(item) + + item = QStandardItem(f"{profile.info.height}") + item.setFlags(flags) + row.append(item) + + fps_string = f"{(profile.info.fps.num / profile.info.fps.den):.2f}" + if profile.info.fps.den == 1: + fps_string = f"{int(profile.info.fps.num / profile.info.fps.den)}" + + item = QStandardItem(fps_string) + item.setFlags(flags) + row.append(item) + + item = QStandardItem(f"{profile.info.display_ratio.num}:{profile.info.display_ratio.den}") + item.setFlags(flags) + row.append(item) + + item = QStandardItem(f"{profile.info.pixel_ratio.num}:{profile.info.pixel_ratio.den}") + item.setFlags(flags) + row.append(item) + + self.model.appendRow(row) def __init__(self, profiles, *args): diff --git a/src/windows/profile.py b/src/windows/profile.py index a49afdebaf..6d9d22e122 100644 --- a/src/windows/profile.py +++ b/src/windows/profile.py @@ -94,6 +94,11 @@ def __init__(self, initial_profile_desc=None): try: # Load Profile profile = openshot.Profile(profile_path) + if profile_folder == info.USER_PROFILES_PATH: + profile.path = profile_path + profile.user_created = True + else: + profile.user_created = False if profile.info.description == initial_profile_desc or profile.Key() == initial_profile_desc: self.project_profile = profile self.project_index = len(self.profile_list) @@ -108,7 +113,7 @@ def __init__(self, initial_profile_desc=None): log.error("Failed to parse file '%s' as a profile: %s" % (profile_path, e)) # Create treeview - self.profileListView = ProfilesTreeView(self.profile_list) + self.profileListView = ProfilesTreeView(self, self.profile_list) self.profileListView.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) self.verticalLayout.insertWidget(1, self.profileListView) @@ -133,10 +138,10 @@ def profileDoubleClick(self): def accept(self): """ Ok button clicked """ - # Get selected profile (if any, and if different than current project) + # Get selected profile profile = self.profileListView.get_profile() - if profile and profile.info.description != get_app().project.get(['profile']): - # New profile selected (different than current project) + if profile: + # New profile selected self.selected_profile = profile super(Profile, self).accept() else: diff --git a/src/windows/profile_edit.py b/src/windows/profile_edit.py new file mode 100644 index 0000000000..377ead23f6 --- /dev/null +++ b/src/windows/profile_edit.py @@ -0,0 +1,200 @@ +""" + @file + @brief This file loads the profile editor (duplicate and edit profiles) + @author Jonathan Thomas + + @section LICENSE + + Copyright (c) 2008-2024 OpenShot Studios, LLC + (http://www.openshotstudios.com). This file is part of + OpenShot Video Editor (http://www.openshot.org), an open-source project + dedicated to delivering high quality video editing and animation solutions + to the world. + + OpenShot Video Editor is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenShot Video Editor is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OpenShot Library. If not, see . + """ + +import os + +import openshot +from PyQt5.QtWidgets import QDialog, QMessageBox +from classes import ui_util, info +from classes.app import get_app +from classes.logger import log + + +class EditProfileDialog(QDialog): + """ Edit Profile Dialog """ + + # Path to ui file + ui_path = os.path.join(info.PATH, 'windows', 'ui', 'profile-edit.ui') + + def __init__(self, profile, duplicate): + super(EditProfileDialog, self).__init__() + + # Make copy of profile + self.original_profile = profile.Json() + self.profile = profile + self.duplicate = duplicate + if duplicate: + # Clear reference to original profile + self.profile = openshot.Profile() + self.profile.SetJson(self.original_profile) + + # Save initial file path (if editing, this needs to be removed) + self.existing_path = None + if hasattr(profile, "path"): + self.existing_path = profile.path + + # Load UI from designer & init + ui_util.load_ui(self, self.ui_path) + ui_util.init_ui(self) + + # Populate fields from profile + self.initialize() + + def initialize(self): + """Initialize the form fields with data from the profile.""" + # get translations + _ = get_app()._tr + + # Update windows title + if self.duplicate: + self.setWindowTitle(_('Duplicate Profile')) + else: + self.setWindowTitle(_('Create Profile')) + + # Add options to cboInterlaced dropdown + self.cboInterlaced.addItem(_('Yes')) + self.cboInterlaced.addItem(_('No')) + + # Connect all signals + self.connect_signals() + + self.txtProfileName.setText(self.profile.info.description) + self.txtWidth.setValue(self.profile.info.width) + self.txtHeight.setValue(self.profile.info.height) + self.txtAspectRatioNum.setValue(self.profile.info.display_ratio.num) + self.txtAspectRatioDen.setValue(self.profile.info.display_ratio.den) + self.txtPixelRatioNum.setValue(self.profile.info.pixel_ratio.num) + self.txtPixelRatioDen.setValue(self.profile.info.pixel_ratio.den) + self.txtFrameRateNum.setValue(self.profile.info.fps.num) + self.txtFrameRateDen.setValue(self.profile.info.fps.den) + self.cboInterlaced.setCurrentText(_('Yes') if self.profile.info.interlaced_frame == 1 else _('No')) + + def connect_signals(self): + """Connect input fields to update profile on change.""" + self.txtProfileName.textChanged.connect(self.update_profile_description) + self.txtWidth.valueChanged.connect(self.update_profile_width) + self.txtHeight.valueChanged.connect(self.update_profile_height) + self.txtPixelRatioNum.valueChanged.connect(self.update_profile_pixel_ratio) + self.txtPixelRatioDen.valueChanged.connect(self.update_profile_pixel_ratio) + self.txtFrameRateNum.valueChanged.connect(self.update_profile_frame_rate_num) + self.txtFrameRateDen.valueChanged.connect(self.update_profile_frame_rate_den) + self.cboInterlaced.currentTextChanged.connect(self.update_profile_interlaced) + self.txtProfileName.setFocus() + + # Handlers for updating profile + def update_profile_description(self, value): + self.profile.info.description = value + self.update_profile_path() + + def update_profile_width(self, value): + self.profile.info.width = value + self.update_display_aspect_ratio() + self.update_profile_path() + + def update_profile_height(self, value): + self.profile.info.height = value + self.update_display_aspect_ratio() + self.update_profile_path() + + def update_profile_pixel_ratio(self): + num = self.txtPixelRatioNum.value() + den = self.txtPixelRatioDen.value() + self.profile.info.pixel_ratio.num = num + self.profile.info.pixel_ratio.den = den + self.update_display_aspect_ratio() + self.update_profile_path() + + def update_display_aspect_ratio(self): + """Update display aspect ratio based on width, height, and pixel ratio.""" + width = self.profile.info.width + height = self.profile.info.height + pixel_ratio_num = self.profile.info.pixel_ratio.num + pixel_ratio_den = self.profile.info.pixel_ratio.den + + if height != 0 and pixel_ratio_den != 0: + self.profile.info.display_ratio.num = width * pixel_ratio_num + self.profile.info.display_ratio.den = height * pixel_ratio_den + self.profile.info.display_ratio.Reduce() + + # Update the aspect ratio fields in the UI + self.txtAspectRatioNum.setValue(self.profile.info.display_ratio.num) + self.txtAspectRatioDen.setValue(self.profile.info.display_ratio.den) + + def update_profile_frame_rate_num(self, value): + self.profile.info.fps.num = value + self.update_profile_path() + + def update_profile_frame_rate_den(self, value): + self.profile.info.fps.den = value + self.update_profile_path() + + def update_profile_interlaced(self, value): + # get translations + _ = get_app()._tr + + self.profile.info.interlaced_frame = True if value == _('Yes') else False + self.update_profile_path() + + def update_profile_path(self): + if self.existing_path and os.path.exists(self.existing_path) and not self.duplicate: + profiles_path = self.existing_path + else: + profiles_path = os.path.join(info.USER_PROFILES_PATH, self.profile.Key()) + + profile_suffix = 1 + while self.duplicate and os.path.exists(profiles_path): + # Add suffix if 'duplicate' mode - and existing file found + profiles_path = f"{os.path.join(info.USER_PROFILES_PATH, self.profile.Key())}-{profile_suffix}" + profile_suffix += 1 + self.lblFilePathValue.setText(profiles_path) + + def accept(self): + """Save the profile to a file when the user accepts the dialog.""" + # get translations + _ = get_app()._tr + + # Prevent saving with no description + if not self.profile.info.description.strip(): + QMessageBox.warning(self, _("Error"), _("Please enter a description for this profile.")) + return + + # Save the profile data as a text file in the user profiles folder + profile_path = self.lblFilePathValue.text() + log.info(f"Saving custom profile: {profile_path}") + self.profile.Save(profile_path) + self.profile.user_created = True + self.profile.path = profile_path + + # Accept the dialog + super(EditProfileDialog, self).accept() + + def reject(self): + """Close the dialog without saving changes.""" + # restore original profile + self.profile.SetJson(self.original_profile) + + super(EditProfileDialog, self).reject() diff --git a/src/windows/ui/profile-edit.ui b/src/windows/ui/profile-edit.ui new file mode 100644 index 0000000000..775e80d4f8 --- /dev/null +++ b/src/windows/ui/profile-edit.ui @@ -0,0 +1,385 @@ + + + ProfileEditDialog + + + + 0 + 0 + 458 + 350 + + + + Create Profile + + + + + + + + + 100 + 0 + + + + File Path: + + + + + + + + 0 + 0 + + + + */.openshot_qt/profiles/* + + + Qt::MarkdownText + + + + + + + + + + + + 100 + 0 + + + + Aspect Ratio: + + + + + + + false + + + + 0 + 0 + + + + 1 + + + 9999 + + + + + + + false + + + + 0 + 0 + + + + 1 + + + 9999 + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + + + + + + 100 + 0 + + + + Description: + + + + + + + + 0 + 0 + + + + + + + + + + + + + 100 + 0 + + + + Height: + + + + + + + + 0 + 0 + + + + 2 + + + 40000 + + + + + + + + + + + + 100 + 0 + + + + Width: + + + + + + + + 0 + 0 + + + + 2 + + + 40000 + + + + + + + + + 0 + + + + + + 100 + 0 + + + + Interlaced: + + + + + + + + 0 + 0 + + + + + + + + + + + + + 100 + 0 + + + + Pixel Ratio: + + + + + + + + 0 + 0 + + + + 1 + + + 9999 + + + + + + + + 0 + 0 + + + + 1 + + + 9999 + + + + + + + + + + + + 100 + 0 + + + + Frame Rate: + + + + + + + + 0 + 0 + + + + 1 + + + 99999 + + + + + + + + 0 + 0 + + + + 1 + + + 9999 + + + + + + + + + txtProfileName + txtWidth + txtHeight + txtFrameRateNum + txtFrameRateDen + txtAspectRatioNum + txtAspectRatioDen + txtPixelRatioNum + txtPixelRatioDen + cboInterlaced + + + + + buttonBox + accepted() + ProfileEditDialog + accept() + + + 228 + 327 + + + 228 + 174 + + + + + buttonBox + rejected() + ProfileEditDialog + reject() + + + 228 + 327 + + + 228 + 174 + + + + + diff --git a/src/windows/views/files_listview.py b/src/windows/views/files_listview.py index 8cf661048c..91092cd81a 100644 --- a/src/windows/views/files_listview.py +++ b/src/windows/views/files_listview.py @@ -92,11 +92,11 @@ def contextMenuEvent(self, event): # Get file's profile file_profile = file.profile() if file_profile.info.description: - action = profile_menu.addAction(profile_icon, _(f"{file_profile.info.description}")) + action = profile_menu.addAction(profile_icon, file_profile.info.description) action.triggered.connect(lambda: get_app().window.actionProfile_trigger(file_profile)) else: - action = profile_menu.addAction(profile_missing_icon, _(f"Create Profile: {file_profile.ShortName()}")) - #action.triggered.connect(lambda: get_app().window.actionProfile_trigger(file_profile)) + action = profile_menu.addAction(profile_missing_icon, _(f"Create Profile") + f": {file_profile.ShortName()}") + action.triggered.connect(lambda: get_app().window.actionProfileEdit_trigger(file_profile)) menu.addMenu(profile_menu) menu.addAction(self.win.actionFile_Properties) diff --git a/src/windows/views/files_treeview.py b/src/windows/views/files_treeview.py index 800d173cba..d0d4485de5 100644 --- a/src/windows/views/files_treeview.py +++ b/src/windows/views/files_treeview.py @@ -95,11 +95,11 @@ def contextMenuEvent(self, event): # Get file's profile file_profile = file.profile() if file_profile.info.description: - action = profile_menu.addAction(profile_icon, _(f"{file_profile.info.description}")) + action = profile_menu.addAction(profile_icon, file_profile.info.description) action.triggered.connect(lambda: get_app().window.actionProfile_trigger(file_profile)) else: - action = profile_menu.addAction(profile_missing_icon, _(f"Create Profile: {file_profile.ShortName()}")) - #action.triggered.connect(lambda: get_app().window.actionProfile_trigger(file_profile)) + action = profile_menu.addAction(profile_missing_icon, _(f"Create Profile") + f": {file_profile.ShortName()}") + action.triggered.connect(lambda: get_app().window.actionProfileEdit_trigger(file_profile)) menu.addMenu(profile_menu) menu.addAction(self.win.actionFile_Properties) diff --git a/src/windows/views/profiles_treeview.py b/src/windows/views/profiles_treeview.py index 520b6d0fcd..2d1e5d7cdc 100644 --- a/src/windows/views/profiles_treeview.py +++ b/src/windows/views/profiles_treeview.py @@ -5,7 +5,7 @@ @section LICENSE - Copyright (c) 2008-2023 OpenShot Studios, LLC + Copyright (c) 2008-2024 OpenShot Studios, LLC (http://www.openshotstudios.com). This file is part of OpenShot Video Editor (http://www.openshot.org), an open-source project dedicated to delivering high quality video editing and animation solutions @@ -26,10 +26,11 @@ """ from PyQt5.QtCore import Qt, QItemSelectionModel, QRegExp, pyqtSignal, QTimer -from PyQt5.QtWidgets import QListView, QTreeView, QAbstractItemView, QSizePolicy +from PyQt5.QtWidgets import QListView, QTreeView, QAbstractItemView, QSizePolicy, QAction from classes.app import get_app from windows.models.profiles_model import ProfilesModel +from .menu import StyledContextMenu class ProfilesTreeView(QTreeView): @@ -72,11 +73,41 @@ def get_profile(self): """Return the selected profile object, if any""" return self.selected_profile_object - def __init__(self, profiles, *args): + def contextMenuEvent(self, event): + """Handle right-click context menu for profiles""" + profile = self.selected_profile_object + if not profile: + return + + # get translations + _ = get_app()._tr + + menu = StyledContextMenu(parent=self) + + # Determine if the profile is user-created or not + if hasattr(profile, 'user_created') and profile.user_created: + edit_action = QAction(_("Edit"), self) + edit_action.triggered.connect(lambda: get_app().window.actionProfileEdit_trigger(profile, duplicate=False, parent=self)) + menu.addAction(edit_action) + + duplicate_action = QAction(_("Duplicate"), self) + duplicate_action.triggered.connect(lambda: get_app().window.actionProfileEdit_trigger(profile, duplicate=True, parent=self)) + menu.addAction(duplicate_action) + menu.addSeparator() + + if hasattr(profile, 'user_created') and profile.user_created: + delete_action = QAction(_("Delete"), self) + delete_action.triggered.connect(lambda: get_app().window.actionProfileEdit_trigger(profile, delete=True, parent=self)) + menu.addAction(delete_action) + + menu.popup(event.globalPos()) + + def __init__(self, dialog, profiles, *args): # Invoke parent init QListView.__init__(self, *args) # Get a reference to the window object + self.parent = dialog self.win = get_app().window # Get Model data