Skip to content

Commit 2715855

Browse files
committed
Adding new "Choose Profile" context menu to Project Files, to make it easy to edit using your source file width+height+FPS profile. Avoid error message prompts when importing multiple files. Large refactor of how profile switching happens (moving to UpdateManger - so it will support undo/redo system). Add new profile() method to File Query class, to make it easy to generate/find a profile object for any File object.
1 parent fbaecef commit 2715855

File tree

6 files changed

+153
-33
lines changed

6 files changed

+153
-33
lines changed

src/classes/project_data.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,30 @@ def changed(self, action):
11071107
action.set_old_values(old_vals) # Save previous values to reverse this action
11081108
self.has_unsaved_changes = True
11091109

1110+
if len(action.key) == 1 and action.key[0] in ["fps"]:
1111+
# FPS changed (apply profile)
1112+
profile_key = self._data.get("profile")
1113+
profile = self.get_profile(profile_key)
1114+
if profile:
1115+
# Get current FPS (prior to changing)
1116+
new_fps = self._data.get("fps")
1117+
new_fps_float = float(new_fps["num"]) / float(new_fps["den"])
1118+
old_fps_float = float(old_vals["num"]) / float(old_vals["den"])
1119+
fps_factor = float(new_fps_float / old_fps_float)
1120+
1121+
if fps_factor != 1.0:
1122+
log.info(f"Convert {old_fps_float} FPS to {new_fps_float} FPS (profile: {profile.ShortName()})")
1123+
# Snap to new FPS grid (start, end, duration)
1124+
change_profile(self._data["clips"] + self._data["effects"], profile)
1125+
1126+
# Rescale keyframes to match new FPS
1127+
self.rescale_keyframes(fps_factor)
1128+
1129+
# Broadcast this change out
1130+
get_app().updates.load(self._data, reset_history=False)
1131+
else:
1132+
log.warning(f"No profile found for {profile_key}")
1133+
11101134
elif action.type == "delete":
11111135
# Delete existing item
11121136
old_vals = self._set(action.key, remove=True)

