Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f78ff8a
Nuke image loader
henriknorin-ftrack Jun 10, 2024
cb218e4
Merge branch 'backlog/framework-loader' of https://github.com/ftrackh…
henriknorin-ftrack Jun 10, 2024
a87d933
Merge branch 'backlog/framework-loader' of https://github.com/ftrackh…
henriknorin-ftrack Jun 10, 2024
71e3abd
Nuke loader
henriknorin-ftrack Jun 10, 2024
5a10ccd
Merge branch 'backlog/framework-loader' of https://github.com/ftrackh…
henriknorin-ftrack Jun 10, 2024
560a1ff
WIP
henriknorin-ftrack Jun 11, 2024
493845d
Merge branch 'backlog/framework-loader' of https://github.com/ftrackh…
henriknorin-ftrack Jun 11, 2024
fc1060e
Nuke loader and lodaer dialig IP
henriknorin-ftrack Jun 11, 2024
e4aecb7
Merge branch 'backlog/framework-loader' of https://github.com/ftrackh…
henriknorin-ftrack Jun 11, 2024
5ec621d
Nuke image loader working
henriknorin-ftrack Jun 11, 2024
3ee45f5
Merge branch 'backlog/framework-loader' of https://github.com/ftrackh…
henriknorin-ftrack Jun 11, 2024
f7c662e
Dcc config fixes
henriknorin-ftrack Jun 11, 2024
853f5e5
Merge branch 'backlog/framework-loader' of https://github.com/ftrackh…
henriknorin-ftrack Jun 11, 2024
89503c9
Nuke image loader working
henriknorin-ftrack Jun 11, 2024
1ad2b26
May revert
henriknorin-ftrack Jun 11, 2024
37b67f2
PR cleanup
henriknorin-ftrack Jun 11, 2024
e9b8339
Nuke image sequence and movie loaders
henriknorin-ftrack Jun 12, 2024
2c91067
Update projects/framework-common-extensions/dialogs/standard_loader_d…
henriknorin-ftrack Jun 12, 2024
39fde92
Update projects/framework-common-extensions/dialogs/standard_loader_d…
henriknorin-ftrack Jun 12, 2024
2aae3a2
Update projects/framework-common-extensions/dialogs/standard_loader_d…
henriknorin-ftrack Jun 12, 2024
6efd9be
PR cleanup
henriknorin-ftrack Jun 12, 2024
54e1c82
Update projects/framework-common-extensions/dialogs/standard_loader_d…
henriknorin-ftrack Jun 12, 2024
f7316b7
Update projects/framework-common-extensions/dialogs/standard_loader_d…
henriknorin-ftrack Jun 12, 2024
c5185f0
Update projects/framework-nuke/extensions/nuke.yaml
henriknorin-ftrack Jun 12, 2024
1bd710a
Merge branch 'backlog/framework-loader-henrik' of https://github.com/…
henriknorin-ftrack Jun 12, 2024
90de0e3
Attended PR changes
henriknorin-ftrack Jun 12, 2024
b7405ce
Merge branch 'backlog/framework-loader' of https://github.com/ftrackh…
henriknorin-ftrack Jun 12, 2024
eb88ccb
Sync with changes
henriknorin-ftrack Jun 12, 2024
abac769
Merged remote 'backlog/framework-loader' into 'backlog/framework-load…
henriknorin-ftrack Jun 18, 2024
9aea2e6
Removed name option; Properly check if still images and movies exist
henriknorin-ftrack Jun 18, 2024
109eb36
Pass event_data to _resolve_entity_paths
henriknorin-ftrack Jun 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ def remote_event_manager(self):
self._remote_event_manager = EventManager(
session=_remote_session, mode=constants.event.REMOTE_EVENT_MODE
)
# Make sure it is shutdown
atexit.register(self.close)
return self._remote_event_manager
# Make sure it is shutdown
atexit.register(self.close)
return self._remote_event_manager

