diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py
index 368a70f395..718c4b574c 100644
--- a/openpype/hosts/flame/hooks/pre_flame_setup.py
+++ b/openpype/hosts/flame/hooks/pre_flame_setup.py
@@ -2,6 +2,7 @@ import os
import json
import tempfile
import contextlib
+import socket
from openpype.lib import (
PreLaunchHook, get_openpype_username)
from openpype.hosts import flame as opflame
@@ -32,6 +33,7 @@ class FlamePrelaunch(PreLaunchHook):
"""Hook entry method."""
project_doc = self.data["project_doc"]
user_name = get_openpype_username()
+ hostname = socket.gethostname() # not returning wiretap host name
self.log.debug("Collected user \"{}\"".format(user_name))
self.log.info(pformat(project_doc))
@@ -53,11 +55,12 @@ class FlamePrelaunch(PreLaunchHook):
"FieldDominance": "PROGRESSIVE"
}
+
data_to_script = {
# from settings
- "host_name": "localhost",
- "volume_name": "stonefs",
- "group_name": "staff",
+ "host_name": os.getenv("FLAME_WIRETAP_HOSTNAME") or hostname,
+ "volume_name": os.getenv("FLAME_WIRETAP_VOLUME"),
+ "group_name": os.getenv("FLAME_WIRETAP_GROUP"),
"color_policy": "ACES 1.1",
# from project
diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml
new file mode 100644
index 0000000000..fa43ceece7
--- /dev/null
+++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml
@@ -0,0 +1,58 @@
+
+
+ sequence
+ Creates a 8-bit Jpeg file per segment.
+
+ NONE
+
+ <name>
+ True
+ True
+
+ image
+ FX
+ NoChange
+ False
+ 10
+
+ True
+ False
+
+ audio
+ FX
+ FlattenTracks
+ True
+ 10
+
+
+
+
+ 4
+ 1
+ 2
+
+
diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml
new file mode 100644
index 0000000000..3ca185b8b4
--- /dev/null
+++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml
@@ -0,0 +1,72 @@
+
+
+ sequence
+ Create MOV H264 files per segment with thumbnail
+
+ NONE
+
+ <name>
+ True
+ True
+
+ movie
+ FX
+ FlattenTracks
+ True
+ 5
+
+ True
+ False
+
+ audio
+ Original
+ NoChange
+ True
+ 5
+
+
+
+ QuickTime
+ <segment name>
+ 0
+ PCS_709
+ None
+ Autodesk
+ Flame
+ 2021
+
+
+
+ 4
+ 1
+ 2
+
+
\ No newline at end of file
diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py
new file mode 100644
index 0000000000..b255d8d3f5
--- /dev/null
+++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py
@@ -0,0 +1,162 @@
+import os
+import io
+import ConfigParser as CP
+from xml.etree import ElementTree as ET
+from contextlib import contextmanager
+
+PLUGIN_DIR = os.path.dirname(os.path.dirname(__file__))
+EXPORT_PRESETS_DIR = os.path.join(PLUGIN_DIR, "export_preset")
+
+CONFIG_DIR = os.path.join(os.path.expanduser(
+ "~/.openpype"), "openpype_flame_to_ftrack")
+
+
+@contextmanager
+def make_temp_dir():
+ import tempfile
+
+ try:
+ dirpath = tempfile.mkdtemp()
+
+ yield dirpath
+
+ except IOError as _error:
+ raise IOError("Not able to create temp dir file: {}".format(_error))
+
+ finally:
+ pass
+
+
+@contextmanager
+def get_config(section=None):
+ cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini")
+
+ # create config dir
+ if not os.path.exists(CONFIG_DIR):
+ print("making dirs at: `{}`".format(CONFIG_DIR))
+ os.makedirs(CONFIG_DIR, mode=0o777)
+
+ # write default data to settings.ini
+ if not os.path.exists(cfg_file_path):
+ default_cfg = cfg_default()
+ config = CP.RawConfigParser()
+ config.readfp(io.BytesIO(default_cfg))
+ with open(cfg_file_path, 'wb') as cfg_file:
+ config.write(cfg_file)
+
+ try:
+ config = CP.RawConfigParser()
+ config.read(cfg_file_path)
+ if section:
+ _cfg_data = {
+ k: v
+ for s in config.sections()
+ for k, v in config.items(s)
+ if s == section
+ }
+ else:
+ _cfg_data = {s: dict(config.items(s)) for s in config.sections()}
+
+ yield _cfg_data
+
+ except IOError as _error:
+ raise IOError('Not able to read settings.ini file: {}'.format(_error))
+
+ finally:
+ pass
+
+
+def set_config(cfg_data, section=None):
+ cfg_file_path = os.path.join(CONFIG_DIR, "settings.ini")
+
+ config = CP.RawConfigParser()
+ config.read(cfg_file_path)
+
+ try:
+ if not section:
+ for section in cfg_data:
+ for key, value in cfg_data[section].items():
+ config.set(section, key, value)
+ else:
+ for key, value in cfg_data.items():
+ config.set(section, key, value)
+
+ with open(cfg_file_path, 'wb') as cfg_file:
+ config.write(cfg_file)
+
+ except IOError as _error:
+ raise IOError('Not able to write settings.ini file: {}'.format(_error))
+
+
+def cfg_default():
+ return """
+[main]
+workfile_start_frame = 1001
+shot_handles = 0
+shot_name_template = {sequence}_{shot}
+hierarchy_template = shots[Folder]/{sequence}[Sequence]
+create_task_type = Compositing
+"""
+
+
+def configure_preset(file_path, data):
+ split_fp = os.path.splitext(file_path)
+ new_file_path = split_fp[0] + "_tmp" + split_fp[-1]
+ with open(file_path, "r") as datafile:
+ tree = ET.parse(datafile)
+ for key, value in data.items():
+ for element in tree.findall(".//{}".format(key)):
+ print(element)
+ element.text = str(value)
+ tree.write(new_file_path)
+
+ return new_file_path
+
+
+def export_thumbnail(sequence, tempdir_path, data):
+ import flame
+ export_preset = os.path.join(
+ EXPORT_PRESETS_DIR,
+ "openpype_seg_thumbnails_jpg.xml"
+ )
+ new_path = configure_preset(export_preset, data)
+ poster_frame_exporter = flame.PyExporter()
+ poster_frame_exporter.foreground = True
+ poster_frame_exporter.export(sequence, new_path, tempdir_path)
+
+
+def export_video(sequence, tempdir_path, data):
+ import flame
+ export_preset = os.path.join(
+ EXPORT_PRESETS_DIR,
+ "openpype_seg_video_h264.xml"
+ )
+ new_path = configure_preset(export_preset, data)
+ poster_frame_exporter = flame.PyExporter()
+ poster_frame_exporter.foreground = True
+ poster_frame_exporter.export(sequence, new_path, tempdir_path)
+
+
+def timecode_to_frames(timecode, framerate):
+ def _seconds(value):
+ if isinstance(value, str):
+ _zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':'))
+ return sum(f * float(t) for f, t in _zip_ft)
+ elif isinstance(value, (int, float)):
+ return value / framerate
+ return 0
+
+ def _frames(seconds):
+ return seconds * framerate
+
+ def tc_to_frames(_timecode, start=None):
+ return _frames(_seconds(_timecode) - _seconds(start))
+
+ if '+' in timecode:
+ timecode = timecode.replace('+', ':')
+ elif '#' in timecode:
+ timecode = timecode.replace('#', ':')
+
+ frames = int(round(tc_to_frames(timecode, start='00:00:00:00')))
+
+ return frames
diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py
new file mode 100644
index 0000000000..26b197ee1d
--- /dev/null
+++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py
@@ -0,0 +1,448 @@
+import os
+import sys
+import six
+import re
+import json
+
+import app_utils
+
+# Fill following constants or set them via environment variable
+FTRACK_MODULE_PATH = None
+FTRACK_API_KEY = None
+FTRACK_API_USER = None
+FTRACK_SERVER = None
+
+
+def import_ftrack_api():
+ try:
+ import ftrack_api
+ return ftrack_api
+ except ImportError:
+ import sys
+ ftrk_m_p = FTRACK_MODULE_PATH or os.getenv("FTRACK_MODULE_PATH")
+ sys.path.append(ftrk_m_p)
+ import ftrack_api
+ return ftrack_api
+
+
+def get_ftrack_session():
+ import os
+ ftrack_api = import_ftrack_api()
+
+ # fill your own credentials
+ url = FTRACK_SERVER or os.getenv("FTRACK_SERVER") or ""
+ user = FTRACK_API_USER or os.getenv("FTRACK_API_USER") or ""
+ api = FTRACK_API_KEY or os.getenv("FTRACK_API_KEY") or ""
+
+ first_validation = True
+ if not user:
+ print('- Ftrack Username is not set')
+ first_validation = False
+ if not api:
+ print('- Ftrack API key is not set')
+ first_validation = False
+ if not first_validation:
+ return False
+
+ try:
+ return ftrack_api.Session(
+ server_url=url,
+ api_user=user,
+ api_key=api
+ )
+ except Exception as _e:
+ print("Can't log into Ftrack with used credentials: {}".format(_e))
+ ftrack_cred = {
+ 'Ftrack server': str(url),
+ 'Username': str(user),
+ 'API key': str(api),
+ }
+
+ item_lens = [len(key) + 1 for key in ftrack_cred]
+ justify_len = max(*item_lens)
+ for key, value in ftrack_cred.items():
+ print('{} {}'.format((key + ':').ljust(justify_len, ' '), value))
+ return False
+
+
+def get_project_task_types(project_entity):
+ tasks = {}
+ proj_template = project_entity['project_schema']
+ temp_task_types = proj_template['_task_type_schema']['types']
+
+ for type in temp_task_types:
+ if type['name'] not in tasks:
+ tasks[type['name']] = type
+
+ return tasks
+
+
+class FtrackComponentCreator:
+ default_location = "ftrack.server"
+ ftrack_locations = {}
+ thumbnails = []
+ videos = []
+ temp_dir = None
+
+ def __init__(self, session):
+ self.session = session
+ self._get_ftrack_location()
+
+ def generate_temp_data(self, selection, change_preset_data):
+ with app_utils.make_temp_dir() as tempdir_path:
+ for seq in selection:
+ app_utils.export_thumbnail(
+ seq, tempdir_path, change_preset_data)
+ app_utils.export_video(seq, tempdir_path, change_preset_data)
+
+ return tempdir_path
+
+ def collect_generated_data(self, tempdir_path):
+ temp_files = os.listdir(tempdir_path)
+ self.thumbnails = [f for f in temp_files if "jpg" in f]
+ self.videos = [f for f in temp_files if "mov" in f]
+ self.temp_dir = tempdir_path
+
+ def get_thumb_path(self, shot_name):
+ # get component files
+ thumb_f = next((f for f in self.thumbnails if shot_name in f), None)
+ return os.path.join(self.temp_dir, thumb_f)
+
+ def get_video_path(self, shot_name):
+ # get component files
+ video_f = next((f for f in self.videos if shot_name in f), None)
+ return os.path.join(self.temp_dir, video_f)
+
+ def close(self):
+ self.ftrack_locations = {}
+ self.session = None
+
+ def create_comonent(self, shot_entity, data, assetversion_entity=None):
+ self.shot_entity = shot_entity
+ location = self._get_ftrack_location()
+
+ file_path = data["file_path"]
+
+ # get extension
+ file = os.path.basename(file_path)
+ _n, ext = os.path.splitext(file)
+
+ name = "ftrackreview-mp4" if "mov" in ext else "thumbnail"
+
+ component_data = {
+ "name": name,
+ "file_path": file_path,
+ "file_type": ext,
+ "location": location
+ }
+
+ if name == "ftrackreview-mp4":
+ duration = data["duration"]
+ handles = data["handles"]
+ fps = data["fps"]
+ component_data["metadata"] = {
+ 'ftr_meta': json.dumps({
+ 'frameIn': int(0),
+ 'frameOut': int(duration + (handles * 2)),
+ 'frameRate': float(fps)
+ })
+ }
+ if not assetversion_entity:
+ # get assettype entity from session
+ assettype_entity = self._get_assettype({"short": "reference"})
+
+ # get or create asset entity from session
+ asset_entity = self._get_asset({
+ "name": "plateReference",
+ "type": assettype_entity,
+ "parent": self.shot_entity
+ })
+
+ # get or create assetversion entity from session
+ assetversion_entity = self._get_assetversion({
+ "version": 0,
+ "asset": asset_entity
+ })
+
+ # get or create component entity
+ self._set_component(component_data, {
+ "name": name,
+ "version": assetversion_entity,
+ })
+
+ return assetversion_entity
+
+ def _overwrite_members(self, entity, data):
+ origin_location = self._get_ftrack_location("ftrack.origin")
+ location = data.pop("location")
+
+ self._remove_component_from_location(entity, location)
+
+ entity["file_type"] = data["file_type"]
+
+ try:
+ origin_location.add_component(
+ entity, data["file_path"]
+ )
+ # Add components to location.
+ location.add_component(
+ entity, origin_location, recursive=True)
+ except Exception as __e:
+ print("Error: {}".format(__e))
+ self._remove_component_from_location(entity, origin_location)
+ origin_location.add_component(
+ entity, data["file_path"]
+ )
+ # Add components to location.
+ location.add_component(
+ entity, origin_location, recursive=True)
+
+ def _remove_component_from_location(self, entity, location):
+ print(location)
+ # Removing existing members from location
+ components = list(entity.get("members", []))
+ components += [entity]
+ for component in components:
+ for loc in component.get("component_locations", []):
+ if location["id"] == loc["location_id"]:
+ print("<< Removing component: {}".format(component))
+ location.remove_component(
+ component, recursive=False
+ )
+
+ # Deleting existing members on component entity
+ for member in entity.get("members", []):
+ self.session.delete(member)
+ print("<< Deleting member: {}".format(member))
+ del(member)
+
+ self._commit()
+
+ # Reset members in memory
+ if "members" in entity.keys():
+ entity["members"] = []
+
+ def _get_assettype(self, data):
+ return self.session.query(
+ self._query("AssetType", data)).first()
+
+ def _set_component(self, comp_data, base_data):
+ component_metadata = comp_data.pop("metadata", {})
+
+ component_entity = self.session.query(
+ self._query("Component", base_data)
+ ).first()
+
+ if component_entity:
+ # overwrite existing members in component enity
+ # - get data for member from `ftrack.origin` location
+ self._overwrite_members(component_entity, comp_data)
+
+ # Adding metadata
+ existing_component_metadata = component_entity["metadata"]
+ existing_component_metadata.update(component_metadata)
+ component_entity["metadata"] = existing_component_metadata
+ return
+
+ assetversion_entity = base_data["version"]
+ location = comp_data.pop("location")
+
+ component_entity = assetversion_entity.create_component(
+ comp_data["file_path"],
+ data=comp_data,
+ location=location
+ )
+
+ # Adding metadata
+ existing_component_metadata = component_entity["metadata"]
+ existing_component_metadata.update(component_metadata)
+ component_entity["metadata"] = existing_component_metadata
+
+ if comp_data["name"] == "thumbnail":
+ self.shot_entity["thumbnail_id"] = component_entity["id"]
+ assetversion_entity["thumbnail_id"] = component_entity["id"]
+
+ self._commit()
+
+ def _get_asset(self, data):
+ # first find already created
+ asset_entity = self.session.query(
+ self._query("Asset", data)
+ ).first()
+
+ if asset_entity:
+ return asset_entity
+
+ asset_entity = self.session.create("Asset", data)
+
+ # _commit if created
+ self._commit()
+
+ return asset_entity
+
+ def _get_assetversion(self, data):
+ assetversion_entity = self.session.query(
+ self._query("AssetVersion", data)
+ ).first()
+
+ if assetversion_entity:
+ return assetversion_entity
+
+ assetversion_entity = self.session.create("AssetVersion", data)
+
+ # _commit if created
+ self._commit()
+
+ return assetversion_entity
+
+ def _commit(self):
+ try:
+ self.session.commit()
+ except Exception:
+ tp, value, tb = sys.exc_info()
+ # self.session.rollback()
+ # self.session._configure_locations()
+ six.reraise(tp, value, tb)
+
+ def _get_ftrack_location(self, name=None):
+ name = name or self.default_location
+
+ if name in self.ftrack_locations:
+ return self.ftrack_locations[name]
+
+ location = self.session.query(
+ 'Location where name is "{}"'.format(name)
+ ).one()
+ self.ftrack_locations[name] = location
+ return location
+
+ def _query(self, entitytype, data):
+ """ Generate a query expression from data supplied.
+
+ If a value is not a string, we'll add the id of the entity to the
+ query.
+
+ Args:
+ entitytype (str): The type of entity to query.
+ data (dict): The data to identify the entity.
+ exclusions (list): All keys to exclude from the query.
+
+ Returns:
+ str: String query to use with "session.query"
+ """
+ queries = []
+ if sys.version_info[0] < 3:
+ for key, value in data.items():
+ if not isinstance(value, (str, int)):
+ print("value: {}".format(value))
+ if "id" in value.keys():
+ queries.append(
+ "{0}.id is \"{1}\"".format(key, value["id"])
+ )
+ else:
+ queries.append("{0} is \"{1}\"".format(key, value))
+ else:
+ for key, value in data.items():
+ if not isinstance(value, (str, int)):
+ print("value: {}".format(value))
+ if "id" in value.keys():
+ queries.append(
+ "{0}.id is \"{1}\"".format(key, value["id"])
+ )
+ else:
+ queries.append("{0} is \"{1}\"".format(key, value))
+
+ query = (
+ "select id from " + entitytype + " where " + " and ".join(queries)
+ )
+ print(query)
+ return query
+
+
+class FtrackEntityOperator:
+ def __init__(self, session, project_entity):
+ self.session = session
+ self.project_entity = project_entity
+
+ def commit(self):
+ try:
+ self.session.commit()
+ except Exception:
+ tp, value, tb = sys.exc_info()
+ self.session.rollback()
+ self.session._configure_locations()
+ six.reraise(tp, value, tb)
+
+ def create_ftrack_entity(self, session, type, name, parent=None):
+ parent = parent or self.project_entity
+ entity = session.create(type, {
+ 'name': name,
+ 'parent': parent
+ })
+ try:
+ session.commit()
+ except Exception:
+ tp, value, tb = sys.exc_info()
+ session.rollback()
+ session._configure_locations()
+ six.reraise(tp, value, tb)
+ return entity
+
+ def get_ftrack_entity(self, session, type, name, parent):
+ query = '{} where name is "{}" and project_id is "{}"'.format(
+ type, name, self.project_entity["id"])
+
+ try:
+ entity = session.query(query).one()
+ except Exception:
+ entity = None
+
+ # if entity doesnt exist then create one
+ if not entity:
+ entity = self.create_ftrack_entity(
+ session,
+ type,
+ name,
+ parent
+ )
+
+ return entity
+
+ def create_parents(self, template):
+ parents = []
+ t_split = template.split("/")
+ replace_patern = re.compile(r"(\[.*\])")
+ type_patern = re.compile(r"\[(.*)\]")
+
+ for t_s in t_split:
+ match_type = type_patern.findall(t_s)
+ if not match_type:
+ raise Exception((
+ "Missing correct type flag in : {}"
+ "/n Example: name[Type]").format(
+ t_s)
+ )
+ new_name = re.sub(replace_patern, "", t_s)
+ f_type = match_type.pop()
+
+ parents.append((new_name, f_type))
+
+ return parents
+
+ def create_task(self, task_type, task_types, parent):
+ existing_task = [
+ child for child in parent['children']
+ if child.entity_type.lower() == 'task'
+ if child['name'].lower() in task_type.lower()
+ ]
+
+ if existing_task:
+ return existing_task.pop()
+
+ task = self.session.create('Task', {
+ "name": task_type.lower(),
+ "parent": parent
+ })
+ task["type"] = task_types[task_type]
+
+ return task
diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py
new file mode 100644
index 0000000000..9e39147776
--- /dev/null
+++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py
@@ -0,0 +1,524 @@
+from PySide2 import QtWidgets, QtCore
+
+import uiwidgets
+import app_utils
+import ftrack_lib
+
+
+def clear_inner_modules():
+ import sys
+
+ if "ftrack_lib" in sys.modules.keys():
+ del sys.modules["ftrack_lib"]
+ print("Ftrack Lib module removed from sys.modules")
+
+ if "app_utils" in sys.modules.keys():
+ del sys.modules["app_utils"]
+ print("app_utils module removed from sys.modules")
+
+ if "uiwidgets" in sys.modules.keys():
+ del sys.modules["uiwidgets"]
+ print("uiwidgets module removed from sys.modules")
+
+
+class MainWindow(QtWidgets.QWidget):
+
+ def __init__(self, klass, *args, **kwargs):
+ super(MainWindow, self).__init__(*args, **kwargs)
+ self.panel_class = klass
+
+ def closeEvent(self, event):
+ # clear all temp data
+ print("Removing temp data")
+ self.panel_class.clear_temp_data()
+ self.panel_class.close()
+ clear_inner_modules()
+ # now the panel can be closed
+ event.accept()
+
+
+class FlameToFtrackPanel(object):
+ session = None
+ temp_data_dir = None
+ processed_components = []
+ project_entity = None
+ task_types = {}
+ all_task_types = {}
+
+ # TreeWidget
+ columns = {
+ "Sequence name": {
+ "columnWidth": 200,
+ "order": 0
+ },
+ "Shot name": {
+ "columnWidth": 200,
+ "order": 1
+ },
+ "Clip duration": {
+ "columnWidth": 100,
+ "order": 2
+ },
+ "Shot description": {
+ "columnWidth": 500,
+ "order": 3
+ },
+ "Task description": {
+ "columnWidth": 500,
+ "order": 4
+ },
+ }
+
+ def __init__(self, selection):
+ print(selection)
+
+ self.session = ftrack_lib.get_ftrack_session()
+ self.selection = selection
+ self.window = MainWindow(self)
+
+ # creating ui
+ self.window.setMinimumSize(1500, 600)
+ self.window.setWindowTitle('Sequence Shots to Ftrack')
+ self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
+ self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
+ self.window.setFocusPolicy(QtCore.Qt.StrongFocus)
+ self.window.setStyleSheet('background-color: #313131')
+
+ self._create_project_widget()
+ self._create_tree_widget()
+ self._set_sequence_params()
+ self._generate_widgets()
+ self._generate_layouts()
+ self._timeline_info()
+ self._fix_resolution()
+
+ self.window.show()
+
+ def _generate_widgets(self):
+ with app_utils.get_config("main") as cfg_data:
+ cfg_d = cfg_data
+
+ self._create_task_type_widget(cfg_d)
+
+ # input fields
+ self.shot_name_label = uiwidgets.FlameLabel(
+ 'Shot name template', 'normal', self.window)
+ self.shot_name_template_input = uiwidgets.FlameLineEdit(
+ cfg_d["shot_name_template"], self.window)
+
+ self.hierarchy_label = uiwidgets.FlameLabel(
+ 'Parents template', 'normal', self.window)
+ self.hierarchy_template_input = uiwidgets.FlameLineEdit(
+ cfg_d["hierarchy_template"], self.window)
+
+ self.start_frame_label = uiwidgets.FlameLabel(
+ 'Workfile start frame', 'normal', self.window)
+ self.start_frame_input = uiwidgets.FlameLineEdit(
+ cfg_d["workfile_start_frame"], self.window)
+
+ self.handles_label = uiwidgets.FlameLabel(
+ 'Shot handles', 'normal', self.window)
+ self.handles_input = uiwidgets.FlameLineEdit(
+ cfg_d["shot_handles"], self.window)
+
+ self.width_label = uiwidgets.FlameLabel(
+ 'Sequence width', 'normal', self.window)
+ self.width_input = uiwidgets.FlameLineEdit(
+ str(self.seq_width), self.window)
+
+ self.height_label = uiwidgets.FlameLabel(
+ 'Sequence height', 'normal', self.window)
+ self.height_input = uiwidgets.FlameLineEdit(
+ str(self.seq_height), self.window)
+
+ self.pixel_aspect_label = uiwidgets.FlameLabel(
+ 'Pixel aspect ratio', 'normal', self.window)
+ self.pixel_aspect_input = uiwidgets.FlameLineEdit(
+ str(1.00), self.window)
+
+ self.fps_label = uiwidgets.FlameLabel(
+ 'Frame rate', 'normal', self.window)
+ self.fps_input = uiwidgets.FlameLineEdit(
+ str(self.fps), self.window)
+
+ # Button
+ self.select_all_btn = uiwidgets.FlameButton(
+ 'Select All', self.select_all, self.window)
+
+ self.remove_temp_data_btn = uiwidgets.FlameButton(
+ 'Remove temp data', self.clear_temp_data, self.window)
+
+ self.ftrack_send_btn = uiwidgets.FlameButton(
+ 'Send to Ftrack', self._send_to_ftrack, self.window)
+
+ def _generate_layouts(self):
+ # left props
+ v_shift = 0
+ prop_layout_l = QtWidgets.QGridLayout()
+ prop_layout_l.setHorizontalSpacing(30)
+ if self.project_selector_enabled:
+ prop_layout_l.addWidget(self.project_select_label, v_shift, 0)
+ prop_layout_l.addWidget(self.project_select_input, v_shift, 1)
+ v_shift += 1
+ prop_layout_l.addWidget(self.shot_name_label, (v_shift + 0), 0)
+ prop_layout_l.addWidget(
+ self.shot_name_template_input, (v_shift + 0), 1)
+ prop_layout_l.addWidget(self.hierarchy_label, (v_shift + 1), 0)
+ prop_layout_l.addWidget(
+ self.hierarchy_template_input, (v_shift + 1), 1)
+ prop_layout_l.addWidget(self.start_frame_label, (v_shift + 2), 0)
+ prop_layout_l.addWidget(self.start_frame_input, (v_shift + 2), 1)
+ prop_layout_l.addWidget(self.handles_label, (v_shift + 3), 0)
+ prop_layout_l.addWidget(self.handles_input, (v_shift + 3), 1)
+ prop_layout_l.addWidget(self.task_type_label, (v_shift + 4), 0)
+ prop_layout_l.addWidget(
+ self.task_type_input, (v_shift + 4), 1)
+
+ # right props
+ prop_widget_r = QtWidgets.QWidget(self.window)
+ prop_layout_r = QtWidgets.QGridLayout(prop_widget_r)
+ prop_layout_r.setHorizontalSpacing(30)
+ prop_layout_r.setAlignment(
+ QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
+ prop_layout_r.setContentsMargins(0, 0, 0, 0)
+ prop_layout_r.addWidget(self.width_label, 1, 0)
+ prop_layout_r.addWidget(self.width_input, 1, 1)
+ prop_layout_r.addWidget(self.height_label, 2, 0)
+ prop_layout_r.addWidget(self.height_input, 2, 1)
+ prop_layout_r.addWidget(self.pixel_aspect_label, 3, 0)
+ prop_layout_r.addWidget(self.pixel_aspect_input, 3, 1)
+ prop_layout_r.addWidget(self.fps_label, 4, 0)
+ prop_layout_r.addWidget(self.fps_input, 4, 1)
+
+ # prop layout
+ prop_main_layout = QtWidgets.QHBoxLayout()
+ prop_main_layout.addLayout(prop_layout_l, 1)
+ prop_main_layout.addSpacing(20)
+ prop_main_layout.addWidget(prop_widget_r, 1)
+
+ # buttons layout
+ hbox = QtWidgets.QHBoxLayout()
+ hbox.addWidget(self.remove_temp_data_btn)
+ hbox.addWidget(self.select_all_btn)
+ hbox.addWidget(self.ftrack_send_btn)
+
+ # put all layouts together
+ main_frame = QtWidgets.QVBoxLayout(self.window)
+ main_frame.setMargin(20)
+ main_frame.addLayout(prop_main_layout)
+ main_frame.addWidget(self.tree)
+ main_frame.addLayout(hbox)
+
+ def _set_sequence_params(self):
+ for select in self.selection:
+ self.seq_height = select.height
+ self.seq_width = select.width
+ self.fps = float(str(select.frame_rate)[:-4])
+ break
+
+ def _create_task_type_widget(self, cfg_d):
+ print(self.project_entity)
+ self.task_types = ftrack_lib.get_project_task_types(
+ self.project_entity)
+
+ self.task_type_label = uiwidgets.FlameLabel(
+ 'Create Task (type)', 'normal', self.window)
+ self.task_type_input = uiwidgets.FlamePushButtonMenu(
+ cfg_d["create_task_type"], self.task_types.keys(), self.window)
+
+ def _create_project_widget(self):
+ import flame
+ # get project name from flame current project
+ self.project_name = flame.project.current_project.name
+
+ # get project from ftrack -
+ # ftrack project name has to be the same as flame project!
+ query = 'Project where full_name is "{}"'.format(self.project_name)
+
+ # globally used variables
+ self.project_entity = self.session.query(query).first()
+
+ self.project_selector_enabled = bool(not self.project_entity)
+
+ if self.project_selector_enabled:
+ self.all_projects = self.session.query(
+ "Project where status is active").all()
+ self.project_entity = self.all_projects[0]
+ project_names = [p["full_name"] for p in self.all_projects]
+ self.all_task_types = {
+ p["full_name"]: ftrack_lib.get_project_task_types(p).keys()
+ for p in self.all_projects
+ }
+ self.project_select_label = uiwidgets.FlameLabel(
+ 'Select Ftrack project', 'normal', self.window)
+ self.project_select_input = uiwidgets.FlamePushButtonMenu(
+ self.project_entity["full_name"], project_names, self.window)
+ self.project_select_input.selection_changed.connect(
+ self._on_project_changed)
+
+ def _create_tree_widget(self):
+ ordered_column_labels = self.columns.keys()
+ for _name, _value in self.columns.items():
+ ordered_column_labels.pop(_value["order"])
+ ordered_column_labels.insert(_value["order"], _name)
+
+ self.tree = uiwidgets.FlameTreeWidget(
+ ordered_column_labels, self.window)
+
+ # Allow multiple items in tree to be selected
+ self.tree.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
+
+ # Set tree column width
+ for _name, _val in self.columns.items():
+ self.tree.setColumnWidth(
+ _val["order"],
+ _val["columnWidth"]
+ )
+
+ # Prevent weird characters when shrinking tree columns
+ self.tree.setTextElideMode(QtCore.Qt.ElideNone)
+
+ def _resolve_project_entity(self):
+ if self.project_selector_enabled:
+ selected_project_name = self.project_select_input.text()
+ self.project_entity = next(
+ (p for p in self.all_projects
+ if p["full_name"] in selected_project_name),
+ None
+ )
+
+ def _save_ui_state_to_cfg(self):
+ _cfg_data_back = {
+ "shot_name_template": self.shot_name_template_input.text(),
+ "workfile_start_frame": self.start_frame_input.text(),
+ "shot_handles": self.handles_input.text(),
+ "hierarchy_template": self.hierarchy_template_input.text(),
+ "create_task_type": self.task_type_input.text()
+ }
+
+ # add cfg data back to settings.ini
+ app_utils.set_config(_cfg_data_back, "main")
+
+ def _send_to_ftrack(self):
+ # resolve active project and add it to self.project_entity
+ self._resolve_project_entity()
+ self._save_ui_state_to_cfg()
+
+ # get hanldes from gui input
+ handles = self.handles_input.text()
+
+ # get frame start from gui input
+ frame_start = int(self.start_frame_input.text())
+
+ # get task type from gui input
+ task_type = self.task_type_input.text()
+
+ # get resolution from gui inputs
+ fps = self.fps_input.text()
+
+ entity_operator = ftrack_lib.FtrackEntityOperator(
+ self.session, self.project_entity)
+ component_creator = ftrack_lib.FtrackComponentCreator(self.session)
+
+ if not self.temp_data_dir:
+ self.window.hide()
+ self.temp_data_dir = component_creator.generate_temp_data(
+ self.selection,
+ {
+ "nbHandles": handles
+ }
+ )
+ self.window.show()
+
+ # collect generated files to list data for farther use
+ component_creator.collect_generated_data(self.temp_data_dir)
+
+ # Get all selected items from treewidget
+ for item in self.tree.selectedItems():
+ # frame ranges
+ frame_duration = int(item.text(2))
+ frame_end = frame_start + frame_duration
+
+ # description
+ shot_description = item.text(3)
+ task_description = item.text(4)
+
+ # other
+ sequence_name = item.text(0)
+ shot_name = item.text(1)
+
+ thumb_fp = component_creator.get_thumb_path(shot_name)
+ video_fp = component_creator.get_video_path(shot_name)
+
+ print("processed comps: {}".format(self.processed_components))
+ print("processed thumb_fp: {}".format(thumb_fp))
+
+ processed = False
+ if thumb_fp not in self.processed_components:
+ self.processed_components.append(thumb_fp)
+ else:
+ processed = True
+
+ print("processed: {}".format(processed))
+
+ # populate full shot info
+ shot_attributes = {
+ "sequence": sequence_name,
+ "shot": shot_name,
+ "task": task_type
+ }
+
+ # format shot name template
+ _shot_name = self.shot_name_template_input.text().format(
+ **shot_attributes)
+
+ # format hierarchy template
+ _hierarchy_text = self.hierarchy_template_input.text().format(
+ **shot_attributes)
+ print(_hierarchy_text)
+
+ # solve parents
+ parents = entity_operator.create_parents(_hierarchy_text)
+ print(parents)
+
+ # obtain shot parents entities
+ _parent = None
+ for _name, _type in parents:
+ p_entity = entity_operator.get_ftrack_entity(
+ self.session,
+ _type,
+ _name,
+ _parent
+ )
+ print(p_entity)
+ _parent = p_entity
+
+ # obtain shot ftrack entity
+ f_s_entity = entity_operator.get_ftrack_entity(
+ self.session,
+ "Shot",
+ _shot_name,
+ _parent
+ )
+ print("Shot entity is: {}".format(f_s_entity))
+
+ if not processed:
+ # first create thumbnail and get version entity
+ assetversion_entity = component_creator.create_comonent(
+ f_s_entity, {
+ "file_path": thumb_fp
+ }
+ )
+
+ # secondly add video to version entity
+ component_creator.create_comonent(
+ f_s_entity, {
+ "file_path": video_fp,
+ "duration": frame_duration,
+ "handles": int(handles),
+ "fps": float(fps)
+ }, assetversion_entity
+ )
+
+ # create custom attributtes
+ custom_attrs = {
+ "frameStart": frame_start,
+ "frameEnd": frame_end,
+ "handleStart": int(handles),
+ "handleEnd": int(handles),
+ "resolutionWidth": int(self.width_input.text()),
+ "resolutionHeight": int(self.height_input.text()),
+ "pixelAspect": float(self.pixel_aspect_input.text()),
+ "fps": float(fps)
+ }
+
+ # update custom attributes on shot entity
+ for key in custom_attrs:
+ f_s_entity['custom_attributes'][key] = custom_attrs[key]
+
+ task_entity = entity_operator.create_task(
+ task_type, self.task_types, f_s_entity)
+
+ # Create notes.
+ user = self.session.query(
+ "User where username is \"{}\"".format(self.session.api_user)
+ ).first()
+
+ f_s_entity.create_note(shot_description, author=user)
+
+ if task_description:
+ task_entity.create_note(task_description, user)
+
+ entity_operator.commit()
+
+ component_creator.close()
+
+ def _fix_resolution(self):
+ # Center window in linux
+ resolution = QtWidgets.QDesktopWidget().screenGeometry()
+ self.window.move(
+ (resolution.width() / 2) - (self.window.frameSize().width() / 2),
+ (resolution.height() / 2) - (self.window.frameSize().height() / 2))
+
+ def _on_project_changed(self):
+ task_types = self.all_task_types[self.project_name]
+ self.task_type_input.set_menu_options(task_types)
+
+ def _timeline_info(self):
+ # identificar as informacoes dos segmentos na timeline
+ for sequence in self.selection:
+ frame_rate = float(str(sequence.frame_rate)[:-4])
+ for ver in sequence.versions:
+ for tracks in ver.tracks:
+ for segment in tracks.segments:
+ print(segment.attributes)
+ if str(segment.name)[1:-1] == "":
+ continue
+ # get clip frame duration
+ record_duration = str(segment.record_duration)[1:-1]
+ clip_duration = app_utils.timecode_to_frames(
+ record_duration, frame_rate)
+
+ # populate shot source metadata
+ shot_description = ""
+ for attr in ["tape_name", "source_name", "head",
+ "tail", "file_path"]:
+ if not hasattr(segment, attr):
+ continue
+ _value = getattr(segment, attr)
+ _label = attr.replace("_", " ").capitalize()
+ row = "{}: {}\n".format(_label, _value)
+ shot_description += row
+
+ # Add timeline segment to tree
+ QtWidgets.QTreeWidgetItem(self.tree, [
+ str(sequence.name)[1:-1], # seq
+ str(segment.name)[1:-1], # shot
+ str(clip_duration), # clip duration
+ shot_description, # shot description
+ str(segment.comment)[1:-1] # task description
+ ]).setFlags(
+ QtCore.Qt.ItemIsEditable
+ | QtCore.Qt.ItemIsEnabled
+ | QtCore.Qt.ItemIsSelectable
+ )
+
+ # Select top item in tree
+ self.tree.setCurrentItem(self.tree.topLevelItem(0))
+
+ def select_all(self, ):
+ self.tree.selectAll()
+
+ def clear_temp_data(self):
+ import shutil
+
+ self.processed_components = []
+
+ if self.temp_data_dir:
+ shutil.rmtree(self.temp_data_dir)
+ self.temp_data_dir = None
+ print("All Temp data were destroied ...")
+
+ def close(self):
+ self._save_ui_state_to_cfg()
+ self.session.close()
diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py
new file mode 100644
index 0000000000..0d4807a4ea
--- /dev/null
+++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py
@@ -0,0 +1,212 @@
+from PySide2 import QtWidgets, QtCore
+
+
+class FlameLabel(QtWidgets.QLabel):
+ """
+ Custom Qt Flame Label Widget
+
+ For different label looks set label_type as:
+ 'normal', 'background', or 'outline'
+
+ To use:
+
+ label = FlameLabel('Label Name', 'normal', window)
+ """
+
+ def __init__(self, label_name, label_type, parent_window, *args, **kwargs):
+ super(FlameLabel, self).__init__(*args, **kwargs)
+
+ self.setText(label_name)
+ self.setParent(parent_window)
+ self.setMinimumSize(130, 28)
+ self.setMaximumHeight(28)
+ self.setFocusPolicy(QtCore.Qt.NoFocus)
+
+ # Set label stylesheet based on label_type
+
+ if label_type == 'normal':
+ self.setStyleSheet(
+ 'QLabel {color: #9a9a9a; border-bottom: 1px inset #282828; font: 14px "Discreet"}' # noqa
+ 'QLabel:disabled {color: #6a6a6a}'
+ )
+ elif label_type == 'background':
+ self.setAlignment(QtCore.Qt.AlignCenter)
+ self.setStyleSheet(
+ 'color: #9a9a9a; background-color: #393939; font: 14px "Discreet"' # noqa
+ )
+ elif label_type == 'outline':
+ self.setAlignment(QtCore.Qt.AlignCenter)
+ self.setStyleSheet(
+ 'color: #9a9a9a; background-color: #212121; border: 1px solid #404040; font: 14px "Discreet"' # noqa
+ )
+
+
+class FlameLineEdit(QtWidgets.QLineEdit):
+ """
+ Custom Qt Flame Line Edit Widget
+
+ Main window should include this:
+ window.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+ To use:
+
+ line_edit = FlameLineEdit('Some text here', window)
+ """
+
+ def __init__(self, text, parent_window, *args, **kwargs):
+ super(FlameLineEdit, self).__init__(*args, **kwargs)
+
+ self.setText(text)
+ self.setParent(parent_window)
+ self.setMinimumHeight(28)
+ self.setMinimumWidth(110)
+ self.setStyleSheet(
+ 'QLineEdit {color: #9a9a9a; background-color: #373e47; selection-color: #262626; selection-background-color: #b8b1a7; font: 14px "Discreet"}' # noqa
+ 'QLineEdit:focus {background-color: #474e58}' # noqa
+ 'QLineEdit:disabled {color: #6a6a6a; background-color: #373737}'
+ )
+
+
+class FlameTreeWidget(QtWidgets.QTreeWidget):
+ """
+ Custom Qt Flame Tree Widget
+
+ To use:
+
+ tree_headers = ['Header1', 'Header2', 'Header3', 'Header4']
+ tree = FlameTreeWidget(tree_headers, window)
+ """
+
+ def __init__(self, tree_headers, parent_window, *args, **kwargs):
+ super(FlameTreeWidget, self).__init__(*args, **kwargs)
+
+ self.setMinimumWidth(1000)
+ self.setMinimumHeight(300)
+ self.setSortingEnabled(True)
+ self.sortByColumn(0, QtCore.Qt.AscendingOrder)
+ self.setAlternatingRowColors(True)
+ self.setFocusPolicy(QtCore.Qt.NoFocus)
+ self.setStyleSheet(
+ 'QTreeWidget {color: #9a9a9a; background-color: #2a2a2a; alternate-background-color: #2d2d2d; font: 14px "Discreet"}' # noqa
+ 'QTreeWidget::item:selected {color: #d9d9d9; background-color: #474747; border: 1px solid #111111}' # noqa
+ 'QHeaderView {color: #9a9a9a; background-color: #393939; font: 14px "Discreet"}' # noqa
+ 'QTreeWidget::item:selected {selection-background-color: #111111}'
+ 'QMenu {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' # noqa
+ 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}'
+ )
+ self.verticalScrollBar().setStyleSheet('color: #818181')
+ self.horizontalScrollBar().setStyleSheet('color: #818181')
+ self.setHeaderLabels(tree_headers)
+
+
+class FlameButton(QtWidgets.QPushButton):
+ """
+ Custom Qt Flame Button Widget
+
+ To use:
+
+ button = FlameButton('Button Name', do_this_when_pressed, window)
+ """
+
+ def __init__(self, button_name, do_when_pressed, parent_window,
+ *args, **kwargs):
+ super(FlameButton, self).__init__(*args, **kwargs)
+
+ self.setText(button_name)
+ self.setParent(parent_window)
+ self.setMinimumSize(QtCore.QSize(110, 28))
+ self.setMaximumSize(QtCore.QSize(110, 28))
+ self.setFocusPolicy(QtCore.Qt.NoFocus)
+ self.clicked.connect(do_when_pressed)
+ self.setStyleSheet(
+ 'QPushButton {color: #9a9a9a; background-color: #424142; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' # noqa
+ 'QPushButton:pressed {color: #d9d9d9; background-color: #4f4f4f; border-top: 1px inset #666666; font: italic}' # noqa
+ 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}' # noqa
+ )
+
+
+class FlamePushButton(QtWidgets.QPushButton):
+ """
+ Custom Qt Flame Push Button Widget
+
+ To use:
+
+ pushbutton = FlamePushButton(' Button Name', True_or_False, window)
+ """
+
+ def __init__(self, button_name, button_checked, parent_window,
+ *args, **kwargs):
+ super(FlamePushButton, self).__init__(*args, **kwargs)
+
+ self.setText(button_name)
+ self.setParent(parent_window)
+ self.setCheckable(True)
+ self.setChecked(button_checked)
+ self.setMinimumSize(155, 28)
+ self.setMaximumSize(155, 28)
+ self.setFocusPolicy(QtCore.Qt.NoFocus)
+ self.setStyleSheet(
+ 'QPushButton {color: #9a9a9a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #424142, stop: .94 #2e3b48); text-align: left; border-top: 1px inset #555555; border-bottom: 1px inset black; font: 14px "Discreet"}' # noqa
+ 'QPushButton:checked {color: #d9d9d9; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #4f4f4f, stop: .94 #5a7fb4); font: italic; border: 1px inset black; border-bottom: 1px inset #404040; border-right: 1px inset #404040}' # noqa
+ 'QPushButton:disabled {color: #6a6a6a; background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: .93 #383838, stop: .94 #353535); font: light; border-top: 1px solid #575757; border-bottom: 1px solid #242424; border-right: 1px solid #353535; border-left: 1px solid #353535}' # noqa
+ 'QToolTip {color: black; background-color: #ffffde; border: black solid 1px}' # noqa
+ )
+
+
+class FlamePushButtonMenu(QtWidgets.QPushButton):
+ """
+ Custom Qt Flame Menu Push Button Widget
+
+ To use:
+
+ push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4']
+ menu_push_button = FlamePushButtonMenu('push_button_name',
+ push_button_menu_options, window)
+
+ or
+
+ push_button_menu_options = ['Item 1', 'Item 2', 'Item 3', 'Item 4']
+ menu_push_button = FlamePushButtonMenu(push_button_menu_options[0],
+ push_button_menu_options, window)
+ """
+ selection_changed = QtCore.Signal(str)
+
+ def __init__(self, button_name, menu_options, parent_window,
+ *args, **kwargs):
+ super(FlamePushButtonMenu, self).__init__(*args, **kwargs)
+
+ self.setParent(parent_window)
+ self.setMinimumHeight(28)
+ self.setMinimumWidth(110)
+ self.setFocusPolicy(QtCore.Qt.NoFocus)
+ self.setStyleSheet(
+ 'QPushButton {color: #9a9a9a; background-color: #24303d; font: 14px "Discreet"}' # noqa
+ 'QPushButton:disabled {color: #747474; background-color: #353535; border-top: 1px solid #444444; border-bottom: 1px solid #242424}' # noqa
+ )
+
+ pushbutton_menu = QtWidgets.QMenu(parent_window)
+ pushbutton_menu.setFocusPolicy(QtCore.Qt.NoFocus)
+ pushbutton_menu.setStyleSheet(
+ 'QMenu {color: #9a9a9a; background-color:#24303d; font: 14px "Discreet"}' # noqa
+ 'QMenu::item:selected {color: #d9d9d9; background-color: #3a4551}'
+ )
+
+ self._pushbutton_menu = pushbutton_menu
+ self.setMenu(pushbutton_menu)
+ self.set_menu_options(menu_options, button_name)
+
+ def set_menu_options(self, menu_options, current_option=None):
+ self._pushbutton_menu.clear()
+ current_option = current_option or menu_options[0]
+
+ for option in menu_options:
+ action = self._pushbutton_menu.addAction(option)
+ action.triggered.connect(self._on_action_trigger)
+
+ if current_option is not None:
+ self.setText(current_option)
+
+ def _on_action_trigger(self):
+ action = self.sender()
+ self.setText(action.text())
+ self.selection_changed.emit(action.text())
diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py
new file mode 100644
index 0000000000..688b8b6ae3
--- /dev/null
+++ b/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py
@@ -0,0 +1,38 @@
+from __future__ import print_function
+
+import os
+import sys
+
+SCRIPT_DIR = os.path.dirname(__file__)
+PACKAGE_DIR = os.path.join(SCRIPT_DIR, "modules")
+sys.path.append(PACKAGE_DIR)
+
+
+def flame_panel_executor(selection):
+ if "panel_app" in sys.modules.keys():
+ print("panel_app module is already loaded")
+ del sys.modules["panel_app"]
+ print("panel_app module removed from sys.modules")
+
+ import panel_app
+ panel_app.FlameToFtrackPanel(selection)
+
+
+def scope_sequence(selection):
+ import flame
+ return any(isinstance(item, flame.PySequence) for item in selection)
+
+
+def get_media_panel_custom_ui_actions():
+ return [
+ {
+ "name": "OpenPype: Ftrack",
+ "actions": [
+ {
+ "name": "Create Shots",
+ "isVisible": scope_sequence,
+ "execute": flame_panel_executor
+ }
+ ]
+ }
+ ]
diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json
index 782424f15b..d536652581 100644
--- a/openpype/settings/defaults/system_settings/applications.json
+++ b/openpype/settings/defaults/system_settings/applications.json
@@ -107,7 +107,10 @@
"windows": "",
"darwin": "",
"linux": ""
- }
+ },
+ "FLAME_WIRETAP_HOSTNAME": "",
+ "FLAME_WIRETAP_VOLUME": "stonefs",
+ "FLAME_WIRETAP_GROUP": "staff"
},
"variants": {
"2021": {