src/classes/query.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
2626
"""
2727

28-
import os
2928
import json
29+
import os
30+
31+
import openshot
3032

3133
from classes import info
3234
from classes.app import get_app
@@ -257,6 +259,60 @@ def relative_path(self):
257259
# Convert path to relative (based on current working directory of Python)
258260
return os.path.relpath(file_path, info.CWD)
259261

262+
def profile(self):
263+
""" Get the profile of the file """
264+
# Create Profile object for current file
265+
d = self.data
266+
267+
# Calculate accurate DAR
268+
pixel_ratio = openshot.Fraction(d.get("pixel_ratio", {}))
269+
display_ratio = openshot.Fraction(d.get("display_ratio", {}))
270+
if display_ratio.num == 1 and display_ratio.den == 1:
271+
# Some audio / image files have inaccurate DAR - calculate from size
272+
display_ratio = openshot.Fraction(round(d.get("width", 1) * pixel_ratio.ToFloat()),
273+
round(d.get("height", 1) * pixel_ratio.ToFloat()))
274+
display_ratio.Reduce()
275+
276+
profile_dict = {
277+
"display_ratio":
278+
{
279+
"den": display_ratio.den,
280+
"num": display_ratio.num,
281+
},
282+
"fps":
283+
{
284+
"den": d.get("fps", {}).get("den", 1),
285+
"num": d.get("fps", {}).get("num", 1),
286+
},
287+
"height": d.get("height", 1),
288+
"interlaced_frame": d.get("interlaced_frame", False),
289+
"pixel_format": d.get("pixel_format", None),
290+
"pixel_ratio":
291+
{
292+
"den": d.get("pixel_ratio", {}).get("den", 1),
293+
"num": d.get("pixel_ratio", {}).get("num", 1),
294+
},
295+
"width": d.get("width", 1)
296+
}
297+
file_profile = openshot.Profile()
298+
file_profile.SetJson(json.dumps(profile_dict))
299+
300+
# Load all possible profiles
301+
for profile_folder in [info.USER_PROFILES_PATH, info.PROFILES_PATH]:
302+
for file in reversed(sorted(os.listdir(profile_folder))):
303+
profile_path = os.path.join(profile_folder, file)
304+
if os.path.isdir(profile_path):
305+
continue
306+
try:
307+
# Load Profile
308+
profile = openshot.Profile(profile_path)
309+
print(profile.Key(), file_profile.Key())
310+
if profile.Key() == file_profile.Key():
311+
return profile
312+
except RuntimeError as e:
313+
pass
314+
return file_profile
315+
260316

261317
class Marker(QueryObject):
262318
""" This class allows Markers to be queried, updated, and deleted from the project data. """

src/windows/main_window.py

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,21 +1753,27 @@ def initShortcuts(self):
17531753
# Log shortcut initialization completion
17541754
log.debug("Shortcuts initialized or updated.")
17551755

1756-
def actionProfile_trigger(self):
1757-
# Show dialog
1758-
from windows.profile import Profile
1759-
log.debug("Showing profile dialog")
1760-
1761-
# Get current project profile description
1762-
current_project_profile_desc = get_app().project.get(['profile'])
1763-
1756+
def actionProfile_trigger(self, profile=None):
17641757
# Disable video caching
17651758
openshot.Settings.Instance().ENABLE_PLAYBACK_CACHING = False
17661759

1767-
win = Profile(current_project_profile_desc)
1768-
# Run the dialog event loop - blocking interaction on this window during this time
1769-
result = win.exec_()
1770-
profile = win.selected_profile
1760+
# Show Profile dialog (if profile not passed)
1761+
if not profile:
1762+
# Show dialog
1763+
from windows.profile import Profile
1764+
log.debug("Showing profile dialog")
1765+
1766+
# Get current project profile description
1767+
current_project_profile_desc = get_app().project.get(['profile'])
1768+
1769+
win = Profile(current_project_profile_desc)
1770+
result = win.exec_()
1771+
profile = win.selected_profile
1772+
else:
1773+
# Profile passed in alraedy
1774+
result = QDialog.Accepted
1775+
1776+
# Update profile (if changed)
17711777
if result == QDialog.Accepted and profile and profile.info.description != get_app().project.get(['profile']):
17721778
proj = get_app().project
17731779

@@ -1792,32 +1798,30 @@ def actionProfile_trigger(self):
17921798
# Check for audio-only files
17931799
if file.data.get("has_audio") and not file.data.get("has_video"):
17941800
# Audio-only file should match the current project size and FPS
1795-
file.data["width"] = proj.get("width")
1796-
file.data["height"] = proj.get("height")
1801+
file.data["width"] = profile.info.width
1802+
file.data["height"] = profile.info.height
1803+
display_ratio = openshot.Fraction(file.data["width"], file.data["height"])
1804+
display_ratio.Reduce()
1805+
file.data["display_ratio"]["num"] = display_ratio.num
1806+
file.data["display_ratio"]["den"] = display_ratio.den
17971807
file.save()
17981808

17991809
# Change all related clips
18001810
for clip in Clip.filter(file_id=file.id):
18011811
clip.data["reader"] = file.data
18021812
clip.save()
18031813

1814+
# Apply new profile (and any FPS precision updates)
1815+
get_app().updates.update(["profile"], profile.info.description)
1816+
get_app().updates.update(["width"], profile.info.width)
1817+
get_app().updates.update(["height"], profile.info.height)
1818+
get_app().updates.update(["display_ratio"], {"num": profile.info.display_ratio.num, "den": profile.info.display_ratio.den})
1819+
get_app().updates.update(["pixel_ratio"], {"num": profile.info.pixel_ratio.num, "den": profile.info.pixel_ratio.den})
1820+
get_app().updates.update(["fps"], {"num": profile.info.fps.num, "den": profile.info.fps.den})
1821+
18041822
# Clear transaction id
18051823
get_app().updates.transaction_id = None
18061824

1807-
# Rescale all keyframes and reload project
1808-
if fps_factor != 1.0:
1809-
# Rescale keyframes (if FPS changed)
1810-
proj.rescale_keyframes(fps_factor)
1811-
1812-
# Apply new profile (and any FPS precision updates)
1813-
proj.apply_profile(profile)
1814-
1815-
# Distribute all project data through update manager
1816-
get_app().updates.load(proj._data, reset_history=False)
1817-
1818-
# Force ApplyMapperToClips to apply these changes
1819-
self.timeline_sync.timeline.ApplyMapperToClips()
1820-
18211825
# Seek to the same location, adjusted for new frame rate
18221826
self.SeekSignal.emit(adjusted_frame)
18231827

src/windows/models/files_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,8 +391,8 @@ def add_files(self, files, image_seq_details=None, quiet=False,
391391
# Log exception
392392
log.warning("Failed to import {}: {}".format(filepath, ex))
393393

394-
if not quiet:
395-
# Show message box to user
394+
if not quiet and start_count == 1:
395+
# Show message box to user (if importing a single file)
396396
app.window.invalidImage(filename)
397397

398398
# Reset list of ignored paths

src/windows/views/files_listview.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"""
2828

