Skip to content

Commit

Permalink
Adding new "Choose Profile" context menu to Project Files, to make it…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
jonoomph committed Oct 8, 2024
1 parent fbaecef commit 2715855
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 33 deletions.
24 changes: 24 additions & 0 deletions src/classes/project_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,30 @@ def changed(self, action):
action.set_old_values(old_vals) # Save previous values to reverse this action
self.has_unsaved_changes = True

if len(action.key) == 1 and action.key[0] in ["fps"]:
# FPS changed (apply profile)
profile_key = self._data.get("profile")
profile = self.get_profile(profile_key)
if profile:
# Get current FPS (prior to changing)
new_fps = self._data.get("fps")
new_fps_float = float(new_fps["num"]) / float(new_fps["den"])
old_fps_float = float(old_vals["num"]) / float(old_vals["den"])
fps_factor = float(new_fps_float / old_fps_float)

if fps_factor != 1.0:
log.info(f"Convert {old_fps_float} FPS to {new_fps_float} FPS (profile: {profile.ShortName()})")
# Snap to new FPS grid (start, end, duration)
change_profile(self._data["clips"] + self._data["effects"], profile)

# Rescale keyframes to match new FPS
self.rescale_keyframes(fps_factor)

# Broadcast this change out
get_app().updates.load(self._data, reset_history=False)
else:
log.warning(f"No profile found for {profile_key}")

elif action.type == "delete":
# Delete existing item
old_vals = self._set(action.key, remove=True)
Expand Down
58 changes: 57 additions & 1 deletion src/classes/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
"""

import os
import json
import os

import openshot

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

def profile(self):
""" Get the profile of the file """
# Create Profile object for current file
d = self.data

# Calculate accurate DAR
pixel_ratio = openshot.Fraction(d.get("pixel_ratio", {}))
display_ratio = openshot.Fraction(d.get("display_ratio", {}))
if display_ratio.num == 1 and display_ratio.den == 1:
# Some audio / image files have inaccurate DAR - calculate from size
display_ratio = openshot.Fraction(round(d.get("width", 1) * pixel_ratio.ToFloat()),
round(d.get("height", 1) * pixel_ratio.ToFloat()))
display_ratio.Reduce()

profile_dict = {
"display_ratio":
{
"den": display_ratio.den,
"num": display_ratio.num,
},
"fps":
{
"den": d.get("fps", {}).get("den", 1),
"num": d.get("fps", {}).get("num", 1),
},
"height": d.get("height", 1),
"interlaced_frame": d.get("interlaced_frame", False),
"pixel_format": d.get("pixel_format", None),
"pixel_ratio":
{
"den": d.get("pixel_ratio", {}).get("den", 1),
"num": d.get("pixel_ratio", {}).get("num", 1),
},
"width": d.get("width", 1)
}
file_profile = openshot.Profile()
file_profile.SetJson(json.dumps(profile_dict))

# Load all possible profiles
for profile_folder in [info.USER_PROFILES_PATH, info.PROFILES_PATH]:
for file in reversed(sorted(os.listdir(profile_folder))):
profile_path = os.path.join(profile_folder, file)
if os.path.isdir(profile_path):
continue
try:
# Load Profile
profile = openshot.Profile(profile_path)
print(profile.Key(), file_profile.Key())
if profile.Key() == file_profile.Key():
return profile
except RuntimeError as e:
pass
return file_profile


class Marker(QueryObject):
""" This class allows Markers to be queried, updated, and deleted from the project data. """
Expand Down
60 changes: 32 additions & 28 deletions src/windows/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -1753,21 +1753,27 @@ def initShortcuts(self):
# Log shortcut initialization completion
log.debug("Shortcuts initialized or updated.")

def actionProfile_trigger(self):
# Show dialog
from windows.profile import Profile
log.debug("Showing profile dialog")

# Get current project profile description
current_project_profile_desc = get_app().project.get(['profile'])

def actionProfile_trigger(self, profile=None):
# Disable video caching
openshot.Settings.Instance().ENABLE_PLAYBACK_CACHING = False

win = Profile(current_project_profile_desc)
# Run the dialog event loop - blocking interaction on this window during this time
result = win.exec_()
profile = win.selected_profile
# Show Profile dialog (if profile not passed)
if not profile:
# Show dialog
from windows.profile import Profile
log.debug("Showing profile dialog")

# Get current project profile description
current_project_profile_desc = get_app().project.get(['profile'])

win = Profile(current_project_profile_desc)
result = win.exec_()
profile = win.selected_profile
else:
# Profile passed in alraedy
result = QDialog.Accepted

# Update profile (if changed)
if result == QDialog.Accepted and profile and profile.info.description != get_app().project.get(['profile']):
proj = get_app().project

Expand All @@ -1792,32 +1798,30 @@ def actionProfile_trigger(self):
# Check for audio-only files
if file.data.get("has_audio") and not file.data.get("has_video"):
# Audio-only file should match the current project size and FPS
file.data["width"] = proj.get("width")
file.data["height"] = proj.get("height")
file.data["width"] = profile.info.width
file.data["height"] = profile.info.height
display_ratio = openshot.Fraction(file.data["width"], file.data["height"])
display_ratio.Reduce()
file.data["display_ratio"]["num"] = display_ratio.num
file.data["display_ratio"]["den"] = display_ratio.den
file.save()

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

# Apply new profile (and any FPS precision updates)
get_app().updates.update(["profile"], profile.info.description)
get_app().updates.update(["width"], profile.info.width)
get_app().updates.update(["height"], profile.info.height)
get_app().updates.update(["display_ratio"], {"num": profile.info.display_ratio.num, "den": profile.info.display_ratio.den})
get_app().updates.update(["pixel_ratio"], {"num": profile.info.pixel_ratio.num, "den": profile.info.pixel_ratio.den})
get_app().updates.update(["fps"], {"num": profile.info.fps.num, "den": profile.info.fps.den})

# Clear transaction id
get_app().updates.transaction_id = None

# Rescale all keyframes and reload project
if fps_factor != 1.0:
# Rescale keyframes (if FPS changed)
proj.rescale_keyframes(fps_factor)

# Apply new profile (and any FPS precision updates)
proj.apply_profile(profile)

# Distribute all project data through update manager
get_app().updates.load(proj._data, reset_history=False)

# Force ApplyMapperToClips to apply these changes
self.timeline_sync.timeline.ApplyMapperToClips()

# Seek to the same location, adjusted for new frame rate
self.SeekSignal.emit(adjusted_frame)

Expand Down
4 changes: 2 additions & 2 deletions src/windows/models/files_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,8 @@ def add_files(self, files, image_seq_details=None, quiet=False,
# Log exception
log.warning("Failed to import {}: {}".format(filepath, ex))

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

# Reset list of ignored paths
Expand Down
20 changes: 19 additions & 1 deletion src/windows/views/files_listview.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"""

