Skip to content

Commit 721bb3c

Browse files
feat: Backlog/framework loader henrik (#530)
Co-authored-by: Lluis Casals Marsol <112543804+lluisFtrack@users.noreply.github.com>
1 parent 052d7a4 commit 721bb3c

File tree

16 files changed

+817
-66
lines changed

16 files changed

+817
-66
lines changed

libs/framework-core/source/ftrack_framework_core/client/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,9 @@ def remote_event_manager(self):
195195
self._remote_event_manager = EventManager(
196196
session=_remote_session, mode=constants.event.REMOTE_EVENT_MODE
197197
)
198-
# Make sure it is shutdown
199-
atexit.register(self.close)
200-
return self._remote_event_manager
198+
# Make sure it is shutdown
199+
atexit.register(self.close)
200+
return self._remote_event_manager
201201

202202
def __init__(
203203
self, event_manager, registry, run_in_main_thread_wrapper=None
@@ -667,6 +667,7 @@ def verify_plugins(self, plugin_names):
667667

668668
def close(self):
669669
self.logger.debug('Shutting down client')
670+
670671
if self._remote_event_manager:
671672
self.logger.debug('Stopping remote_event_manager')
672673
self.remote_event_manager.close()

libs/utils/source/ftrack_utils/paths/__init__.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import os
55
import clique
66
import tempfile
7+
import logging
8+
9+
logger = logging.getLogger(__name__)
710

811

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

5861
return result
62+
63+
64+
def check_image_sequence(path):
65+
'''Check if the image sequence pointed out by *path* exists, returns metadata
66+
about the sequence if it does, raises an exception otherwise.'''
67+
directory, basename = os.path.split(path)
68+
69+
p_pos = basename.find('%')
70+
d_pos = basename.find('d', p_pos)
71+
exp = basename[p_pos : d_pos + 1]
72+
73+
padding = 0
74+
if d_pos > p_pos + 2:
75+
# %04d expression
76+
padding = int(basename[p_pos + 1 : d_pos])
77+
78+
ws_pos = basename.rfind(' ')
79+
dash_pos = basename.find('-', ws_pos)
80+
81+
prefix = basename[:p_pos]
82+
suffix = basename[d_pos + 1 : ws_pos]
83+
84+
start = int(basename[ws_pos + 2 : dash_pos])
85+
end = int(basename[dash_pos + 1 : -1])
86+
87+
if padding == 0:
88+
# No padding, calculate padding from start and end
89+
padding = len(str(end))
90+
91+
logger.debug(
92+
f'Looking for frames {start}>{end} in directory {directory} starting '
93+
f'with {prefix}, ending with {suffix} (padding: {padding})'
94+
)
95+
96+
for frame in range(start, end + 1):
97+
filename = f'{prefix}{exp % frame}{suffix}'
98+
test_path = os.path.join(directory, filename)
99+
if not os.path.exists(test_path):
100+
raise Exception(
101+
f'Image sequence member {frame} not ' f'found @ "{test_path}"!'
102+
)
103+
logger.debug(f'Frame {frame} verified: {filename}')
104+
105+
return {
106+
'directory': directory,
107+
'prefix': prefix,
108+
'suffix': suffix,
109+
'start': start,
110+
'end': end,
111+
'padding': padding,
112+
}
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# :coding: utf-8
2+
# :copyright: Copyright (c) 2024 ftrack
3+
import os.path
4+
5+
try:
6+
from PySide6 import QtWidgets, QtCore
7+
except ImportError:
8+
from PySide2 import QtWidgets, QtCore
9+
10+
from ftrack_utils.framework.config.tool import get_plugins, get_groups
11+
from ftrack_utils.string import str_version
12+
from ftrack_framework_qt.dialogs import BaseDialog
13+
from ftrack_qt.widgets.progress import ProgressWidget
14+
from ftrack_qt.utils.decorators import invoke_in_qt_main_thread
15+
from ftrack_qt.utils.widget import build_progress_data
16+
17+
18+
class StandardLoaderDialog(BaseDialog):
19+
'''Default Framework Loader dialog'''
20+
21+
name = 'framework_standard_loader_dialog'
22+
tool_config_type_filter = ['loader']
23+
ui_type = 'qt'
24+
run_button_title = 'LOAD'
25+
26+
def __init__(
27+
self,
28+
event_manager,
29+
client_id,
30+
connect_methods_callback,
31+
connect_setter_property_callback,
32+
connect_getter_property_callback,
33+
dialog_options,
34+
parent=None,
35+
):
36+
'''
37+
Initialize Mixin class loader dialog. It will load the qt dialog and
38+
mix it with the framework dialog.
39+
*event_manager*: instance of
40+
:class:`~ftrack_framework_core.event.EventManager`
41+
*client_id*: Id of the client that initializes the current dialog
42+
*connect_methods_callback*: Client callback method for the dialog to be
43+
able to execute client methods.
44+
*connect_setter_property_callback*: Client callback property setter for
45+
the dialog to be able to read client properties.
46+
*connect_getter_property_callback*: Client callback property getter for
47+
the dialog to be able to write client properties.
48+
*dialog_options*: Dictionary of arguments passed on to configure the
49+
current dialog.
50+
'''
51+
self._scroll_area = None
52+
self._scroll_area_widget = None
53+
self._progress_widget = None
54+
55+
super(StandardLoaderDialog, self).__init__(
56+
event_manager,
57+
client_id,
58+
connect_methods_callback,
59+
connect_setter_property_callback,
60+
connect_getter_property_callback,
61+
dialog_options,
62+
parent=parent,
63+
)
64+
self.resize(400, 450)
65+
self.setWindowTitle('ftrack Loader')
66+
67+
def get_entities(self):
68+
'''Get the entities to load from dialog options'''
69+
result = []
70+
for entry in self.dialog_options.get('event_data', {}).get(
71+
'selection', []
72+
):
73+
result.append(
74+
{
75+
'entity_id': entry['entityId'],
76+
'entity_type': entry['entityType'],
77+
}
78+
)
79+
return result
80+
81+
def is_compatible(self, tool_config, component):
82+
'''Check if the *tool_config* is compatible with provided *component* entity,
83+
returns True if compatible, False or error message as string otherwise
84+
'''
85+
compatible = tool_config.get('compatible')
86+
if not compatible:
87+
return f'Tool config {tool_config} is missing required loader "compatible" entry!'
88+
# Filter on combination of component name, asset_type and file extension
89+
result = False
90+
if compatible.get('component'):
91+
# Component name match?
92+
if compatible['component'].lower() == component['name'].lower():
93+
result = True
94+
else:
95+
self.logger.debug(
96+
f"Component {compatible['component']} doesn't match {component['name']}"
97+
)
98+
return False
99+
if compatible.get('asset_type'):
100+
# Asset type match?
101+
asset = component['version']['asset']
102+
asset_type = asset['type']['name']
103+
if compatible['asset_type'].lower() == asset_type.lower():
104+
result = True
105+
else:
106+
self.logger.debug(
107+
f"Asset type {compatible['asset_type']} doesn't match {asset_type}"
108+
)
109+
return False
110+
if compatible.get('supported_file_extensions'):
111+
# Any file extension match?
112+
result = False
113+
file_extension = component['file_type']
114+
for file_type in compatible.get('supported_file_extensions'):
115+
if file_type.lower() == file_extension.lower():
116+
result = True
117+
break
118+
if not result:
119+
self.logger.debug(
120+
f"File extensions {compatible['supported_file_extensions']} doesn't match component: {file_extension}"
121+
)
122+
return False
123+
if compatible.get('entity_types'):
124+
result = False
125+
for entity_type in compatible['entity_types']:
126+
if component.entity_type == entity_type:
127+
result = True
128+
break
129+
if not result:
130+
self.logger.debug(
131+
f"Component {component['name']} entity type {component.entity_type} doesn't match {compatible['entity_types']}"
132+
)
133+
return False
134+
if not result:
135+
self.logger.debug(
136+
f'Tool config {tool_config} is not compatible with component'
137+
)
138+
return result
139+
140+
def pre_build_ui(self):
141+
pass
142+
143+
def build_ui(self):
144+
# Check entities
145+
# Select the desired tool_config
146+
tool_config_message = None
147+
if 'event_data' not in self.dialog_options:
148+
tool_config_message = 'No event data provided!'
149+
elif not self.get_entities():
150+
tool_config_message = 'No entity provided to load!'
151+
elif len(self.get_entities()) != 1:
152+
tool_config_message = 'Only one entity supported!'
153+
elif self.get_entities()[0]['entity_type'].lower() != 'component':
154+
tool_config_message = 'Only components can be loaded'
155+
elif self.filtered_tool_configs.get("loader"):
156+
component_id = self.get_entities()[0]['entity_id']
157+
component = self.session.query(
158+
f'Component where id={component_id}'
159+
).first()
160+
161+
if not component:
162+
tool_config_message = f'Component not found: {component_id}'
163+
else:
164+
# Loop through tool configs, find a one that can load the component in question
165+
for tool_config in self.filtered_tool_configs["loader"]:
166+
result = self.is_compatible(tool_config, component)
167+
if result is not True:
168+
if isinstance(result, str):
169+
# Unrecoverable error
170+
tool_config_message = result
171+
break
172+
else:
173+
tool_config_name = tool_config['name']
174+
self.logger.debug(
175+
f'Using tool config {tool_config_name}'
176+
)
177+
if self.tool_config != tool_config:
178+
try:
179+
self.tool_config = tool_config
180+
except Exception as error:
181+
tool_config_message = error
182+
break
183+
self._progress_widget = ProgressWidget(
184+
'load', build_progress_data(tool_config)
185+
)
186+
self.header.set_widget(
187+
self._progress_widget.status_widget
188+
)
189+
self.overlay_layout.addWidget(
190+
self._progress_widget.overlay_widget
191+
)
192+
break
193+
if not self.tool_config and not tool_config_message:
194+
tool_config_message = f'Could not find a tool config compatible with the component {component_id}!'
195+
else:
196+
tool_config_message = 'No loader tool configs available!'
197+
198+
if not self.tool_config:
199+
self.logger.warning(tool_config_message)
200+
label_widget = QtWidgets.QLabel(f'{tool_config_message}')
201+
label_widget.setStyleSheet(
202+
"font-style: italic; font-weight: bold; color: red;"
203+
)
204+
self.tool_widget.layout().addWidget(label_widget)
205+
return
206+
207+
# Build context widgets
208+
context_plugins = get_plugins(
209+
self.tool_config, filters={'tags': ['context']}
210+
)
211+
for context_plugin in context_plugins:
212+
if not context_plugin.get('ui'):
213+
continue
214+
# Inject the entity data into the context plugin
215+
if 'options' not in context_plugin:
216+
context_plugin['options'] = {}
217+
context_plugin['options'].update(self.dialog_options)
218+
context_widget = self.init_framework_widget(context_plugin)
219+
self.tool_widget.layout().addWidget(context_widget)
220+
221+
# Build human readable loader name from tool config name
222+
loader_name_widget = QtWidgets.QWidget()
223+
loader_name_widget.setLayout(QtWidgets.QHBoxLayout())
224+
225+
label = QtWidgets.QLabel('Loader:')
226+
label.setProperty('secondary', True)
227+
loader_name_widget.layout().addWidget(label)
228+
229+
label = QtWidgets.QLabel(
230+
self.tool_config['name'].replace('-', ' ').title()
231+
)
232+
label.setProperty('h2', True)
233+
loader_name_widget.layout().addWidget(label, 100)
234+
235+
self.tool_widget.layout().addWidget(loader_name_widget)
236+
237+
# Add loader plugin(s)
238+
loader_plugins = get_plugins(
239+
self.tool_config, filters={'tags': ['loader']}
240+
)
241+
# Expect only one for now
242+
if len(loader_plugins) != 1:
243+
raise Exception('Only one(1) loader plugin is supported!')
244+
245+
for loader_plugin in loader_plugins:
246+
options = loader_plugin.get('options', {})
247+
if not loader_plugin.get('ui'):
248+
continue
249+
loader_widget = self.init_framework_widget(loader_plugin)
250+
self.tool_widget.layout().addWidget(loader_widget)
251+
252+
spacer = QtWidgets.QSpacerItem(
253+
1,
254+
1,
255+
QtWidgets.QSizePolicy.Minimum,
256+
QtWidgets.QSizePolicy.Expanding,
257+
)
258+
self.tool_widget.layout().addItem(spacer)
259+
260+
def post_build_ui(self):
261+
self._progress_widget.hide_overlay_signal.connect(
262+
self.show_main_widget
263+
)
264+
self._progress_widget.show_overlay_signal.connect(
265+
self.show_overlay_widget
266+
)
267+
268+
def _on_run_button_clicked(self):
269+
'''(Override) Drive the progress widget'''
270+
self.show_overlay_widget()
271+
self._progress_widget.run()
272+
super(StandardLoaderDialog, self)._on_run_button_clicked()
273+
274+
@invoke_in_qt_main_thread
275+
def plugin_run_callback(self, log_item):
276+
'''(Override) Pass framework log item to the progress widget'''
277+
self._progress_widget.update_phase_status(
278+
log_item.reference,
279+
log_item.status,
280+
log_message=log_item.message,
281+
time=log_item.execution_time,
282+
)
283+
284+
def closeEvent(self, event):
285+
'''(Override) Close the context and progress widgets'''
286+
self._progress_widget.teardown()
287+
self._progress_widget.deleteLater()
288+
super(StandardLoaderDialog, self).closeEvent(event)

0 commit comments

Comments
 (0)