2929
from PyQt5.QtCore import QSize, Qt, QPoint, QRegExp
30-
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter
30+
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter, QIcon
3131
from PyQt5.QtWidgets import QListView, QAbstractItemView
3232

3333
from classes import info
@@ -47,6 +47,7 @@ def contextMenuEvent(self, event):
4747

4848
# Set context menu mode
4949
app = get_app()
50+
_ = app._tr
5051
app.context_menu_object = "files"
5152

5253
index = self.indexAt(event.pos())
@@ -81,6 +82,23 @@ def contextMenuEvent(self, event):
8182
menu.addAction(self.win.actionExportFiles)
8283
menu.addSeparator()
8384
menu.addAction(self.win.actionAdd_to_Timeline)
85+
86+
# Add Profile menu
87+
profile_menu = StyledContextMenu(title=_("Choose Profile"), parent=self)
88+
profile_icon = get_app().window.actionProfile.icon()
89+
profile_missing_icon = QIcon(":/icons/Humanity/actions/16/list-add.svg")
90+
profile_menu.setIcon(profile_icon)
91+
92+
# Get file's profile
93+
file_profile = file.profile()
94+
if file_profile.info.description:
95+
action = profile_menu.addAction(profile_icon, _(f"{file_profile.info.description}"))
96+
action.triggered.connect(lambda: get_app().window.actionProfile_trigger(file_profile))
97+
else:
98+
action = profile_menu.addAction(profile_missing_icon, _(f"Create Profile: {file_profile.ShortName()}"))
99+
#action.triggered.connect(lambda: get_app().window.actionProfile_trigger(file_profile))
100+
menu.addMenu(profile_menu)
101+
84102
menu.addAction(self.win.actionFile_Properties)
85103
menu.addSeparator()
86104
menu.addAction(self.win.actionRemove_from_Project)

src/windows/views/files_treeview.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import os
3131

3232
from PyQt5.QtCore import QSize, Qt, QPoint
33-
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter
33+
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter, QIcon
3434
from PyQt5.QtWidgets import QTreeView, QAbstractItemView, QSizePolicy, QHeaderView
3535

3636
from classes import info
@@ -49,6 +49,7 @@ def contextMenuEvent(self, event):
4949

5050
# Set context menu mode
5151
app = get_app()
52+
_ = app._tr
5253
app.context_menu_object = "files"
5354

5455
event.accept()
@@ -84,6 +85,23 @@ def contextMenuEvent(self, event):
8485
menu.addAction(self.win.actionExportFiles)
8586
menu.addSeparator()
8687
menu.addAction(self.win.actionAdd_to_Timeline)
88+
89+
# Add Profile menu
90+
profile_menu = StyledContextMenu(title=_("Choose Profile"), parent=self)
91+
profile_icon = get_app().window.actionProfile.icon()
92+
profile_missing_icon = QIcon(":/icons/Humanity/actions/16/list-add.svg")
93+
profile_menu.setIcon(profile_icon)
94+
95+
# Get file's profile
96+
file_profile = file.profile()
97+
if file_profile.info.description:
98+
action = profile_menu.addAction(profile_icon, _(f"{file_profile.info.description}"))
99+
action.triggered.connect(lambda: get_app().window.actionProfile_trigger(file_profile))
100+
else:
101+
action = profile_menu.addAction(profile_missing_icon, _(f"Create Profile: {file_profile.ShortName()}"))
102+
#action.triggered.connect(lambda: get_app().window.actionProfile_trigger(file_profile))
103+
menu.addMenu(profile_menu)
104+
87105
menu.addAction(self.win.actionFile_Properties)
88106
menu.addSeparator()
89107
menu.addAction(self.win.actionRemove_from_Project)

0 commit comments

Comments
 (0)