def __init__(
self, event_manager, registry, run_in_main_thread_wrapper=None
Expand Down Expand Up @@ -667,6 +667,7 @@ def verify_plugins(self, plugin_names):

def close(self):
self.logger.debug('Shutting down client')

if self._remote_event_manager:
self.logger.debug('Stopping remote_event_manager')
self.remote_event_manager.close()
Expand Down
54 changes: 54 additions & 0 deletions libs/utils/source/ftrack_utils/paths/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import os
import clique
import tempfile
import logging

logger = logging.getLogger(__name__)


def find_image_sequence(file_path):
Expand Down Expand Up @@ -56,3 +59,54 @@ def get_temp_path(filename_extension=None):
os.makedirs(os.path.dirname(result))

return result


def check_image_sequence(path):
'''Check if the image sequence pointed out by *path* exists, returns metadata
about the sequence if it does, raises an exception otherwise.'''
directory, basename = os.path.split(path)

p_pos = basename.find('%')
d_pos = basename.find('d', p_pos)
exp = basename[p_pos : d_pos + 1]

padding = 0
if d_pos > p_pos + 2:
# %04d expression
padding = int(basename[p_pos + 1 : d_pos])

ws_pos = basename.rfind(' ')
dash_pos = basename.find('-', ws_pos)

prefix = basename[:p_pos]
suffix = basename[d_pos + 1 : ws_pos]

start = int(basename[ws_pos + 2 : dash_pos])
end = int(basename[dash_pos + 1 : -1])

if padding == 0:
# No padding, calculate padding from start and end
padding = len(str(end))

logger.debug(
f'Looking for frames {start}>{end} in directory {directory} starting '
f'with {prefix}, ending with {suffix} (padding: {padding})'
)

for frame in range(start, end + 1):
filename = f'{prefix}{exp % frame}{suffix}'
test_path = os.path.join(directory, filename)
if not os.path.exists(test_path):
raise Exception(
f'Image sequence member {frame} not ' f'found @ "{test_path}"!'
)
logger.debug(f'Frame {frame} verified: {filename}')

return {
'directory': directory,
'prefix': prefix,
'suffix': suffix,
'start': start,
'end': end,
'padding': padding,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
# :coding: utf-8
# :copyright: Copyright (c) 2024 ftrack
import os.path

try:
from PySide6 import QtWidgets, QtCore
except ImportError:
from PySide2 import QtWidgets, QtCore

from ftrack_utils.framework.config.tool import get_plugins, get_groups
from ftrack_utils.string import str_version
from ftrack_framework_qt.dialogs import BaseDialog
from ftrack_qt.widgets.progress import ProgressWidget
from ftrack_qt.utils.decorators import invoke_in_qt_main_thread
from ftrack_qt.utils.widget import build_progress_data


class StandardLoaderDialog(BaseDialog):
'''Default Framework Loader dialog'''

name = 'framework_standard_loader_dialog'
tool_config_type_filter = ['loader']
ui_type = 'qt'
run_button_title = 'LOAD'

def __init__(
self,
event_manager,
client_id,
connect_methods_callback,
connect_setter_property_callback,
connect_getter_property_callback,
dialog_options,
parent=None,
):
'''
Initialize Mixin class loader dialog. It will load the qt dialog and
mix it with the framework dialog.
*event_manager*: instance of
:class:`~ftrack_framework_core.event.EventManager`
*client_id*: Id of the client that initializes the current dialog
*connect_methods_callback*: Client callback method for the dialog to be
able to execute client methods.
*connect_setter_property_callback*: Client callback property setter for
the dialog to be able to read client properties.
*connect_getter_property_callback*: Client callback property getter for
the dialog to be able to write client properties.
*dialog_options*: Dictionary of arguments passed on to configure the
current dialog.
'''
self._scroll_area = None
self._scroll_area_widget = None
self._progress_widget = None

super(StandardLoaderDialog, self).__init__(
event_manager,
client_id,
connect_methods_callback,
connect_setter_property_callback,
connect_getter_property_callback,
dialog_options,
parent=parent,
)
self.resize(400, 450)
self.setWindowTitle('ftrack Loader')

def get_entities(self):
'''Get the entities to load from dialog options'''
result = []
for entry in self.dialog_options.get('event_data', {}).get(
'selection', []
):
result.append(
{
'entity_id': entry['entityId'],
'entity_type': entry['entityType'],
}
)
return result

def is_compatible(self, tool_config, component):
'''Check if the *tool_config* is compatible with provided *component* entity,
returns True if compatible, False or error message as string otherwise
'''
compatible = tool_config.get('compatible')
if not compatible:
return f'Tool config {tool_config} is missing required loader "compatible" entry!'
# Filter on combination of component name, asset_type and file extension
result = False
if compatible.get('component'):
# Component name match?
if compatible['component'].lower() == component['name'].lower():
result = True
else:
self.logger.debug(
f"Component {compatible['component']} doesn't match {component['name']}"
)
return False
if compatible.get('asset_type'):
# Asset type match?
asset = component['version']['asset']
asset_type = asset['type']['name']
if compatible['asset_type'].lower() == asset_type.lower():
result = True
else:
self.logger.debug(
f"Asset type {compatible['asset_type']} doesn't match {asset_type}"
)
return False
if compatible.get('supported_file_extensions'):
# Any file extension match?
result = False
file_extension = component['file_type']
for file_type in compatible.get('supported_file_extensions'):
if file_type.lower() == file_extension.lower():
result = True
break
if not result:
self.logger.debug(
f"File extensions {compatible['supported_file_extensions']} doesn't match component: {file_extension}"
)
return False
if compatible.get('entity_types'):
result = False
for entity_type in compatible['entity_types']:
if component.entity_type == entity_type:
result = True
break
if not result:
self.logger.debug(
f"Component {component['name']} entity type {component.entity_type} doesn't match {compatible['entity_types']}"
)
return False
if not result:
self.logger.debug(
f'Tool config {tool_config} is not compatible with component'
)
return result

def pre_build_ui(self):
pass

def build_ui(self):
# Check entities
# Select the desired tool_config
tool_config_message = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach decided on the meeting:
The dialog contains the function to filter out the tool_configs based on the compatible:file_types keys from the tool_config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving logic to a is_compatible class function

if 'event_data' not in self.dialog_options:
tool_config_message = 'No event data provided!'
elif not self.get_entities():
tool_config_message = 'No entity provided to load!'
elif len(self.get_entities()) != 1:
tool_config_message = 'Only one entity supported!'
elif self.get_entities()[0]['entity_type'].lower() != 'component':
tool_config_message = 'Only components can be loaded'
elif self.filtered_tool_configs.get("loader"):
component_id = self.get_entities()[0]['entity_id']
component = self.session.query(
f'Component where id={component_id}'
).first()

if not component:
tool_config_message = f'Component not found: {component_id}'
else:
# Loop through tool configs, find a one that can load the component in question
for tool_config in self.filtered_tool_configs["loader"]:
result = self.is_compatible(tool_config, component)
if result is not True:
if isinstance(result, str):
# Unrecoverable error
tool_config_message = result
break
else:
tool_config_name = tool_config['name']
self.logger.debug(
f'Using tool config {tool_config_name}'
)
if self.tool_config != tool_config:
try:
self.tool_config = tool_config
except Exception as error:
tool_config_message = error
break
self._progress_widget = ProgressWidget(
'load', build_progress_data(tool_config)
)
self.header.set_widget(
self._progress_widget.status_widget
)
self.overlay_layout.addWidget(
self._progress_widget.overlay_widget
)
break
if not self.tool_config and not tool_config_message:
tool_config_message = f'Could not find a tool config compatible with the component {component_id}!'
else:
tool_config_message = 'No loader tool configs available!'

if not self.tool_config:
self.logger.warning(tool_config_message)
label_widget = QtWidgets.QLabel(f'{tool_config_message}')
label_widget.setStyleSheet(
"font-style: italic; font-weight: bold; color: red;"
)
self.tool_widget.layout().addWidget(label_widget)
return

# Build context widgets
context_plugins = get_plugins(
self.tool_config, filters={'tags': ['context']}
)
for context_plugin in context_plugins:
if not context_plugin.get('ui'):
continue
# Inject the entity data into the context plugin
if 'options' not in context_plugin:
context_plugin['options'] = {}
Comment on lines +214 to +216
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be needed, as the entity data comes from ftrack, it will always be in the plugin right?

context_plugin['options'].update(self.dialog_options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I think this shouldn't happend, as in that case you are double passing the options to the context plugin

Suggested change
context_plugin['options'].update(self.dialog_options)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we try then to be more specific? so instead of passing the self.dialog_optins pass the self.dialog_options.get('event_data')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to improve the calls within resolve_entity_paths plugin instead

context_widget = self.init_framework_widget(context_plugin)
self.tool_widget.layout().addWidget(context_widget)

# Build human readable loader name from tool config name
loader_name_widget = QtWidgets.QWidget()
loader_name_widget.setLayout(QtWidgets.QHBoxLayout())

label = QtWidgets.QLabel('Loader:')
label.setProperty('secondary', True)
loader_name_widget.layout().addWidget(label)

label = QtWidgets.QLabel(
self.tool_config['name'].replace('-', ' ').title()
)
label.setProperty('h2', True)
loader_name_widget.layout().addWidget(label, 100)

self.tool_widget.layout().addWidget(loader_name_widget)

# Add loader plugin(s)
loader_plugins = get_plugins(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this dialog is only supporting raw plugins but not grouping them, should we somehow represent the groups in the tool_config in case they are provided? Otherwise here will simply represent all the plugins plain and might lead to a duplicated plugins.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify the loader now this first iteration, to just support one single plugin. And then if users ask for advanced loaders with multiple plugins we could unlock that

self.tool_config, filters={'tags': ['loader']}
)
# Expect only one for now
if len(loader_plugins) != 1:
raise Exception('Only one(1) loader plugin is supported!')

for loader_plugin in loader_plugins:
options = loader_plugin.get('options', {})
if not loader_plugin.get('ui'):
continue
loader_widget = self.init_framework_widget(loader_plugin)
self.tool_widget.layout().addWidget(loader_widget)

spacer = QtWidgets.QSpacerItem(
1,
1,
QtWidgets.QSizePolicy.Minimum,
QtWidgets.QSizePolicy.Expanding,
)
self.tool_widget.layout().addItem(spacer)

def post_build_ui(self):
self._progress_widget.hide_overlay_signal.connect(
self.show_main_widget
)
self._progress_widget.show_overlay_signal.connect(
self.show_overlay_widget
)

def _on_run_button_clicked(self):
'''(Override) Drive the progress widget'''
self.show_overlay_widget()
self._progress_widget.run()
super(StandardLoaderDialog, self)._on_run_button_clicked()

@invoke_in_qt_main_thread
def plugin_run_callback(self, log_item):
'''(Override) Pass framework log item to the progress widget'''
self._progress_widget.update_phase_status(
log_item.reference,
log_item.status,
log_message=log_item.message,
time=log_item.execution_time,
)

def closeEvent(self, event):
'''(Override) Close the context and progress widgets'''
self._progress_widget.teardown()
self._progress_widget.deleteLater()
super(StandardLoaderDialog, self).closeEvent(event)
Loading