from PyQt5.QtCore import QSize, Qt, QPoint, QRegExp
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter, QIcon
from PyQt5.QtWidgets import QListView, QAbstractItemView

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

# Set context menu mode
app = get_app()
_ = app._tr
app.context_menu_object = "files"

index = self.indexAt(event.pos())
Expand Down Expand Up @@ -81,6 +82,23 @@ def contextMenuEvent(self, event):
menu.addAction(self.win.actionExportFiles)
menu.addSeparator()
menu.addAction(self.win.actionAdd_to_Timeline)

# Add Profile menu
profile_menu = StyledContextMenu(title=_("Choose Profile"), parent=self)
profile_icon = get_app().window.actionProfile.icon()
profile_missing_icon = QIcon(":/icons/Humanity/actions/16/list-add.svg")
profile_menu.setIcon(profile_icon)

# 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.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))
menu.addMenu(profile_menu)

menu.addAction(self.win.actionFile_Properties)
menu.addSeparator()
menu.addAction(self.win.actionRemove_from_Project)
Expand Down
20 changes: 19 additions & 1 deletion src/windows/views/files_treeview.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import os

from PyQt5.QtCore import QSize, Qt, QPoint
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter
from PyQt5.QtGui import QDrag, QCursor, QPixmap, QPainter, QIcon
from PyQt5.QtWidgets import QTreeView, QAbstractItemView, QSizePolicy, QHeaderView

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

# Set context menu mode
app = get_app()
_ = app._tr
app.context_menu_object = "files"

event.accept()
Expand Down Expand Up @@ -84,6 +85,23 @@ def contextMenuEvent(self, event):
menu.addAction(self.win.actionExportFiles)
menu.addSeparator()
menu.addAction(self.win.actionAdd_to_Timeline)

# Add Profile menu
profile_menu = StyledContextMenu(title=_("Choose Profile"), parent=self)
profile_icon = get_app().window.actionProfile.icon()
profile_missing_icon = QIcon(":/icons/Humanity/actions/16/list-add.svg")
profile_menu.setIcon(profile_icon)

# 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.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))
menu.addMenu(profile_menu)

menu.addAction(self.win.actionFile_Properties)
menu.addSeparator()
menu.addAction(self.win.actionRemove_from_Project)
Expand Down

0 comments on commit 2715855

Please sign in to comment.