diff --git a/changelog.md b/changelog.md
new file mode 100644
index 0000000000..159ff0baeb
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,54 @@
+# Pype changelog #
+Welcome to pype changelog
+
+## 2.1 ##
+
+A large cleanup release. Most of the change are under the hood.
+
+**new**:
+- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts
+- _(pype)_ Added configurable option to add burnins to any generated quicktimes
+- _(ftrack)_ Action that identifies what machines pype is running on.
+- _(system)_ unify subprocess calls
+- _(maya)_ add audio to review quicktimes
+- _(nuke)_ add crop before write node to prevent overscan problems in ffmpeg
+- **Nuke Studio** publishing and workfiles support
+- **Muster** render manager support
+- _(nuke)_ Framerange, FPS and Resolution are set automatically at startup
+- _(maya)_ Ability to load published sequences as image planes
+- _(system)_ Ftrack event that sets asset folder permissions based on task assignees in ftrack.
+- _(maya)_ Pyblish plugin that allow validation of maya attributes
+- _(system)_ added better startup logging to tray debug, including basic connection information
+- _(avalon)_ option to group published subsets to groups in the loader
+- _(avalon)_ loader family filters are working now
+
+**changed**:
+- change multiple key attributes to unify their behaviour across the pipeline
+ - `frameRate` to `fps`
+ - `startFrame` to `frameStart`
+ - `endFrame` to `frameEnd`
+ - `fstart` to `frameStart`
+ - `fend` to `frameEnd`
+ - `handle_start` to `handleStart`
+ - `handle_end` to `handleEnd`
+ - `resolution_width` to `resolutionWidth`
+ - `resolution_height` to `resolutionHeight`
+ - `pixel_aspect` to `pixelAspect`
+
+- _(nuke)_ write nodes are now created inside group with only some attributes editable by the artist
+- rendered frames are now deleted from temporary location after their publishing is finished.
+- _(ftrack)_ RV action can now be launched from any entity
+- after publishing only refresh button is now available in pyblish UI
+- added context instance pyblish-lite so that artist knows if context plugin fails
+- _(avalon)_ allow opening selected files using enter key
+- _(avalon)_ core updated to v5.2.9 with our forked changes on top
+
+**fix**:
+- faster hierarchy retrieval from db
+- _(nuke)_ A lot of stability enhancements
+- _(nuke studio)_ A lot of stability enhancements
+- _(nuke)_ now only renders a single write node on farm
+- _(ftrack)_ pype would crash when launcher project level task
+- work directory was sometimes not being created correctly
+- major pype.lib cleanup. Removing of unused functions, merging those that were doing the same and general house cleaning.
+- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner
diff --git a/pype/__init__.py b/pype/__init__.py
index 35511eb6c1..a5858f49e7 100644
--- a/pype/__init__.py
+++ b/pype/__init__.py
@@ -7,6 +7,8 @@ from .lib import filter_pyblish_plugins
import logging
log = logging.getLogger(__name__)
+__version__ = "2.1.0"
+
PACKAGE_DIR = os.path.dirname(__file__)
PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins")
diff --git a/pype/clockify/__init__.py b/pype/clockify/__init__.py
index 5f61acd751..063f88db73 100644
--- a/pype/clockify/__init__.py
+++ b/pype/clockify/__init__.py
@@ -7,3 +7,6 @@ __all__ = [
'ClockifySettings',
'ClockifyModule'
]
+
+def tray_init(tray_widget, main_widget):
+ return ClockifyModule(main_widget, tray_widget)
diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py
index 0b84bf3953..1476c3d488 100644
--- a/pype/clockify/clockify.py
+++ b/pype/clockify/clockify.py
@@ -1,3 +1,4 @@
+import os
import threading
from pypeapp import style
from Qt import QtWidgets
@@ -20,9 +21,10 @@ class ClockifyModule:
self.bool_workspace_set = False
self.bool_timer_run = False
- def start_up(self):
self.clockapi.set_master(self)
self.bool_api_key_set = self.clockapi.set_api()
+
+ def tray_start(self):
if self.bool_api_key_set is False:
self.show_settings()
return
@@ -41,7 +43,7 @@ class ClockifyModule:
os.path.dirname(__file__),
'ftrack_actions'
])
- current = os.environ('FTRACK_ACTIONS_PATH', '')
+ current = os.environ.get('FTRACK_ACTIONS_PATH', '')
if current:
current += os.pathsep
os.environ['FTRACK_ACTIONS_PATH'] = current + actions_path
@@ -57,6 +59,24 @@ class ClockifyModule:
current += os.pathsep
os.environ['AVALON_ACTIONS'] = current + actions_path
+ if 'TimersManager' in modules:
+ self.timer_manager = modules['TimersManager']
+ self.timer_manager.add_module(self)
+
+ def start_timer_manager(self, data):
+ self.start_timer(data)
+
+ def stop_timer_manager(self):
+ self.stop_timer()
+
+ def timer_started(self, data):
+ if hasattr(self, 'timer_manager'):
+ self.timer_manager.start_timers(data)
+
+ def timer_stopped(self):
+ if hasattr(self, 'timer_manager'):
+ self.timer_manager.stop_timers()
+
def start_timer_check(self):
self.bool_thread_check_running = True
if self.thread_timer_check is None:
@@ -75,21 +95,81 @@ class ClockifyModule:
def check_running(self):
import time
while self.bool_thread_check_running is True:
+ bool_timer_run = False
if self.clockapi.get_in_progress() is not None:
- self.bool_timer_run = True
- else:
- self.bool_timer_run = False
- self.set_menu_visibility()
+ bool_timer_run = True
+
+ if self.bool_timer_run != bool_timer_run:
+ if self.bool_timer_run is True:
+ self.timer_stopped()
+ else:
+ actual_timer = self.clockapi.get_in_progress()
+ if not actual_timer:
+ continue
+
+ actual_project_id = actual_timer["projectId"]
+ project = self.clockapi.get_project_by_id(
+ actual_project_id
+ )
+ project_name = project["name"]
+
+ actual_timer_hierarchy = actual_timer["description"]
+ hierarchy_items = actual_timer_hierarchy.split("/")
+ task_name = hierarchy_items[-1]
+ hierarchy = hierarchy_items[:-1]
+
+ data = {
+ "task_name": task_name,
+ "hierarchy": hierarchy,
+ "project_name": project_name
+ }
+
+ self.timer_started(data)
+
+ self.bool_timer_run = bool_timer_run
+ self.set_menu_visibility()
time.sleep(5)
def stop_timer(self):
self.clockapi.finish_time_entry()
+ if self.bool_timer_run:
+ self.timer_stopped()
self.bool_timer_run = False
+ def start_timer(self, input_data):
+ actual_timer = self.clockapi.get_in_progress()
+ actual_timer_hierarchy = None
+ actual_project_id = None
+ if actual_timer is not None:
+ actual_timer_hierarchy = actual_timer.get("description")
+ actual_project_id = actual_timer.get("projectId")
+
+ desc_items = [val for val in input_data.get("hierarchy", [])]
+ desc_items.append(input_data["task_name"])
+ description = "/".join(desc_items)
+
+ project_id = self.clockapi.get_project_id(input_data["project_name"])
+
+ if (
+ actual_timer is not None and
+ description == actual_timer_hierarchy and
+ project_id == actual_project_id
+ ):
+ return
+
+ tag_ids = []
+ task_tag_id = self.clockapi.get_tag_id(input_data["task_name"])
+ if task_tag_id is not None:
+ tag_ids.append(task_tag_id)
+
+ self.clockapi.start_time_entry(
+ description, project_id, tag_ids=tag_ids
+ )
+
# Definition of Tray menu
- def tray_menu(self, parent):
+ def tray_menu(self, parent_menu):
# Menu for Tray App
- self.menu = QtWidgets.QMenu('Clockify', parent)
+ self.menu = QtWidgets.QMenu('Clockify', parent_menu)
self.menu.setProperty('submenu', 'on')
self.menu.setStyleSheet(style.load_stylesheet())
@@ -109,7 +189,7 @@ class ClockifyModule:
self.set_menu_visibility()
- return self.menu
+ parent_menu.addMenu(self.menu)
def show_settings(self):
self.widget_settings.input_api_key.setText(self.clockapi.get_api_key())
diff --git a/pype/clockify/clockify_api.py b/pype/clockify/clockify_api.py
index f5ebac0cef..ed932eedce 100644
--- a/pype/clockify/clockify_api.py
+++ b/pype/clockify/clockify_api.py
@@ -1,4 +1,5 @@
import os
+import re
import requests
import json
import datetime
@@ -22,6 +23,7 @@ class ClockifyAPI(metaclass=Singleton):
app_dir = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype'))
file_name = 'clockify.json'
fpath = os.path.join(app_dir, file_name)
+ admin_permission_names = ['WORKSPACE_OWN', 'WORKSPACE_ADMIN']
master_parent = None
workspace_id = None
@@ -55,31 +57,41 @@ class ClockifyAPI(metaclass=Singleton):
return False
return True
- def validate_workspace_perm(self):
- test_project = '__test__'
- action_url = 'workspaces/{}/projects/'.format(self.workspace_id)
- body = {
- "name": test_project, "clientId": "", "isPublic": "false",
- "estimate": {"type": "AUTO"},
- "color": "#f44336", "billable": "true"
- }
- response = requests.post(
- self.endpoint + action_url,
- headers=self.headers, json=body
+ def validate_workspace_perm(self, workspace_id=None):
+ user_id = self.get_user_id()
+ if user_id is None:
+ return False
+ if workspace_id is None:
+ workspace_id = self.workspace_id
+ action_url = "/workspaces/{}/users/{}/permissions".format(
+ workspace_id, user_id
)
- if response.status_code == 201:
- self.delete_project(self.get_project_id(test_project))
- return True
- else:
- projects = self.get_projects()
- if test_project in projects:
- try:
- self.delete_project(self.get_project_id(test_project))
- return True
- except json.decoder.JSONDecodeError:
- return False
+ response = requests.get(
+ self.endpoint + action_url,
+ headers=self.headers
+ )
+ user_permissions = response.json()
+ for perm in user_permissions:
+ if perm['name'] in self.admin_permission_names:
+ return True
return False
+ def get_user_id(self):
+ action_url = 'v1/user/'
+ response = requests.get(
+ self.endpoint + action_url,
+ headers=self.headers
+ )
+ # this regex is neccessary: UNICODE strings are crashing
+ # during json serialization
+ id_regex ='\"{1}id\"{1}\:{1}\"{1}\w+\"{1}'
+ result = re.findall(id_regex, str(response.content))
+ if len(result) != 1:
+ # replace with log and better message?
+ print('User ID was not found (this is a BUG!!!)')
+ return None
+ return json.loads('{'+result[0]+'}')['id']
+
def set_workspace(self, name=None):
if name is None:
name = os.environ.get('CLOCKIFY_WORKSPACE', None)
@@ -147,6 +159,19 @@ class ClockifyAPI(metaclass=Singleton):
project["name"]: project["id"] for project in response.json()
}
+ def get_project_by_id(self, project_id, workspace_id=None):
+ if workspace_id is None:
+ workspace_id = self.workspace_id
+ action_url = 'workspaces/{}/projects/{}/'.format(
+ workspace_id, project_id
+ )
+ response = requests.get(
+ self.endpoint + action_url,
+ headers=self.headers
+ )
+
+ return response.json()
+
def get_tags(self, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
@@ -279,6 +304,9 @@ class ClockifyAPI(metaclass=Singleton):
if workspace_id is None:
workspace_id = self.workspace_id
current = self.get_in_progress(workspace_id)
+ if current is None:
+ return
+
current_id = current["id"]
action_url = 'workspaces/{}/timeEntries/{}'.format(
workspace_id, current_id
diff --git a/pype/clockify/ftrack_actions/action_clockify_start.py b/pype/clockify/ftrack_actions/action_clockify_start.py
index e09d0b76e6..5b54476297 100644
--- a/pype/clockify/ftrack_actions/action_clockify_start.py
+++ b/pype/clockify/ftrack_actions/action_clockify_start.py
@@ -14,7 +14,7 @@ class StartClockify(BaseAction):
#: Action identifier.
identifier = 'clockify.start.timer'
#: Action label.
- label = 'Start timer'
+ label = 'Clockify - Start timer'
#: Action description.
description = 'Starts timer on clockify'
#: roles that are allowed to register this action
@@ -67,42 +67,3 @@ def register(session, **kw):
return
StartClockify(session).register()
-
-
-def main(arguments=None):
- '''Set up logging and register action.'''
- if arguments is None:
- arguments = []
-
- parser = argparse.ArgumentParser()
- # Allow setting of logging level from arguments.
- loggingLevels = {}
- for level in (
- logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
- logging.ERROR, logging.CRITICAL
- ):
- loggingLevels[logging.getLevelName(level).lower()] = level
-
- parser.add_argument(
- '-v', '--verbosity',
- help='Set the logging output verbosity.',
- choices=loggingLevels.keys(),
- default='info'
- )
- namespace = parser.parse_args(arguments)
-
- # Set up basic logging
- logging.basicConfig(level=loggingLevels[namespace.verbosity])
-
- session = ftrack_api.Session()
- register(session)
-
- # Wait for events
- logging.info(
- 'Registered actions and listening for events. Use Ctrl-C to abort.'
- )
- session.event_hub.wait()
-
-
-if __name__ == '__main__':
- raise SystemExit(main(sys.argv[1:]))
diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py
index 41bcc0c0b0..813eee358a 100644
--- a/pype/ftrack/actions/action_application_loader.py
+++ b/pype/ftrack/actions/action_application_loader.py
@@ -4,11 +4,12 @@ import time
from pype.ftrack import AppAction
from avalon import lib
from pypeapp import Logger
+from pype.lib import get_all_avalon_projects
log = Logger().get_logger(__name__)
-def registerApp(app, session):
+def registerApp(app, session, plugins_presets):
name = app['name']
variant = ""
try:
@@ -40,14 +41,18 @@ def registerApp(app, session):
# register action
AppAction(
session, label, name, executable, variant,
- icon, description, preactions
+ icon, description, preactions, plugins_presets
).register()
if not variant:
log.info('- Variant is not set')
-def register(session):
+def register(session, plugins_presets={}):
+ # WARNING getting projects only helps to check connection to mongo
+ # - without will `discover` of ftrack apps actions take ages
+ result = get_all_avalon_projects()
+
apps = []
launchers_path = os.path.join(os.environ["PYPE_CONFIG"], "launchers")
@@ -66,7 +71,7 @@ def register(session):
app_counter = 0
for app in apps:
try:
- registerApp(app, session)
+ registerApp(app, session, plugins_presets)
if app_counter%5 == 0:
time.sleep(0.1)
app_counter += 1
diff --git a/pype/ftrack/actions/action_asset_delete.py b/pype/ftrack/actions/action_asset_delete.py
index 684b3862a8..654c78049b 100644
--- a/pype/ftrack/actions/action_asset_delete.py
+++ b/pype/ftrack/actions/action_asset_delete.py
@@ -78,7 +78,7 @@ class AssetDelete(BaseAction):
}
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register action. Called when used as an event plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
@@ -87,7 +87,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
- AssetDelete(session).register()
+ AssetDelete(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_attributes_remapper.py b/pype/ftrack/actions/action_attributes_remapper.py
index e1c32bca35..db33fd1365 100644
--- a/pype/ftrack/actions/action_attributes_remapper.py
+++ b/pype/ftrack/actions/action_attributes_remapper.py
@@ -11,13 +11,14 @@ class AttributesRemapper(BaseAction):
#: Action identifier.
identifier = 'attributes.remapper'
#: Action label.
- label = 'Attributes Remapper'
+ label = "Pype Doctor"
+ variant = '- Attributes Remapper'
#: Action description.
description = 'Remaps attributes in avalon DB'
#: roles that are allowed to register this action
role_list = ["Pypeclub", "Administrator"]
- icon = '{}/ftrack/action_icons/AttributesRemapper.svg'.format(
+ icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
@@ -249,7 +250,9 @@ class AttributesRemapper(BaseAction):
if interface_messages:
self.show_interface_from_dict(
- event, interface_messages, "Errors during remapping attributes"
+ messages=interface_messages,
+ title="Errors during remapping attributes",
+ event=event
)
return True
@@ -274,10 +277,10 @@ class AttributesRemapper(BaseAction):
self.show_interface(event, items, title)
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- AttributesRemapper(session).register()
+ AttributesRemapper(session, plugins_presets).register()
diff --git a/pype/ftrack/actions/action_client_review_sort.py b/pype/ftrack/actions/action_client_review_sort.py
index b06a928007..6a659ce5e3 100644
--- a/pype/ftrack/actions/action_client_review_sort.py
+++ b/pype/ftrack/actions/action_client_review_sort.py
@@ -53,12 +53,12 @@ class ClientReviewSort(BaseAction):
}
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register action. Called when used as an event plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- action_handler = ClientReviewSort(session)
+ action_handler = ClientReviewSort(session, plugins_presets)
action_handler.register()
diff --git a/pype/ftrack/actions/action_component_open.py b/pype/ftrack/actions/action_component_open.py
index d3213c555a..33f4d38890 100644
--- a/pype/ftrack/actions/action_component_open.py
+++ b/pype/ftrack/actions/action_component_open.py
@@ -65,7 +65,7 @@ class ComponentOpen(BaseAction):
}
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register action. Called when used as an event plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
@@ -74,7 +74,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
- ComponentOpen(session).register()
+ ComponentOpen(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_create_cust_attrs.py b/pype/ftrack/actions/action_create_cust_attrs.py
index 2cd06cc479..47a6bb5d5f 100644
--- a/pype/ftrack/actions/action_create_cust_attrs.py
+++ b/pype/ftrack/actions/action_create_cust_attrs.py
@@ -110,12 +110,13 @@ class CustomAttributes(BaseAction):
#: Action identifier.
identifier = 'create.update.attributes'
#: Action label.
- label = 'Create/Update Avalon Attributes'
+ label = "Pype Admin"
+ variant = '- Create/Update Avalon Attributes'
#: Action description.
description = 'Creates Avalon/Mongo ID for double check'
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
- icon = '{}/ftrack/action_icons/CustomAttributes.svg'.format(
+ icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
@@ -568,7 +569,7 @@ class CustomAttributes(BaseAction):
}
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
@@ -577,7 +578,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
- CustomAttributes(session).register()
+ CustomAttributes(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py
index 2a777911b4..b9e10f7c30 100644
--- a/pype/ftrack/actions/action_create_folders.py
+++ b/pype/ftrack/actions/action_create_folders.py
@@ -30,11 +30,13 @@ class CreateFolders(BaseAction):
def discover(self, session, entities, event):
''' Validation '''
- not_allowed = ['assetversion']
if len(entities) != 1:
return False
+
+ not_allowed = ['assetversion', 'project']
if entities[0].entity_type.lower() in not_allowed:
return False
+
return True
def interface(self, session, entities, event):
@@ -322,13 +324,13 @@ class PartialDict(dict):
return '{'+key+'}'
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- CreateFolders(session).register()
+ CreateFolders(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_create_project_folders.py b/pype/ftrack/actions/action_create_project_structure.py
similarity index 95%
rename from pype/ftrack/actions/action_create_project_folders.py
rename to pype/ftrack/actions/action_create_project_structure.py
index 3ccdb08714..74d458b5f8 100644
--- a/pype/ftrack/actions/action_create_project_folders.py
+++ b/pype/ftrack/actions/action_create_project_structure.py
@@ -13,9 +13,9 @@ class CreateProjectFolders(BaseAction):
'''Edit meta data action.'''
#: Action identifier.
- identifier = 'create.project.folders'
+ identifier = 'create.project.structure'
#: Action label.
- label = 'Create Project Folders'
+ label = 'Create Project Structure'
#: Action description.
description = 'Creates folder structure'
#: roles that are allowed to register this action
@@ -31,6 +31,11 @@ class CreateProjectFolders(BaseAction):
def discover(self, session, entities, event):
''' Validation '''
+ if len(entities) != 1:
+ return False
+
+ if entities[0].entity_type.lower() != "project":
+ return False
return True
@@ -190,13 +195,13 @@ class CreateProjectFolders(BaseAction):
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- CreateProjectFolders(session).register()
+ CreateProjectFolders(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_cust_attr_doctor.py b/pype/ftrack/actions/action_cust_attr_doctor.py
index 9d0ce2071f..1b8f250e5b 100644
--- a/pype/ftrack/actions/action_cust_attr_doctor.py
+++ b/pype/ftrack/actions/action_cust_attr_doctor.py
@@ -12,14 +12,15 @@ class CustomAttributeDoctor(BaseAction):
#: Action identifier.
identifier = 'custom.attributes.doctor'
#: Action label.
- label = 'Custom Attributes Doctor'
+ label = "Pype Doctor"
+ variant = '- Custom Attributes Doctor'
#: Action description.
description = (
'Fix hierarchical custom attributes mainly handles, fstart'
' and fend'
)
- icon = '{}/ftrack/action_icons/TestAction.svg'.format(
+ icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
hierarchical_ca = ['handle_start', 'handle_end', 'fstart', 'fend']
@@ -286,13 +287,13 @@ class CustomAttributeDoctor(BaseAction):
return all_roles
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- CustomAttributeDoctor(session).register()
+ CustomAttributeDoctor(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py
index 96087f4c8e..a408de45b2 100644
--- a/pype/ftrack/actions/action_delete_asset.py
+++ b/pype/ftrack/actions/action_delete_asset.py
@@ -311,7 +311,7 @@ class DeleteAsset(BaseAction):
return assets
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
@@ -320,7 +320,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
- DeleteAsset(session).register()
+ DeleteAsset(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_delete_asset_byname.py b/pype/ftrack/actions/action_delete_asset_byname.py
index fa966096a8..4f2a0e515c 100644
--- a/pype/ftrack/actions/action_delete_asset_byname.py
+++ b/pype/ftrack/actions/action_delete_asset_byname.py
@@ -13,12 +13,13 @@ class AssetsRemover(BaseAction):
#: Action identifier.
identifier = 'remove.assets'
#: Action label.
- label = 'Delete Assets by Name'
+ label = "Pype Admin"
+ variant = '- Delete Assets by Name'
#: Action description.
description = 'Removes assets from Ftrack and Avalon db with all childs'
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
- icon = '{}/ftrack/action_icons/AssetsRemover.svg'.format(
+ icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
#: Db
@@ -131,7 +132,7 @@ class AssetsRemover(BaseAction):
return assets
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
@@ -140,7 +141,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
- AssetsRemover(session).register()
+ AssetsRemover(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_delete_unpublished.py b/pype/ftrack/actions/action_delete_unpublished.py
index 377e118ffb..5e7f783ba7 100644
--- a/pype/ftrack/actions/action_delete_unpublished.py
+++ b/pype/ftrack/actions/action_delete_unpublished.py
@@ -42,7 +42,7 @@ class VersionsCleanup(BaseAction):
}
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register action. Called when used as an event plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
@@ -51,7 +51,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
- VersionsCleanup(session).register()
+ VersionsCleanup(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py
index e0c0334e5f..58914fbc1e 100644
--- a/pype/ftrack/actions/action_djvview.py
+++ b/pype/ftrack/actions/action_djvview.py
@@ -21,9 +21,9 @@ class DJVViewAction(BaseAction):
)
type = 'Application'
- def __init__(self, session):
+ def __init__(self, session, plugins_presets):
'''Expects a ftrack_api.Session instance'''
- super().__init__(session)
+ super().__init__(session, plugins_presets)
self.djv_path = None
self.config_data = config.get_presets()['djv_view']['config']
@@ -218,12 +218,12 @@ class DJVViewAction(BaseAction):
return True
-def register(session):
+def register(session, plugins_presets={}):
"""Register hooks."""
if not isinstance(session, ftrack_api.session.Session):
return
- DJVViewAction(session).register()
+ DJVViewAction(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py
index 44acb24d55..8584b26aa4 100644
--- a/pype/ftrack/actions/action_job_killer.py
+++ b/pype/ftrack/actions/action_job_killer.py
@@ -14,12 +14,13 @@ class JobKiller(BaseAction):
#: Action identifier.
identifier = 'job.killer'
#: Action label.
- label = 'Job Killer'
+ label = "Pype Admin"
+ variant = '- Job Killer'
#: Action description.
description = 'Killing selected running jobs'
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
- icon = '{}/ftrack/action_icons/JobKiller.svg'.format(
+ icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
@@ -117,7 +118,7 @@ class JobKiller(BaseAction):
}
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
@@ -126,7 +127,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
- JobKiller(session).register()
+ JobKiller(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_multiple_notes.py b/pype/ftrack/actions/action_multiple_notes.py
index 338083fe47..6e28b7bed6 100644
--- a/pype/ftrack/actions/action_multiple_notes.py
+++ b/pype/ftrack/actions/action_multiple_notes.py
@@ -112,13 +112,13 @@ class MultipleNotes(BaseAction):
return True
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- MultipleNotes(session).register()
+ MultipleNotes(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_prepare_project.py b/pype/ftrack/actions/action_prepare_project.py
new file mode 100644
index 0000000000..60a7435907
--- /dev/null
+++ b/pype/ftrack/actions/action_prepare_project.py
@@ -0,0 +1,240 @@
+import os
+import json
+
+from pype.vendor import ftrack_api
+from pype.ftrack import BaseAction
+from pypeapp import config
+from pype.ftrack.lib import get_avalon_attr
+
+
+class PrepareProject(BaseAction):
+ '''Edit meta data action.'''
+
+ #: Action identifier.
+ identifier = 'prepare.project'
+ #: Action label.
+ label = 'Prepare Project'
+ #: Action description.
+ description = 'Set basic attributes on the project'
+ #: roles that are allowed to register this action
+ role_list = ["Pypeclub", "Administrator", "Project manager"]
+ icon = '{}/ftrack/action_icons/PrepareProject.svg'.format(
+ os.environ.get('PYPE_STATICS_SERVER', '')
+ )
+
+ def discover(self, session, entities, event):
+ ''' Validation '''
+ if len(entities) != 1:
+ return False
+
+ if entities[0].entity_type.lower() != "project":
+ return False
+
+ return True
+
+ def interface(self, session, entities, event):
+ if event['data'].get('values', {}):
+ return
+
+ # Inform user that this may take a while
+ self.show_message(event, "Preparing data... Please wait", True)
+
+ self.log.debug("Loading custom attributes")
+ cust_attrs, hier_cust_attrs = get_avalon_attr(session, True)
+ project_defaults = config.get_presets().get("ftrack", {}).get(
+ "project_defaults", {}
+ )
+
+ self.log.debug("Preparing data which will be shown")
+ attributes_to_set = {}
+ for attr in hier_cust_attrs:
+ key = attr["key"]
+ attributes_to_set[key] = {
+ "label": attr["label"],
+ "object": attr,
+ "default": project_defaults.get(key)
+ }
+
+ for attr in cust_attrs:
+ if attr["entity_type"].lower() != "show":
+ continue
+ key = attr["key"]
+ attributes_to_set[key] = {
+ "label": attr["label"],
+ "object": attr,
+ "default": project_defaults.get(key)
+ }
+
+ # Sort by label
+ attributes_to_set = dict(sorted(
+ attributes_to_set.items(),
+ key=lambda x: x[1]["label"]
+ ))
+ self.log.debug("Preparing interface for keys: \"{}\"".format(
+ str([key for key in attributes_to_set])
+ ))
+
+ title = "Set Attribute values"
+ items = []
+ multiselect_enumerators = []
+
+ # This item will be last (before enumerators)
+ # - sets value of auto synchronization
+ auto_sync_name = "avalon_auto_sync"
+ auto_sync_item = {
+ "name": auto_sync_name,
+ "type": "boolean",
+ "value": project_defaults.get(auto_sync_name, False),
+ "label": "AutoSync to Avalon"
+ }
+
+ item_splitter = {'type': 'label', 'value': '---'}
+
+ for key, in_data in attributes_to_set.items():
+ attr = in_data["object"]
+
+ # initial item definition
+ item = {
+ "name": key,
+ "label": in_data["label"]
+ }
+
+ # cust attr type - may have different visualization
+ type_name = attr["type"]["name"].lower()
+ easy_types = ["text", "boolean", "date", "number"]
+
+ easy_type = False
+ if type_name in easy_types:
+ easy_type = True
+
+ elif type_name == "enumerator":
+
+ attr_config = json.loads(attr["config"])
+ attr_config_data = json.loads(attr_config["data"])
+
+ if attr_config["multiSelect"] is True:
+ multiselect_enumerators.append(item_splitter)
+
+ multiselect_enumerators.append({
+ "type": "label",
+ "value": in_data["label"]
+ })
+
+ default = in_data["default"]
+ names = []
+ for option in sorted(
+ attr_config_data, key=lambda x: x["menu"]
+ ):
+ name = option["value"]
+ new_name = "__{}__{}".format(key, name)
+ names.append(new_name)
+ item = {
+ "name": new_name,
+ "type": "boolean",
+ "label": "- {}".format(option["menu"])
+ }
+ if default:
+ if (
+ isinstance(default, list) or
+ isinstance(default, tuple)
+ ):
+ if name in default:
+ item["value"] = True
+ else:
+ if name == default:
+ item["value"] = True
+
+ multiselect_enumerators.append(item)
+
+ multiselect_enumerators.append({
+ "type": "hidden",
+ "name": "__hidden__{}".format(key),
+ "value": json.dumps(names)
+ })
+ else:
+ easy_type = True
+ item["data"] = attr_config_data
+
+ else:
+ self.log.warning((
+ "Custom attribute \"{}\" has type \"{}\"."
+ " I don't know how to handle"
+ ).format(key, type_name))
+ items.append({
+ "type": "label",
+ "value": (
+ "!!! Can't handle Custom attritubte type \"{}\""
+ " (key: \"{}\")"
+ ).format(type_name, key)
+ })
+
+ if easy_type:
+ item["type"] = type_name
+
+ # default value in interface
+ default = in_data["default"]
+ if default is not None:
+ item["value"] = default
+
+ items.append(item)
+
+ # Add autosync attribute
+ items.append(auto_sync_item)
+
+ # Add enumerator items at the end
+ for item in multiselect_enumerators:
+ items.append(item)
+
+ return {
+ 'items': items,
+ 'title': title
+ }
+
+ def launch(self, session, entities, event):
+ if not event['data'].get('values', {}):
+ return
+
+ in_data = event['data']['values']
+ # Find hidden items for multiselect enumerators
+ keys_to_process = []
+ for key in in_data:
+ if key.startswith("__hidden__"):
+ keys_to_process.append(key)
+
+ self.log.debug("Preparing data for Multiselect Enumerators")
+ enumerators = {}
+ for key in keys_to_process:
+ new_key = key.replace("__hidden__", "")
+ enumerator_items = in_data.pop(key)
+ enumerators[new_key] = json.loads(enumerator_items)
+
+ # find values set for multiselect enumerator
+ for key, enumerator_items in enumerators.items():
+ in_data[key] = []
+
+ name = "__{}__".format(key)
+
+ for item in enumerator_items:
+ value = in_data.pop(item)
+ if value is True:
+ new_key = item.replace(name, "")
+ in_data[key].append(new_key)
+
+ self.log.debug("Setting Custom Attribute values:")
+ entity = entities[0]
+ for key, value in in_data.items():
+ entity["custom_attributes"][key] = value
+ self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value))
+
+ session.commit()
+
+ return True
+
+
+def register(session, plugins_presets={}):
+ '''Register plugin. Called when used as an plugin.'''
+
+ if not isinstance(session, ftrack_api.session.Session):
+ return
+
+ PrepareProject(session, plugins_presets).register()
diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py
index 223fd0a94b..6b6591355f 100644
--- a/pype/ftrack/actions/action_rv.py
+++ b/pype/ftrack/actions/action_rv.py
@@ -23,13 +23,13 @@ class RVAction(BaseAction):
)
type = 'Application'
- def __init__(self, session):
+ def __init__(self, session, plugins_presets):
""" Constructor
:param session: ftrack Session
:type session: :class:`ftrack_api.Session`
"""
- super().__init__(session)
+ super().__init__(session, plugins_presets)
self.rv_path = None
self.config_data = None
@@ -326,12 +326,12 @@ class RVAction(BaseAction):
return paths
-def register(session):
+def register(session, plugins_presets={}):
"""Register hooks."""
if not isinstance(session, ftrack_api.session.Session):
return
- RVAction(session).register()
+ RVAction(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_set_version.py b/pype/ftrack/actions/action_set_version.py
index f6e745b3ec..5bf965e3ef 100644
--- a/pype/ftrack/actions/action_set_version.py
+++ b/pype/ftrack/actions/action_set_version.py
@@ -71,7 +71,7 @@ class SetVersion(BaseAction):
}
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register action. Called when used as an event plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
@@ -80,7 +80,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
- SetVersion(session).register()
+ SetVersion(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_start_timer.py b/pype/ftrack/actions/action_start_timer.py
index d27908541e..ad83edfc9e 100644
--- a/pype/ftrack/actions/action_start_timer.py
+++ b/pype/ftrack/actions/action_start_timer.py
@@ -70,10 +70,10 @@ class StartTimer(BaseAction):
self.log.info('Starting Clockify timer for task: ' + task['name'])
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- StartTimer(session).register()
+ StartTimer(session, plugins_presets).register()
diff --git a/pype/ftrack/actions/action_sync_hier_attrs_local.py b/pype/ftrack/actions/action_sync_hier_attrs_local.py
index df95159a2c..01434470f3 100644
--- a/pype/ftrack/actions/action_sync_hier_attrs_local.py
+++ b/pype/ftrack/actions/action_sync_hier_attrs_local.py
@@ -19,11 +19,12 @@ class SyncHierarchicalAttrs(BaseAction):
#: Action identifier.
identifier = 'sync.hierarchical.attrs.local'
#: Action label.
- label = 'Sync HierAttrs - Local'
+ label = "Pype Admin"
+ variant = '- Sync Hier Attrs (Local)'
#: Action description.
description = 'Synchronize hierarchical attributes'
#: Icon
- icon = '{}/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg'.format(
+ icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
@@ -186,7 +187,10 @@ class SyncHierarchicalAttrs(BaseAction):
job['status'] = 'failed'
session.commit()
if self.interface_messages:
- self.show_interface_from_dict(self.interface_messages, event)
+ title = "Errors during SyncHierarchicalAttrs"
+ self.show_interface_from_dict(
+ messages=self.interface_messages, title=title, event=event
+ )
return True
@@ -302,13 +306,13 @@ class SyncHierarchicalAttrs(BaseAction):
self.update_hierarchical_attribute(child, key, value)
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- SyncHierarchicalAttrs(session).register()
+ SyncHierarchicalAttrs(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py
index 34070c7e1f..ad39b0ca12 100644
--- a/pype/ftrack/actions/action_sync_to_avalon_local.py
+++ b/pype/ftrack/actions/action_sync_to_avalon_local.py
@@ -47,11 +47,12 @@ class SyncToAvalon(BaseAction):
#: Action identifier.
identifier = 'sync.to.avalon.local'
#: Action label.
- label = 'SyncToAvalon - Local'
+ label = "Pype Admin"
+ variant = '- Sync To Avalon (Local)'
#: Action description.
description = 'Send data from Ftrack to Avalon'
#: Action icon.
- icon = '{}/ftrack/action_icons/SyncToAvalon-local.svg'.format(
+ icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
#: roles that are allowed to register this action
@@ -59,7 +60,7 @@ class SyncToAvalon(BaseAction):
#: Action priority
priority = 200
- def __init__(self, session):
+ def __init__(self, session, plugins_presets):
super(SyncToAvalon, self).__init__(session)
# reload utils on initialize (in case of server restart)
@@ -212,7 +213,7 @@ class SyncToAvalon(BaseAction):
self.add_childs_to_importable(child)
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
@@ -221,7 +222,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
- SyncToAvalon(session).register()
+ SyncToAvalon(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py
index b7cfd4934a..a2bc8bf892 100644
--- a/pype/ftrack/actions/action_test.py
+++ b/pype/ftrack/actions/action_test.py
@@ -40,13 +40,13 @@ class TestAction(BaseAction):
return True
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- TestAction(session).register()
+ TestAction(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbnail_to_childern.py
similarity index 92%
rename from pype/ftrack/actions/action_thumbToChildern.py
rename to pype/ftrack/actions/action_thumbnail_to_childern.py
index 4e7f1298f5..101b678512 100644
--- a/pype/ftrack/actions/action_thumbToChildern.py
+++ b/pype/ftrack/actions/action_thumbnail_to_childern.py
@@ -14,9 +14,11 @@ class ThumbToChildren(BaseAction):
# Action identifier
identifier = 'thumb.to.children'
# Action label
- label = 'Thumbnail to Children'
+ label = 'Thumbnail'
+ # Action variant
+ variant = " to Children"
# Action icon
- icon = '{}/ftrack/action_icons/thumbToChildren.svg'.format(
+ icon = '{}/ftrack/action_icons/Thumbnail.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
@@ -64,12 +66,12 @@ class ThumbToChildren(BaseAction):
}
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register action. Called when used as an event plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- ThumbToChildren(session).register()
+ ThumbToChildren(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbnail_to_parent.py
similarity index 94%
rename from pype/ftrack/actions/action_thumbToParent.py
rename to pype/ftrack/actions/action_thumbnail_to_parent.py
index 632d2a50b2..c382d9303c 100644
--- a/pype/ftrack/actions/action_thumbToParent.py
+++ b/pype/ftrack/actions/action_thumbnail_to_parent.py
@@ -13,9 +13,11 @@ class ThumbToParent(BaseAction):
# Action identifier
identifier = 'thumb.to.parent'
# Action label
- label = 'Thumbnail to Parent'
+ label = 'Thumbnail'
+ # Action variant
+ variant = " to Parent"
# Action icon
- icon = '{}/ftrack/action_icons/thumbToParent.svg'.format(
+ icon = '{}/ftrack/action_icons/Thumbnail.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
@@ -86,12 +88,12 @@ class ThumbToParent(BaseAction):
}
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register action. Called when used as an event plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- ThumbToParent(session).register()
+ ThumbToParent(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/actions/action_where_run_ask.py b/pype/ftrack/actions/action_where_run_ask.py
index 2bd1ed3d22..95bbf1fdd7 100644
--- a/pype/ftrack/actions/action_where_run_ask.py
+++ b/pype/ftrack/actions/action_where_run_ask.py
@@ -45,10 +45,10 @@ class ActionAskWhereIRun(BaseAction):
return True
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- ActionAskWhereIRun(session).register()
+ ActionAskWhereIRun(session, plugins_presets).register()
diff --git a/pype/ftrack/actions/action_where_run_show.py b/pype/ftrack/actions/action_where_run_show.py
index 7875d2e262..7fea23e3b7 100644
--- a/pype/ftrack/actions/action_where_run_show.py
+++ b/pype/ftrack/actions/action_where_run_show.py
@@ -77,10 +77,10 @@ class ActionShowWhereIRun(BaseAction):
return True
-def register(session, **kw):
+def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- ActionShowWhereIRun(session).register()
+ ActionShowWhereIRun(session, plugins_presets).register()
diff --git a/pype/ftrack/events/action_sync_hier_attrs.py b/pype/ftrack/events/action_sync_hier_attrs.py
index a94d43cad2..22ad7bf5aa 100644
--- a/pype/ftrack/events/action_sync_hier_attrs.py
+++ b/pype/ftrack/events/action_sync_hier_attrs.py
@@ -20,11 +20,12 @@ class SyncHierarchicalAttrs(BaseAction):
#: Action identifier.
identifier = 'sync.hierarchical.attrs'
#: Action label.
- label = 'Sync HierAttrs'
+ label = "Pype Admin"
+ variant = '- Sync Hier Attrs (server)'
#: Action description.
description = 'Synchronize hierarchical attributes'
#: Icon
- icon = '{}/ftrack/action_icons/SyncHierarchicalAttrs.svg'.format(
+ icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get(
'PYPE_STATICS_SERVER',
'http://localhost:{}'.format(
@@ -333,13 +334,13 @@ class SyncHierarchicalAttrs(BaseAction):
self.update_hierarchical_attribute(child, key, value)
-def register(session, **kw):
+def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- SyncHierarchicalAttrs(session).register()
+ SyncHierarchicalAttrs(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py
index e78b209fac..5628554c85 100644
--- a/pype/ftrack/events/action_sync_to_avalon.py
+++ b/pype/ftrack/events/action_sync_to_avalon.py
@@ -48,11 +48,12 @@ class Sync_To_Avalon(BaseAction):
#: Action identifier.
identifier = 'sync.to.avalon'
#: Action label.
- label = 'SyncToAvalon'
+ label = "Pype Admin"
+ variant = "- Sync To Avalon (Server)"
#: Action description.
description = 'Send data from Ftrack to Avalon'
#: Action icon.
- icon = '{}/ftrack/action_icons/SyncToAvalon.svg'.format(
+ icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get(
'PYPE_STATICS_SERVER',
'http://localhost:{}'.format(
@@ -242,7 +243,7 @@ class Sync_To_Avalon(BaseAction):
self.add_childs_to_importable(child)
-def register(session, **kw):
+def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
@@ -251,7 +252,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
- Sync_To_Avalon(session).register()
+ SyncToAvalon(session, plugins_presets).register()
def main(arguments=None):
diff --git a/pype/ftrack/events/event_del_avalon_id_from_new.py b/pype/ftrack/events/event_del_avalon_id_from_new.py
index f27a329429..6f6320f51b 100644
--- a/pype/ftrack/events/event_del_avalon_id_from_new.py
+++ b/pype/ftrack/events/event_del_avalon_id_from_new.py
@@ -51,9 +51,9 @@ class DelAvalonIdFromNew(BaseEvent):
continue
-def register(session, **kw):
+def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- DelAvalonIdFromNew(session).register()
+ DelAvalonIdFromNew(session, plugins_presets).register()
diff --git a/pype/ftrack/events/event_next_task_update.py b/pype/ftrack/events/event_next_task_update.py
index 1ae06050bc..e25514a2b4 100644
--- a/pype/ftrack/events/event_next_task_update.py
+++ b/pype/ftrack/events/event_next_task_update.py
@@ -86,9 +86,9 @@ class NextTaskUpdate(BaseEvent):
session.rollback()
-def register(session, **kw):
+def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- NextTaskUpdate(session).register()
+ NextTaskUpdate(session, plugins_presets).register()
diff --git a/pype/ftrack/events/event_radio_buttons.py b/pype/ftrack/events/event_radio_buttons.py
index a185280ecc..9c6f2d490a 100644
--- a/pype/ftrack/events/event_radio_buttons.py
+++ b/pype/ftrack/events/event_radio_buttons.py
@@ -34,9 +34,9 @@ class Radio_buttons(BaseEvent):
session.commit()
-def register(session):
+def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- Radio_buttons(session).register()
+ Radio_buttons(session, plugins_presets).register()
diff --git a/pype/ftrack/events/event_sync_hier_attr.py b/pype/ftrack/events/event_sync_hier_attr.py
index 867e2cde2b..7c5c4b820b 100644
--- a/pype/ftrack/events/event_sync_hier_attr.py
+++ b/pype/ftrack/events/event_sync_hier_attr.py
@@ -115,9 +115,9 @@ class SyncHierarchicalAttrs(BaseEvent):
self.update_hierarchical_attribute(child, key, value)
-def register(session, **kw):
+def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- SyncHierarchicalAttrs(session).register()
+ SyncHierarchicalAttrs(session, plugins_presets).register()
diff --git a/pype/ftrack/events/event_sync_to_avalon.py b/pype/ftrack/events/event_sync_to_avalon.py
index dbd58111b3..ae7ebbbf90 100644
--- a/pype/ftrack/events/event_sync_to_avalon.py
+++ b/pype/ftrack/events/event_sync_to_avalon.py
@@ -118,10 +118,10 @@ class Sync_to_Avalon(BaseEvent):
return
-def register(session, **kw):
+def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- Sync_to_Avalon(session).register()
+ Sync_to_Avalon(session, plugins_presets).register()
diff --git a/pype/ftrack/events/event_test.py b/pype/ftrack/events/event_test.py
index cc7851afb7..94d99dbf67 100644
--- a/pype/ftrack/events/event_test.py
+++ b/pype/ftrack/events/event_test.py
@@ -20,9 +20,9 @@ class Test_Event(BaseEvent):
return True
-def register(session, **kw):
+def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- Test_Event(session).register()
+ Test_Event(session, plugins_presets).register()
diff --git a/pype/ftrack/events/event_thumbnail_updates.py b/pype/ftrack/events/event_thumbnail_updates.py
index 042f6cc600..7f52177161 100644
--- a/pype/ftrack/events/event_thumbnail_updates.py
+++ b/pype/ftrack/events/event_thumbnail_updates.py
@@ -45,9 +45,9 @@ class ThumbnailEvents(BaseEvent):
pass
-def register(session, **kw):
+def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- ThumbnailEvents(session).register()
+ ThumbnailEvents(session, plugins_presets).register()
diff --git a/pype/ftrack/events/event_user_assigment.py b/pype/ftrack/events/event_user_assigment.py
index 0bb7f21590..3e250b988a 100644
--- a/pype/ftrack/events/event_user_assigment.py
+++ b/pype/ftrack/events/event_user_assigment.py
@@ -229,11 +229,11 @@ class UserAssigmentEvent(BaseEvent):
return True
-def register(session, **kw):
+def register(session, plugins_presets):
"""
Register plugin. Called when used as an plugin.
"""
if not isinstance(session, ftrack_api.session.Session):
return
- UserAssigmentEvent(session).register()
+ UserAssigmentEvent(session, plugins_presets).register()
diff --git a/pype/ftrack/events/event_version_to_task_statuses.py b/pype/ftrack/events/event_version_to_task_statuses.py
index 8b14e025d3..306d594647 100644
--- a/pype/ftrack/events/event_version_to_task_statuses.py
+++ b/pype/ftrack/events/event_version_to_task_statuses.py
@@ -69,9 +69,9 @@ class VersionToTaskStatus(BaseEvent):
path, task_status['name']))
-def register(session, **kw):
+def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
- VersionToTaskStatus(session).register()
+ VersionToTaskStatus(session, plugins_presets).register()
diff --git a/pype/ftrack/ftrack_server/ftrack_server.py b/pype/ftrack/ftrack_server/ftrack_server.py
index 81fd6eee63..2a58c12d09 100644
--- a/pype/ftrack/ftrack_server/ftrack_server.py
+++ b/pype/ftrack/ftrack_server/ftrack_server.py
@@ -5,7 +5,9 @@ import importlib
from pype.vendor import ftrack_api
import time
import logging
-from pypeapp import Logger
+import inspect
+from pypeapp import Logger, config
+
log = Logger().get_logger(__name__)
@@ -27,8 +29,8 @@ PYTHONPATH # Path to ftrack_api and paths to all modules used in actions
"""
-class FtrackServer():
- def __init__(self, type='action'):
+class FtrackServer:
+ def __init__(self, server_type='action'):
"""
- 'type' is by default set to 'action' - Runs Action server
- enter 'event' for Event server
@@ -43,21 +45,12 @@ class FtrackServer():
ftrack_log = logging.getLogger("ftrack_api")
ftrack_log.setLevel(logging.WARNING)
- self.type = type
- self.actionsAvailable = True
- self.eventsAvailable = True
- # Separate all paths
- if "FTRACK_ACTIONS_PATH" in os.environ:
- all_action_paths = os.environ["FTRACK_ACTIONS_PATH"]
- self.actionsPaths = all_action_paths.split(os.pathsep)
- else:
- self.actionsAvailable = False
+ env_key = "FTRACK_ACTIONS_PATH"
+ if server_type.lower() == 'event':
+ env_key = "FTRACK_EVENTS_PATH"
- if "FTRACK_EVENTS_PATH" in os.environ:
- all_event_paths = os.environ["FTRACK_EVENTS_PATH"]
- self.eventsPaths = all_event_paths.split(os.pathsep)
- else:
- self.eventsAvailable = False
+ self.server_type = server_type
+ self.env_key = env_key
def stop_session(self):
if self.session.event_hub.connected is True:
@@ -67,7 +60,7 @@ class FtrackServer():
def set_files(self, paths):
# Iterate all paths
- functions = []
+ register_functions_dict = []
for path in paths:
# add path to PYTHON PATH
if path not in sys.path:
@@ -92,13 +85,11 @@ class FtrackServer():
# separate files by register function
if 'register' not in mod_functions:
- msg = (
- '"{0}" - Missing register method'
- ).format(file, self.type)
+ msg = ('"{}" - Missing register method').format(file)
log.warning(msg)
continue
- functions.append({
+ register_functions_dict.append({
'name': file,
'register': mod_functions['register']
})
@@ -108,43 +99,47 @@ class FtrackServer():
)
log.warning(msg)
- if len(functions) < 1:
+ if len(register_functions_dict) < 1:
raise Exception
+ # Load presets for setting plugins
+ key = "user"
+ if self.server_type.lower() == "event":
+ key = "server"
+ plugins_presets = config.get_presets().get(
+ "ftrack", {}
+ ).get("plugins", {}).get(key, {})
+
function_counter = 0
- for function in functions:
+ for function_dict in register_functions_dict:
+ register = function_dict["register"]
try:
- function['register'](self.session)
+ if len(inspect.signature(register).parameters) == 1:
+ register(self.session)
+ else:
+ register(self.session, plugins_presets=plugins_presets)
+
if function_counter%7 == 0:
time.sleep(0.1)
function_counter += 1
- except Exception as e:
+ except Exception as exc:
msg = '"{}" - register was not successful ({})'.format(
- function['name'], str(e)
+ function_dict['name'], str(exc)
)
log.warning(msg)
def run_server(self):
self.session = ftrack_api.Session(auto_connect_event_hub=True,)
- if self.type.lower() == 'event':
- if self.eventsAvailable is False:
- msg = (
- 'FTRACK_EVENTS_PATH is not set'
- ', event server won\'t launch'
- )
- log.error(msg)
- return
- self.set_files(self.eventsPaths)
- else:
- if self.actionsAvailable is False:
- msg = (
- 'FTRACK_ACTIONS_PATH is not set'
- ', action server won\'t launch'
- )
- log.error(msg)
- return
- self.set_files(self.actionsPaths)
+ paths_str = os.environ.get(self.env_key)
+ if paths_str is None:
+ log.error((
+ "Env var \"{}\" is not set, \"{}\" server won\'t launch"
+ ).format(self.env_key, self.server_type))
+ return
+
+ paths = paths_str.split(os.pathsep)
+ self.set_files(paths)
log.info(60*"*")
log.info('Registration of actions/events has finished!')
diff --git a/pype/ftrack/lib/avalon_sync.py b/pype/ftrack/lib/avalon_sync.py
index b677529ec3..169bc4b051 100644
--- a/pype/ftrack/lib/avalon_sync.py
+++ b/pype/ftrack/lib/avalon_sync.py
@@ -116,13 +116,13 @@ def import_to_avalon(
# not override existing templates!
templates = av_project['config'].get('template', None)
if templates is not None:
- for key, value in config['template'].items():
+ for key, value in proj_config['template'].items():
if (
key in templates and
templates[key] is not None and
templates[key] != value
):
- config['template'][key] = templates[key]
+ proj_config['template'][key] = templates[key]
projectId = av_project['_id']
@@ -142,7 +142,7 @@ def import_to_avalon(
{'_id': ObjectId(projectId)},
{'$set': {
'name': project_name,
- 'config': config,
+ 'config': proj_config,
'data': data
}}
)
@@ -326,13 +326,26 @@ def import_to_avalon(
return output
-def get_avalon_attr(session):
+def get_avalon_attr(session, split_hierarchical=False):
custom_attributes = []
+ hier_custom_attributes = []
query = 'CustomAttributeGroup where name is "avalon"'
all_avalon_attr = session.query(query).one()
for cust_attr in all_avalon_attr['custom_attribute_configurations']:
- if 'avalon_' not in cust_attr['key']:
- custom_attributes.append(cust_attr)
+ if 'avalon_' in cust_attr['key']:
+ continue
+
+ if split_hierarchical:
+ if cust_attr["is_hierarchical"]:
+ hier_custom_attributes.append(cust_attr)
+ continue
+
+ custom_attributes.append(cust_attr)
+
+ if split_hierarchical:
+ # return tuple
+ return custom_attributes, hier_custom_attributes
+
return custom_attributes
diff --git a/pype/ftrack/lib/ftrack_action_handler.py b/pype/ftrack/lib/ftrack_action_handler.py
index 7a25155718..7fd7eccfb7 100644
--- a/pype/ftrack/lib/ftrack_action_handler.py
+++ b/pype/ftrack/lib/ftrack_action_handler.py
@@ -21,9 +21,9 @@ class BaseAction(BaseHandler):
icon = None
type = 'Action'
- def __init__(self, session):
+ def __init__(self, session, plugins_presets={}):
'''Expects a ftrack_api.Session instance'''
- super().__init__(session)
+ super().__init__(session, plugins_presets)
if self.label is None:
raise ValueError(
diff --git a/pype/ftrack/lib/ftrack_app_handler.py b/pype/ftrack/lib/ftrack_app_handler.py
index 7389d1c2c3..dbb38a3247 100644
--- a/pype/ftrack/lib/ftrack_app_handler.py
+++ b/pype/ftrack/lib/ftrack_app_handler.py
@@ -26,10 +26,10 @@ class AppAction(BaseHandler):
preactions = ['start.timer']
def __init__(
- self, session, label, name, executable,
- variant=None, icon=None, description=None, preactions=[]
+ self, session, label, name, executable, variant=None,
+ icon=None, description=None, preactions=[], plugins_presets={}
):
- super().__init__(session)
+ super().__init__(session, plugins_presets)
'''Expects a ftrack_api.Session instance'''
if label is None:
diff --git a/pype/ftrack/lib/ftrack_base_handler.py b/pype/ftrack/lib/ftrack_base_handler.py
index 1bb3eb7a8f..c6aa176363 100644
--- a/pype/ftrack/lib/ftrack_base_handler.py
+++ b/pype/ftrack/lib/ftrack_base_handler.py
@@ -29,7 +29,7 @@ class BaseHandler(object):
ignore_me = False
preactions = []
- def __init__(self, session):
+ def __init__(self, session, plugins_presets={}):
'''Expects a ftrack_api.Session instance'''
self._session = session
self.log = Logger().get_logger(self.__class__.__name__)
@@ -37,13 +37,23 @@ class BaseHandler(object):
# Using decorator
self.register = self.register_decorator(self.register)
self.launch = self.launch_log(self.launch)
+ self.plugins_presets = plugins_presets
# Decorator
def register_decorator(self, func):
@functools.wraps(func)
def wrapper_register(*args, **kwargs):
+
+ presets_data = self.plugins_presets.get(self.__class__.__name__)
+ if presets_data:
+ for key, value in presets_data.items():
+ if not hasattr(self, key):
+ continue
+ setattr(self, key, value)
+
if self.ignore_me:
return
+
label = self.__class__.__name__
if hasattr(self, 'label'):
if self.variant is None:
@@ -495,13 +505,12 @@ class BaseHandler(object):
)
def show_interface_from_dict(
- self, messages, event=None, user=None, username=None, user_id=None
+ self, messages, title="", event=None, user=None, username=None, user_id=None
):
if not messages:
self.log.debug("No messages to show! (messages dict is empty)")
return
items = []
- title = 'Errors during mirroring'
splitter = {'type': 'label', 'value': '---'}
first = True
for key, value in messages.items():
diff --git a/pype/ftrack/lib/ftrack_event_handler.py b/pype/ftrack/lib/ftrack_event_handler.py
index b2f47c3fbb..db55eef16e 100644
--- a/pype/ftrack/lib/ftrack_event_handler.py
+++ b/pype/ftrack/lib/ftrack_event_handler.py
@@ -15,9 +15,9 @@ class BaseEvent(BaseHandler):
type = 'Event'
- def __init__(self, session):
+ def __init__(self, session, plugins_presets={}):
'''Expects a ftrack_api.Session instance'''
- super().__init__(session)
+ super().__init__(session, plugins_presets)
# Decorator
def launch_log(self, func):
diff --git a/pype/ftrack/tray/ftrack_module.py b/pype/ftrack/tray/ftrack_module.py
index adcce9c2b1..ce2754c25d 100644
--- a/pype/ftrack/tray/ftrack_module.py
+++ b/pype/ftrack/tray/ftrack_module.py
@@ -88,9 +88,11 @@ class FtrackModule:
def set_action_server(self):
try:
self.action_server.run_server()
- except Exception:
- msg = 'Ftrack Action server crashed! Please try to start again.'
- log.error(msg)
+ except Exception as exc:
+ log.error(
+ "Ftrack Action server crashed! Please try to start again.",
+ exc_info=True
+ )
# TODO show message to user
self.bool_action_server = False
self.set_menu_visibility()
diff --git a/pype/lib.py b/pype/lib.py
index 66cef40674..6eee38f6d8 100644
--- a/pype/lib.py
+++ b/pype/lib.py
@@ -51,7 +51,7 @@ def get_hierarchy(asset_name=None):
})
not_set = "PARENTS_NOT_SET"
- entity_parents = entity.get("data", {}).get("parents", not_set)
+ entity_parents = asset_entity.get("data", {}).get("parents", not_set)
# If entity already have parents then just return joined
if entity_parents != not_set:
@@ -467,10 +467,18 @@ def filter_pyblish_plugins(plugins):
host = api.current_host()
+ presets = config.get_presets().get('plugins', {}).get(host, {}).get(
+ "publish", {}
+ )
+
# iterate over plugins
for plugin in plugins[:]:
+ # skip if there are no presets to process
+ if not presets:
+ continue
+
try:
- config_data = config.get_presets()['plugins'][host]["publish"][plugin.__name__] # noqa: E501
+ config_data = presets[plugin.__name__] # noqa: E501
except KeyError:
continue
@@ -483,3 +491,7 @@ def filter_pyblish_plugins(plugins):
option, value, plugin.__name__))
setattr(plugin, option, value)
+
+ # Remove already processed plugins from dictionary
+ # WARNING Requires plugins with unique names
+ presets.pop(plugin.__name__)
diff --git a/pype/nuke/__init__.py b/pype/nuke/__init__.py
index 94f591b2e8..0c4cdc10ab 100644
--- a/pype/nuke/__init__.py
+++ b/pype/nuke/__init__.py
@@ -59,13 +59,14 @@ class NukeHandler(logging.Handler):
'''Adding Nuke Logging Handler'''
+log.info([handler.get_name() for handler in logging.root.handlers[:]])
nuke_handler = NukeHandler()
if nuke_handler.get_name() \
not in [handler.get_name()
for handler in logging.root.handlers[:]]:
logging.getLogger().addHandler(nuke_handler)
logging.getLogger().setLevel(logging.INFO)
-
+log.info([handler.get_name() for handler in logging.root.handlers[:]])
def reload_config():
"""Attempt to reload pipeline at run-time.
@@ -77,10 +78,7 @@ def reload_config():
import importlib
for module in (
- "app",
- "app.api",
"{}.api".format(AVALON_CONFIG),
- "{}.templates".format(AVALON_CONFIG),
"{}.nuke.actions".format(AVALON_CONFIG),
"{}.nuke.templates".format(AVALON_CONFIG),
"{}.nuke.menu".format(AVALON_CONFIG),
@@ -96,9 +94,8 @@ def reload_config():
def install():
-
- # api.set_avalon_workdir()
- # reload_config()
+ ''' Installing all requarements for Nuke host
+ '''
log.info("Registering Nuke plug-ins..")
pyblish.register_plugin_path(PUBLISH_PATH)
@@ -117,8 +114,6 @@ def install():
avalon.data["familiesStateDefault"] = False
avalon.data["familiesStateToggled"] = family_states
- menu.install()
-
# Workfiles.
launch_workfiles = os.environ.get("WORKFILES_STARTUP")
@@ -128,14 +123,21 @@ def install():
# Set context settings.
nuke.addOnCreate(lib.set_context_settings, nodeClass="Root")
+ menu.install()
+
+
def launch_workfiles_app():
+ '''Function letting start workfiles after start of host
+ '''
if not self.workfiles_launched:
self.workfiles_launched = True
workfiles.show(os.environ["AVALON_WORKDIR"])
def uninstall():
+ '''Uninstalling host's integration
+ '''
log.info("Deregistering Nuke plug-ins..")
pyblish.deregister_plugin_path(PUBLISH_PATH)
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
@@ -144,8 +146,13 @@ def uninstall():
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)
+ reload_config()
+ menu.uninstall()
+
+
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node passthrough states on instance toggles."""
+
log.info("instance toggle: {}, old_value: {}, new_value:{} ".format(
instance, old_value, new_value))
diff --git a/pype/nuke/actions.py b/pype/nuke/actions.py
index 640e41a7de..c0c95e9080 100644
--- a/pype/nuke/actions.py
+++ b/pype/nuke/actions.py
@@ -1,6 +1,3 @@
-# absolute_import is needed to counter the `module has no cmds error` in Maya
-from __future__ import absolute_import
-
import pyblish.api
from avalon.nuke.lib import (
@@ -12,7 +9,7 @@ from ..action import get_errored_instances_from_context
class SelectInvalidAction(pyblish.api.Action):
- """Select invalid nodes in Maya when plug-in failed.
+ """Select invalid nodes in Nuke when plug-in failed.
To retrieve the invalid nodes this assumes a static `get_invalid()`
method is available on the plugin.
diff --git a/pype/nuke/lib.py b/pype/nuke/lib.py
index ec6d9514f4..bd6a581e6f 100644
--- a/pype/nuke/lib.py
+++ b/pype/nuke/lib.py
@@ -20,6 +20,8 @@ self._project = None
def onScriptLoad():
+ ''' Callback for ffmpeg support
+ '''
if nuke.env['LINUX']:
nuke.tcl('load ffmpegReader')
nuke.tcl('load ffmpegWriter')
@@ -37,6 +39,7 @@ def checkInventoryVersions():
and check if the node is having actual version. If not then it will color
it to red.
"""
+ # TODO: make it for all nodes not just Read (Loader
# get all Loader nodes by avalon attribute metadata
for each in nuke.allNodes():
@@ -76,13 +79,16 @@ def checkInventoryVersions():
def writes_version_sync():
+ ''' Callback synchronizing version of publishable write nodes
+ '''
+ # TODO: make it work with new write node group
try:
rootVersion = pype.get_version_from_path(nuke.root().name())
padding = len(rootVersion)
new_version = "v" + str("{" + ":0>{}".format(padding) + "}").format(
int(rootVersion)
)
- log.info("new_version: {}".format(new_version))
+ log.debug("new_version: {}".format(new_version))
except Exception:
return
@@ -92,32 +98,34 @@ def writes_version_sync():
try:
if avalon_knob_data['families'] not in ["render"]:
- log.info(avalon_knob_data['families'])
+ log.debug(avalon_knob_data['families'])
continue
node_file = each['file'].value()
- log.info("node_file: {}".format(node_file))
node_version = "v" + pype.get_version_from_path(node_file)
- log.info("node_version: {}".format(node_version))
+ log.debug("node_version: {}".format(node_version))
node_new_file = node_file.replace(node_version, new_version)
each['file'].setValue(node_new_file)
if not os.path.isdir(os.path.dirname(node_new_file)):
- log.info("path does not exist")
+ log.warning("Path does not exist! I am creating it.")
os.makedirs(os.path.dirname(node_new_file), 0o766)
except Exception as e:
- log.debug(
+ log.warning(
"Write node: `{}` has no version in path: {}".format(each.name(), e))
def version_up_script():
+ ''' Raising working script's version
+ '''
import nukescripts
nukescripts.script_and_write_nodes_version_up()
def get_render_path(node):
-
+ ''' Generate Render path from presets regarding avalon knob data
+ '''
data = dict()
data['avalon'] = avalon.nuke.get_avalon_knob_data(node)
@@ -141,12 +149,24 @@ def get_render_path(node):
def format_anatomy(data):
+ ''' Helping function for formating of anatomy paths
+
+ Arguments:
+ data (dict): dictionary with attributes used for formating
+
+ Return:
+ path (str)
+ '''
+ # TODO: perhaps should be nonPublic
+
from .templates import (
get_anatomy
)
+ # TODO: remove get_anatomy and import directly Anatomy() here
anatomy = get_anatomy()
- log.info("__ anatomy.templates: {}".format(anatomy.templates))
+ log.debug("__ anatomy.templates: {}".format(anatomy.templates))
+
# TODO: perhaps should be in try!
padding = int(anatomy.templates['render']['padding'])
version = data.get("version", None)
@@ -167,17 +187,24 @@ def format_anatomy(data):
"hierarchy": pype.get_hierarchy(),
"frame": "#" * padding,
})
- log.info("__ data: {}".format(data))
- log.info("__ format_anatomy: {}".format(anatomy.format(data)))
return anatomy.format(data)
def script_name():
+ ''' Returns nuke script path
+ '''
return nuke.root().knob('name').value()
+def add_button_write_to_read(node):
+ name = "createReadNode"
+ label = "Create Read"
+ value = "import write_to_read;write_to_read.write_to_read(nuke.thisNode())"
+ k = nuke.PyScript_Knob(name, label, value)
+ k.setFlag(0x1000)
+ node.addKnob(k)
def create_write_node(name, data, prenodes=None):
- '''Creating write node which is group node
+ ''' Creating write node which is group node
Arguments:
name (str): name of node
@@ -200,6 +227,8 @@ def create_write_node(name, data, prenodes=None):
)
]
+ Return:
+ node (obj): group node with avalon data as Knobs
'''
nuke_dataflow_writes = get_node_dataflow_preset(**data)
@@ -212,7 +241,6 @@ def create_write_node(name, data, prenodes=None):
"nuke_dataflow_writes": nuke_dataflow_writes,
"nuke_colorspace_writes": nuke_colorspace_writes
})
-
anatomy_filled = format_anatomy(data)
except Exception as e:
@@ -228,7 +256,7 @@ def create_write_node(name, data, prenodes=None):
# create directory
if not os.path.isdir(os.path.dirname(fpath)):
- log.info("path does not exist")
+ log.warning("Path does not exist! I am creating it.")
os.makedirs(os.path.dirname(fpath), 0o766)
_data = OrderedDict({
@@ -303,11 +331,15 @@ def create_write_node(name, data, prenodes=None):
# imprinting group node
GN = avalon.nuke.imprint(GN, data["avalon"])
+
divider = nuke.Text_Knob('')
GN.addKnob(divider)
add_rendering_knobs(GN)
+ # adding write to read button
+ add_button_write_to_read(GN)
+
divider = nuke.Text_Knob('')
GN.addKnob(divider)
@@ -325,6 +357,14 @@ def create_write_node(name, data, prenodes=None):
def add_rendering_knobs(node):
+ ''' Adds additional rendering knobs to given node
+
+ Arguments:
+ node (obj): nuke node object to be fixed
+
+ Return:
+ node (obj): with added knobs
+ '''
if "render" not in node.knobs():
knob = nuke.Boolean_Knob("render", "Render")
knob.setFlag(0x1000)
@@ -338,6 +378,12 @@ def add_rendering_knobs(node):
def set_viewers_colorspace(viewer):
+ ''' Adds correct colorspace to viewer
+
+ Arguments:
+ viewer (obj): nuke viewer node object to be fixed
+
+ '''
assert isinstance(viewer, dict), log.error(
"set_viewers_colorspace(): argument should be dictionary")
@@ -381,6 +427,12 @@ def set_viewers_colorspace(viewer):
def set_root_colorspace(root_dict):
+ ''' Adds correct colorspace to root
+
+ Arguments:
+ root_dict (dict): nuke root node as dictionary
+
+ '''
assert isinstance(root_dict, dict), log.error(
"set_root_colorspace(): argument should be dictionary")
@@ -397,17 +449,26 @@ def set_root_colorspace(root_dict):
for knob, value in root_dict.items():
if nuke.root()[knob].value() not in value:
nuke.root()[knob].setValue(str(value))
- log.info("nuke.root()['{}'] changed to: {}".format(knob, value))
+ log.debug("nuke.root()['{}'] changed to: {}".format(knob, value))
def set_writes_colorspace(write_dict):
+ ''' Adds correct colorspace to write node dict
+
+ Arguments:
+ write_dict (dict): nuke write node as dictionary
+
+ '''
+ # TODO: complete this function so any write node in scene will have fixed colorspace following presets for the project
assert isinstance(write_dict, dict), log.error(
"set_root_colorspace(): argument should be dictionary")
- log.info("set_writes_colorspace(): {}".format(write_dict))
+
+ log.debug("__ set_writes_colorspace(): {}".format(write_dict))
def set_colorspace():
-
+ ''' Setting colorpace following presets
+ '''
nuke_colorspace = get_colorspace_preset().get("nuke", None)
try:
@@ -428,7 +489,7 @@ def set_colorspace():
try:
for key in nuke_colorspace:
- log.info("{}".format(key))
+ log.debug("Preset's colorspace key: {}".format(key))
except TypeError:
log.error("Nuke is not in templates! \n\n\n"
"contact your supervisor!")
@@ -474,10 +535,6 @@ def reset_frame_range_handles():
root["first_frame"].setValue(frame_start)
root["last_frame"].setValue(frame_end)
- log.info("__ handle_start: `{}`".format(handle_start))
- log.info("__ handle_end: `{}`".format(handle_end))
- log.info("__ fps: `{}`".format(fps))
-
# setting active viewers
nuke.frame(int(asset_entity["data"]["frameStart"]))
@@ -488,15 +545,11 @@ def reset_frame_range_handles():
for node in nuke.allNodes(filter="Viewer"):
node['frame_range'].setValue(range)
node['frame_range_lock'].setValue(True)
-
- log.info("_frameRange: {}".format(range))
- log.info("frameRange: {}".format(node['frame_range'].value()))
-
node['frame_range'].setValue(range)
node['frame_range_lock'].setValue(True)
# adding handle_start/end to root avalon knob
- if not avalon.nuke.set_avalon_knob_data(root, {
+ if not avalon.nuke.imprint(root, {
"handleStart": int(handle_start),
"handleEnd": int(handle_end)
}):
diff --git a/pype/nuke/menu.py b/pype/nuke/menu.py
index 169ac81096..4f5410f8fd 100644
--- a/pype/nuke/menu.py
+++ b/pype/nuke/menu.py
@@ -2,10 +2,11 @@ import nuke
from avalon.api import Session
from pype.nuke import lib
+from pypeapp import Logger
+log = Logger().get_logger(__name__, "nuke")
def install():
-
menubar = nuke.menu("Nuke")
menu = menubar.findItem(Session["AVALON_LABEL"])
@@ -15,8 +16,11 @@ def install():
rm_item = [
(i, item) for i, item in enumerate(menu.items()) if name in item.name()
][0]
+
+ log.debug("Changing Item: {}".format(rm_item))
+ # rm_item[1].setEnabled(False)
menu.removeItem(rm_item[1].name())
- menu.addCommand(new_name, lib.reset_resolution, index=rm_item[0])
+ menu.addCommand(new_name, lib.reset_resolution, index=(rm_item[0]))
# replace reset frame range from avalon core to pype's
name = "Reset Frame Range"
@@ -24,8 +28,10 @@ def install():
rm_item = [
(i, item) for i, item in enumerate(menu.items()) if name in item.name()
][0]
+ log.debug("Changing Item: {}".format(rm_item))
+ # rm_item[1].setEnabled(False)
menu.removeItem(rm_item[1].name())
- menu.addCommand(new_name, lib.reset_frame_range_handles, index=rm_item[0])
+ menu.addCommand(new_name, lib.reset_frame_range_handles, index=(rm_item[0]))
# add colorspace menu item
name = "Set colorspace"
@@ -33,9 +39,22 @@ def install():
name, lib.set_colorspace,
index=(rm_item[0]+2)
)
+ log.debug("Adding menu item: {}".format(name))
# add item that applies all setting above
name = "Apply all settings"
menu.addCommand(
name, lib.set_context_settings, index=(rm_item[0]+3)
)
+ log.debug("Adding menu item: {}".format(name))
+
+
+
+def uninstall():
+
+ menubar = nuke.menu("Nuke")
+ menu = menubar.findItem(Session["AVALON_LABEL"])
+
+ for item in menu.items():
+ log.info("Removing menu item: {}".format(item.name()))
+ menu.removeItem(item.name())
diff --git a/pype/nuke/templates.py b/pype/nuke/templates.py
index 797335d982..6434d73f1d 100644
--- a/pype/nuke/templates.py
+++ b/pype/nuke/templates.py
@@ -20,6 +20,8 @@ def get_colorspace_preset():
def get_node_dataflow_preset(**kwarg):
+ ''' Get preset data for dataflow (fileType, compression, bitDepth)
+ '''
log.info(kwarg)
host = kwarg.get("host", "nuke")
cls = kwarg.get("class", None)
@@ -39,6 +41,8 @@ def get_node_dataflow_preset(**kwarg):
def get_node_colorspace_preset(**kwarg):
+ ''' Get preset data for colorspace
+ '''
log.info(kwarg)
host = kwarg.get("host", "nuke")
cls = kwarg.get("class", None)
diff --git a/pype/nukestudio/__init__.py b/pype/nukestudio/__init__.py
index 2e7ccdd675..c900848a93 100644
--- a/pype/nukestudio/__init__.py
+++ b/pype/nukestudio/__init__.py
@@ -1,5 +1,6 @@
import os
-
+from pypeapp import Logger
+import hiero
from avalon.tools import workfiles
from avalon import api as avalon
from pyblish import api as pyblish
@@ -13,20 +14,12 @@ from .workio import (
work_root
)
-from .. import api
-
from .menu import (
install as menu_install,
_update_menu_task_label
)
from .tags import add_tags_from_presets
-from pypeapp import Logger
-
-import hiero
-
-log = Logger().get_logger(__name__, "nukestudio")
-
__all__ = [
# Workfiles API
"open",
@@ -35,11 +28,16 @@ __all__ = [
"has_unsaved_changes",
"file_extensions",
"work_root",
- ]
+]
+
+# get logger
+log = Logger().get_logger(__name__, "nukestudio")
+''' Creating all important host related variables '''
AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype")
+# plugin root path
PARENT_DIR = os.path.dirname(__file__)
PACKAGE_DIR = os.path.dirname(PARENT_DIR)
PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins")
@@ -49,13 +47,21 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "nukestudio", "inventory")
-
+# registering particular pyblish gui but `lite` is recomended!!
if os.getenv("PYBLISH_GUI", None):
pyblish.register_gui(os.getenv("PYBLISH_GUI", None))
def install(config):
+ """
+ Installing Nukestudio integration for avalon
+ Args:
+ config (obj): avalon config module `pype` in our case, it is not used but required by avalon.api.install()
+
+ """
+
+ # adding all events
_register_events()
log.info("Registering NukeStudio plug-ins..")
@@ -74,6 +80,7 @@ def install(config):
avalon.data["familiesStateDefault"] = False
avalon.data["familiesStateToggled"] = family_states
+ # install menu
menu_install()
# Workfiles.
@@ -91,11 +98,26 @@ def install(config):
def add_tags(event):
+ """
+ Event for automatic tag creation after nukestudio start
+
+ Args:
+ event (obj): required but unused
+ """
+
add_tags_from_presets()
def launch_workfiles_app(event):
- workfiles.show(os.environ["AVALON_WORKDIR"])
+ """
+ Event for launching workfiles after nukestudio start
+
+ Args:
+ event (obj): required but unused
+ """
+ from .lib import set_workfiles
+
+ set_workfiles()
# Closing the new project.
event.sender.close()
@@ -107,6 +129,10 @@ def launch_workfiles_app(event):
def uninstall():
+ """
+ Uninstalling Nukestudio integration for avalon
+
+ """
log.info("Deregistering NukeStudio plug-ins..")
pyblish.deregister_host("nukestudio")
pyblish.deregister_plugin_path(PUBLISH_PATH)
@@ -115,6 +141,11 @@ def uninstall():
def _register_events():
+ """
+ Adding all callbacks.
+ """
+
+ # if task changed then change notext of nukestudio
avalon.on("taskChanged", _update_menu_task_label)
log.info("Installed event callback for 'taskChanged'..")
@@ -129,4 +160,5 @@ def ls():
See the `container.json` schema for details on how it should look,
and the Maya equivalent, which is in `avalon.maya.pipeline`
"""
+ # TODO: listing all availabe containers form sequence
return
diff --git a/pype/nukestudio/lib.py b/pype/nukestudio/lib.py
index 9136fb359a..6674e8a3aa 100644
--- a/pype/nukestudio/lib.py
+++ b/pype/nukestudio/lib.py
@@ -1,19 +1,13 @@
-# Standard library
import os
import sys
-
-# Pyblish libraries
-import pyblish.api
-
-import avalon.api as avalon
-import pype.api as pype
-
-from avalon.vendor.Qt import (QtWidgets, QtGui)
-
-# Host libraries
import hiero
-
+import pyblish.api
+import avalon.api as avalon
+from avalon.vendor.Qt import (QtWidgets, QtGui)
+import pype.api as pype
from pypeapp import Logger
+
+
log = Logger().get_logger(__name__, "nukestudio")
cached_process = None
@@ -30,12 +24,18 @@ AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype")
def set_workfiles():
''' Wrapping function for workfiles launcher '''
from avalon.tools import workfiles
+
+ # import session to get project dir
S = avalon.Session
active_project_root = os.path.normpath(
os.path.join(S['AVALON_PROJECTS'], S['AVALON_PROJECT'])
)
workdir = os.environ["AVALON_WORKDIR"]
+
+ # show workfile gui
workfiles.show(workdir)
+
+ # getting project
project = hiero.core.projects()[-1]
# set project root with backward compatibility
@@ -64,11 +64,10 @@ def set_workfiles():
# set fps to hiero project
project.setFramerate(fps)
+ # TODO: add auto colorspace set from project drop
log.info("Project property has been synchronised with Avalon db")
-
-
def reload_config():
"""Attempt to reload pipeline at run-time.
@@ -189,6 +188,10 @@ def add_submission():
class PublishAction(QtWidgets.QAction):
+ """
+ Action with is showing as menu item
+ """
+
def __init__(self):
QtWidgets.QAction.__init__(self, "Publish", None)
self.triggered.connect(self.publish)
@@ -213,7 +216,8 @@ class PublishAction(QtWidgets.QAction):
def _show_no_gui():
- """Popup with information about how to register a new GUI
+ """
+ Popup with information about how to register a new GUI
In the event of no GUI being registered or available,
this information dialog will appear to guide the user
through how to get set up with one.
diff --git a/pype/nukestudio/menu.py b/pype/nukestudio/menu.py
index 6babceff41..a996389524 100644
--- a/pype/nukestudio/menu.py
+++ b/pype/nukestudio/menu.py
@@ -1,24 +1,23 @@
import os
import sys
import hiero.core
+from pypeapp import Logger
+from avalon.api import Session
+from hiero.ui import findMenuAction
+# this way we secure compatibility between nuke 10 and 11
try:
from PySide.QtGui import *
except Exception:
from PySide2.QtGui import *
from PySide2.QtWidgets import *
-from hiero.ui import findMenuAction
-
-from avalon.api import Session
-
from .tags import add_tags_from_presets
from .lib import (
reload_config,
set_workfiles
)
-from pypeapp import Logger
log = Logger().get_logger(__name__, "nukestudio")
@@ -45,6 +44,11 @@ def _update_menu_task_label(*args):
def install():
+ """
+ Installing menu into Nukestudio
+
+ """
+
# here is the best place to add menu
from avalon.tools import (
creator,
@@ -127,8 +131,6 @@ def install():
'icon': QIcon('icons:ColorAdd.png')
}]
-
-
# Create menu items
for a in actions:
add_to_menu = menu
diff --git a/pype/nukestudio/tags.py b/pype/nukestudio/tags.py
index d9574bdf2b..8ae88d731c 100644
--- a/pype/nukestudio/tags.py
+++ b/pype/nukestudio/tags.py
@@ -1,5 +1,6 @@
import re
import os
+import hiero
from pypeapp import (
config,
@@ -7,8 +8,6 @@ from pypeapp import (
)
from avalon import io
-import hiero
-
log = Logger().get_logger(__name__, "nukestudio")
diff --git a/pype/plugins/maya/load/load_reference.py b/pype/plugins/maya/load/load_reference.py
index 199d79c941..fb4b90a1cd 100644
--- a/pype/plugins/maya/load/load_reference.py
+++ b/pype/plugins/maya/load/load_reference.py
@@ -74,12 +74,14 @@ class ReferenceLoader(pype.maya.plugin.ReferenceLoader):
# for backwards compatibility
class AbcLoader(ReferenceLoader):
+ label = "Deprecated loader (don't use)"
families = ["pointcache", "animation"]
representations = ["abc"]
tool_names = []
# for backwards compatibility
class ModelLoader(ReferenceLoader):
+ label = "Deprecated loader (don't use)"
families = ["model", "pointcache"]
representations = ["abc"]
tool_names = []
diff --git a/pype/plugins/maya/publish/collect_renderlayers.py b/pype/plugins/maya/publish/collect_renderlayers.py
index 593ab2e74d..ce80039362 100644
--- a/pype/plugins/maya/publish/collect_renderlayers.py
+++ b/pype/plugins/maya/publish/collect_renderlayers.py
@@ -64,9 +64,9 @@ class CollectMayaRenderlayers(pyblish.api.ContextPlugin):
"subset": layername,
"setMembers": layer,
"publish": True,
- "frameStart": self.get_render_attribute("frameStart",
+ "frameStart": self.get_render_attribute("startFrame",
layer=layer),
- "frameEnd": self.get_render_attribute("frameEnd",
+ "frameEnd": self.get_render_attribute("endFrame",
layer=layer),
"byFrameStep": self.get_render_attribute("byFrameStep",
layer=layer),
diff --git a/pype/plugins/maya/publish/validate_scene_set_workspace.py b/pype/plugins/maya/publish/validate_scene_set_workspace.py
index 778c7eae86..bda397cf2a 100644
--- a/pype/plugins/maya/publish/validate_scene_set_workspace.py
+++ b/pype/plugins/maya/publish/validate_scene_set_workspace.py
@@ -12,7 +12,7 @@ def is_subdir(path, root_dir):
root_dir = os.path.realpath(root_dir)
# If not on same drive
- if os.path.splitdrive(path)[0] != os.path.splitdrive(root_dir)[0]:
+ if os.path.splitdrive(path)[0].lower() != os.path.splitdrive(root_dir)[0].lower(): # noqa: E501
return False
# Get 'relative path' (can contain ../ which means going up)
diff --git a/pype/plugins/nuke/create/create_write.py b/pype/plugins/nuke/create/create_write.py
index 588e5ee6f3..03107238b5 100644
--- a/pype/plugins/nuke/create/create_write.py
+++ b/pype/plugins/nuke/create/create_write.py
@@ -16,7 +16,6 @@ def subset_to_families(subset, family, families):
new_subset = families + subset_sufx
return "{}.{}".format(family, new_subset)
-
class CreateWriteRender(avalon.nuke.Creator):
# change this to template preset
preset = "render"
diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py
index 44b43d0f9a..5fd43d3481 100644
--- a/pype/plugins/nuke/load/load_sequence.py
+++ b/pype/plugins/nuke/load/load_sequence.py
@@ -76,7 +76,7 @@ class LoadSequence(api.Loader):
"""Load image sequence into Nuke"""
families = ["write", "source", "plate", "render"]
- representations = ["exr", "dpx"]
+ representations = ["exr", "dpx", "jpg", "jpeg"]
label = "Load sequence"
order = -10
@@ -94,20 +94,11 @@ class LoadSequence(api.Loader):
log.info("version_data: {}\n".format(version_data))
+ self.first_frame = int(nuke.root()["first_frame"].getValue())
+ self.handle_start = version_data.get("handleStart", 0)
+
first = version_data.get("frameStart", None)
last = version_data.get("frameEnd", None)
- handles = version_data.get("handles", 0)
- handle_start = version_data.get("handleStart", 0)
- handle_end = version_data.get("handleEnd", 0)
-
- # fix handle start and end if none are available
- if not handle_start and not handle_end:
- handle_start = handles
- handle_end = handles
-
- # # create handles offset
- # first -= handle_start
- # last += handle_end
# Fallback to asset name when namespace is None
if namespace is None:
@@ -138,7 +129,7 @@ class LoadSequence(api.Loader):
r["last"].setValue(int(last))
# add additional metadata from the version to imprint to Avalon knob
- add_keys = ["frameStart", "frameEnd", "handles",
+ add_keys = ["frameStart", "frameEnd",
"source", "colorspace", "author", "fps", "version",
"handleStart", "handleEnd"]
@@ -147,12 +138,18 @@ class LoadSequence(api.Loader):
if k is 'version':
data_imprint.update({k: context["version"]['name']})
else:
- data_imprint.update({k: context["version"]['data'].get(k, str(None))})
+ data_imprint.update(
+ {k: context["version"]['data'].get(k, str(None))})
data_imprint.update({"objectName": read_name})
r["tile_color"].setValue(int("0x4ecd25ff", 16))
+ if version_data.get("retime", None):
+ speed = version_data.get("speed", 1)
+ time_warp_nodes = version_data.get("timewarps", [])
+ self.make_retimes(r, speed, time_warp_nodes)
+
return containerise(r,
name=name,
namespace=namespace,
@@ -160,6 +157,34 @@ class LoadSequence(api.Loader):
loader=self.__class__.__name__,
data=data_imprint)
+ def make_retimes(self, node, speed, time_warp_nodes):
+ ''' Create all retime and timewarping nodes with coppied animation '''
+ if speed != 1:
+ rtn = nuke.createNode(
+ "Retime",
+ "speed {}".format(speed))
+ rtn["before"].setValue("continue")
+ rtn["after"].setValue("continue")
+ rtn["input.first_lock"].setValue(True)
+ rtn["input.first"].setValue(
+ self.handle_start + self.first_frame
+ )
+
+ if time_warp_nodes != []:
+ for timewarp in time_warp_nodes:
+ twn = nuke.createNode(timewarp["Class"],
+ "name {}".format(timewarp["name"]))
+ if isinstance(timewarp["lookup"], list):
+ # if array for animation
+ twn["lookup"].setAnimated()
+ for i, value in enumerate(timewarp["lookup"]):
+ twn["lookup"].setValueAt(
+ (self.first_frame + i) + value,
+ (self.first_frame + i))
+ else:
+ # if static value `int`
+ twn["lookup"].setValue(timewarp["lookup"])
+
def switch(self, container, representation):
self.update(container, representation)
@@ -200,11 +225,11 @@ class LoadSequence(api.Loader):
version_data = version.get("data", {})
+ self.first_frame = int(nuke.root()["first_frame"].getValue())
+ self.handle_start = version_data.get("handleStart", 0)
+
first = version_data.get("frameStart", None)
last = version_data.get("frameEnd", None)
- handles = version_data.get("handles", 0)
- handle_start = version_data.get("handleStart", 0)
- handle_end = version_data.get("handleEnd", 0)
if first is None:
log.warning("Missing start frame for updated version"
@@ -212,15 +237,6 @@ class LoadSequence(api.Loader):
"{} ({})".format(node['name'].value(), representation))
first = 0
- # fix handle start and end if none are available
- if not handle_start and not handle_end:
- handle_start = handles
- handle_end = handles
-
- # create handles offset
- first -= handle_start
- last += handle_end
-
# Update the loader's path whilst preserving some values
with preserve_trim(node):
node["file"].setValue(file["path"])
@@ -241,7 +257,6 @@ class LoadSequence(api.Loader):
"version": version.get("name"),
"colorspace": version_data.get("colorspace"),
"source": version_data.get("source"),
- "handles": version_data.get("handles"),
"handleStart": version_data.get("handleStart"),
"handleEnd": version_data.get("handleEnd"),
"fps": version_data.get("fps"),
@@ -255,6 +270,11 @@ class LoadSequence(api.Loader):
else:
node["tile_color"].setValue(int("0x4ecd25ff", 16))
+ if version_data.get("retime", None):
+ speed = version_data.get("speed", 1)
+ time_warp_nodes = version_data.get("timewarps", [])
+ self.make_retimes(node, speed, time_warp_nodes)
+
# Update the imprinted representation
update_container(
node,
diff --git a/pype/plugins/nuke/publish/collect_asset_info.py b/pype/plugins/nuke/publish/collect_asset_info.py
index 4bfcb0ab00..76b93ef3d0 100644
--- a/pype/plugins/nuke/publish/collect_asset_info.py
+++ b/pype/plugins/nuke/publish/collect_asset_info.py
@@ -1,4 +1,3 @@
-import nuke
from avalon import api, io
import pyblish.api
@@ -19,5 +18,6 @@ class CollectAssetInfo(pyblish.api.ContextPlugin):
self.log.info("asset_data: {}".format(asset_data))
context.data['handles'] = int(asset_data["data"].get("handles", 0))
- context.data["handleStart"] = int(asset_data["data"].get("handleStart", 0))
+ context.data["handleStart"] = int(asset_data["data"].get(
+ "handleStart", 0))
context.data["handleEnd"] = int(asset_data["data"].get("handleEnd", 0))
diff --git a/pype/plugins/nuke/publish/collect_legacy_read.py b/pype/plugins/nuke/publish/collect_legacy_read.py
new file mode 100644
index 0000000000..6b6ce57245
--- /dev/null
+++ b/pype/plugins/nuke/publish/collect_legacy_read.py
@@ -0,0 +1,30 @@
+import toml
+
+import nuke
+
+import pyblish.api
+
+
+class CollectReadLegacy(pyblish.api.ContextPlugin):
+ """Collect legacy read nodes."""
+
+ order = pyblish.api.CollectorOrder
+ label = "Collect Read Legacy"
+ hosts = ["nuke", "nukeassist"]
+
+ def process(self, context):
+
+ for node in nuke.allNodes():
+ if node.Class() != "Read":
+ continue
+
+ if "avalon" not in node.knobs().keys():
+ continue
+
+ if not toml.loads(node["avalon"].value()):
+ return
+
+ instance = context.create_instance(
+ node.name(), family="read.legacy"
+ )
+ instance.append(node)
diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py
index a5782ac223..0017de3ec4 100644
--- a/pype/plugins/nuke/publish/submit_nuke_deadline.py
+++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py
@@ -27,9 +27,13 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
def process(self, instance):
- # root = nuke.root()
- # node_subset_name = instance.data.get("name", None)
- node = instance[1]
+ node = None
+ for x in instance:
+ if x.Class() == "Write":
+ node = x
+
+ if node is None:
+ return
DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL",
"http://localhost:8082")
diff --git a/pype/plugins/nuke/publish/validate_read_legacy.py b/pype/plugins/nuke/publish/validate_read_legacy.py
new file mode 100644
index 0000000000..22a9b3678e
--- /dev/null
+++ b/pype/plugins/nuke/publish/validate_read_legacy.py
@@ -0,0 +1,83 @@
+import os
+import toml
+
+import nuke
+
+import pyblish.api
+from avalon import api
+from bson.objectid import ObjectId
+
+
+class RepairReadLegacyAction(pyblish.api.Action):
+
+ label = "Repair"
+ icon = "wrench"
+ on = "failed"
+
+ def process(self, context, plugin):
+
+ # Get the errored instances
+ failed = []
+ for result in context.data["results"]:
+ if (result["error"] is not None and result["instance"] is not None
+ and result["instance"] not in failed):
+ failed.append(result["instance"])
+
+ # Apply pyblish.logic to get the instances for the plug-in
+ instances = pyblish.api.instances_by_plugin(failed, plugin)
+
+ for instance in instances:
+
+ data = toml.loads(instance[0]["avalon"].value())
+ data["name"] = instance[0].name()
+ data["xpos"] = instance[0].xpos()
+ data["ypos"] = instance[0].ypos()
+ data["extension"] = os.path.splitext(
+ instance[0]["file"].value()
+ )[1][1:]
+
+ data["connections"] = []
+ for d in instance[0].dependent():
+ for i in range(d.inputs()):
+ if d.input(i) == instance[0]:
+ data["connections"].append([i, d])
+
+ nuke.delete(instance[0])
+
+ loader_name = "LoadSequence"
+ if data["extension"] == "mov":
+ loader_name = "LoadMov"
+
+ loader_plugin = None
+ for Loader in api.discover(api.Loader):
+ if Loader.__name__ != loader_name:
+ continue
+
+ loader_plugin = Loader
+
+ api.load(
+ Loader=loader_plugin,
+ representation=ObjectId(data["representation"])
+ )
+
+ node = nuke.toNode(data["name"])
+ for connection in data["connections"]:
+ connection[1].setInput(connection[0], node)
+
+ node.setXYpos(data["xpos"], data["ypos"])
+
+
+class ValidateReadLegacy(pyblish.api.InstancePlugin):
+ """Validate legacy read instance[0]s."""
+
+ order = pyblish.api.ValidatorOrder
+ optional = True
+ families = ["read.legacy"]
+ label = "Read Legacy"
+ hosts = ["nuke"]
+ actions = [RepairReadLegacyAction]
+
+ def process(self, instance):
+
+ msg = "Clean up legacy read node \"{}\"".format(instance)
+ assert False, msg
diff --git a/pype/plugins/nukestudio/publish/collect_calculate_retime.py b/pype/plugins/nukestudio/publish/collect_calculate_retime.py
new file mode 100644
index 0000000000..a97b43a4ce
--- /dev/null
+++ b/pype/plugins/nukestudio/publish/collect_calculate_retime.py
@@ -0,0 +1,121 @@
+from pyblish import api
+import hiero
+import math
+
+
+class CollectCalculateRetime(api.InstancePlugin):
+ """Calculate Retiming of selected track items."""
+
+ order = api.CollectorOrder + 0.02
+ label = "Collect Calculate Retiming"
+ hosts = ["nukestudio"]
+ families = ['retime']
+
+ def process(self, instance):
+ margin_in = instance.data["retimeMarginIn"]
+ margin_out = instance.data["retimeMarginOut"]
+ self.log.debug("margin_in: '{0}', margin_out: '{1}'".format(margin_in, margin_out))
+
+ handle_start = instance.data["handleStart"]
+ handle_end = instance.data["handleEnd"]
+
+ track_item = instance.data["item"]
+
+ # define basic clip frame range variables
+ timeline_in = int(track_item.timelineIn())
+ timeline_out = int(track_item.timelineOut())
+ source_in = int(track_item.sourceIn())
+ source_out = int(track_item.sourceOut())
+ speed = track_item.playbackSpeed()
+ self.log.debug("_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`,\
+ \n source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n handle_start: `{5}`,\n handle_end: `{6}`".format(
+ timeline_in,
+ timeline_out,
+ source_in,
+ source_out,
+ speed,
+ handle_start,
+ handle_end
+ ))
+
+ # loop withing subtrack items
+ source_in_change = 0
+ source_out_change = 0
+ for s_track_item in track_item.linkedItems():
+ if isinstance(s_track_item, hiero.core.EffectTrackItem) \
+ and "TimeWarp" in s_track_item.node().Class():
+
+ # adding timewarp attribute to instance
+ if not instance.data.get("timeWarpNodes", None):
+ instance.data["timeWarpNodes"] = list()
+
+ # ignore item if not enabled
+ if s_track_item.isEnabled():
+ node = s_track_item.node()
+ name = node["name"].value()
+ look_up = node["lookup"].value()
+ animated = node["lookup"].isAnimated()
+ if animated:
+ look_up = [((node["lookup"].getValueAt(i)) - i)
+ for i in range((timeline_in - handle_start), (timeline_out + handle_end) + 1)
+ ]
+ # calculate differnce
+ diff_in = (node["lookup"].getValueAt(
+ timeline_in)) - timeline_in
+ diff_out = (node["lookup"].getValueAt(
+ timeline_out)) - timeline_out
+
+ # calculate source
+ source_in_change += diff_in
+ source_out_change += diff_out
+
+ # calculate speed
+ speed_in = (node["lookup"].getValueAt(timeline_in) / (
+ float(timeline_in) * .01)) * .01
+ speed_out = (node["lookup"].getValueAt(timeline_out) / (
+ float(timeline_out) * .01)) * .01
+
+ # calculate handles
+ handle_start = int(
+ math.ceil(
+ (handle_start * speed_in * 1000) / 1000.0)
+ )
+
+ handle_end = int(
+ math.ceil(
+ (handle_end * speed_out * 1000) / 1000.0)
+ )
+ self.log.debug(
+ ("diff_in, diff_out", diff_in, diff_out))
+ self.log.debug(
+ ("source_in_change, source_out_change", source_in_change, source_out_change))
+
+ instance.data["timeWarpNodes"].append({"Class": "TimeWarp",
+ "name": name,
+ "lookup": look_up})
+
+ self.log.debug((source_in_change, source_out_change))
+ # recalculate handles by the speed
+ handle_start *= speed
+ handle_end *= speed
+ self.log.debug("speed: handle_start: '{0}', handle_end: '{1}'".format(handle_start, handle_end))
+
+ source_in += int(source_in_change)
+ source_out += int(source_out_change * speed)
+ handle_start += (margin_in)
+ handle_end += (margin_out)
+ self.log.debug("margin: handle_start: '{0}', handle_end: '{1}'".format(handle_start, handle_end))
+
+ # add all data to Instance
+ instance.data["sourceIn"] = source_in
+ instance.data["sourceOut"] = source_out
+ instance.data["sourceInH"] = int(source_in - math.ceil(
+ (handle_start * 1000) / 1000.0))
+ instance.data["sourceOutH"] = int(source_out + math.ceil(
+ (handle_end * 1000) / 1000.0))
+ instance.data["speed"] = speed
+
+ self.log.debug("timeWarpNodes: {}".format(instance.data["timeWarpNodes"]))
+ self.log.debug("sourceIn: {}".format(instance.data["sourceIn"]))
+ self.log.debug("sourceOut: {}".format(instance.data["sourceOut"]))
+ self.log.debug("speed: {}".format(instance.data["speed"]))
diff --git a/pype/plugins/nukestudio/publish/collect_frame_ranges.py b/pype/plugins/nukestudio/publish/collect_frame_ranges.py
index 392dbba68b..38224f683d 100644
--- a/pype/plugins/nukestudio/publish/collect_frame_ranges.py
+++ b/pype/plugins/nukestudio/publish/collect_frame_ranges.py
@@ -1,5 +1,6 @@
import pyblish.api
+
class CollectClipFrameRanges(pyblish.api.InstancePlugin):
"""Collect all frame range data: source(In,Out), timeline(In,Out), edit_(in, out), f(start, end)"""
@@ -15,8 +16,10 @@ class CollectClipFrameRanges(pyblish.api.InstancePlugin):
handle_start = instance.data["handleStart"]
handle_end = instance.data["handleEnd"]
- source_in_h = instance.data["sourceIn"] - handle_start
- source_out_h = instance.data["sourceOut"] + handle_end
+ source_in_h = instance.data("sourceInH",
+ instance.data("sourceIn") - handle_start)
+ source_out_h = instance.data("sourceOutH",
+ instance.data("sourceOut") + handle_end)
timeline_in = instance.data["clipIn"]
timeline_out = instance.data["clipOut"]
diff --git a/pype/plugins/nukestudio/publish/collect_plates.py b/pype/plugins/nukestudio/publish/collect_plates.py
index c9e6305062..9843307f14 100644
--- a/pype/plugins/nukestudio/publish/collect_plates.py
+++ b/pype/plugins/nukestudio/publish/collect_plates.py
@@ -137,7 +137,6 @@ class CollectPlatesData(api.InstancePlugin):
"subset": name,
"fps": instance.context.data["fps"]
})
- instance.data["versionData"] = version_data
try:
basename, ext = os.path.splitext(source_file)
@@ -156,9 +155,11 @@ class CollectPlatesData(api.InstancePlugin):
start_frame = source_first_frame + instance.data["sourceInH"]
duration = instance.data["sourceOutH"] - instance.data["sourceInH"]
end_frame = start_frame + duration
+ self.log.debug("start_frame: `{}`".format(start_frame))
+ self.log.debug("end_frame: `{}`".format(end_frame))
files = [file % i for i in range(start_frame, (end_frame + 1), 1)]
except Exception as e:
- self.log.debug("Exception in file: {}".format(e))
+ self.log.warning("Exception in file: {}".format(e))
head, ext = os.path.splitext(source_file)
ext = ext[1:]
files = source_file
@@ -207,16 +208,41 @@ class CollectPlatesData(api.InstancePlugin):
thumb_representation)
# adding representation for plates
+ frame_start = instance.data["frameStart"] - \
+ instance.data["handleStart"]
+ frame_end = instance.data["frameEnd"] + instance.data["handleEnd"]
+
+ # exception for retimes
+ if instance.data.get("retime"):
+ source_in_h = instance.data["sourceInH"]
+ source_in = instance.data["sourceIn"]
+ source_handle_start = source_in_h - source_in
+ frame_start = instance.data["frameStart"] + source_handle_start
+ duration = instance.data["sourceOutH"] - instance.data["sourceInH"]
+ frame_end = frame_start + duration
+
plates_representation = {
'files': files,
'stagingDir': staging_dir,
'name': ext,
'ext': ext,
- "frameStart": instance.data["frameStart"] - instance.data["handleStart"],
- "frameEnd": instance.data["frameEnd"] + instance.data["handleEnd"],
+ "frameStart": frame_start,
+ "frameEnd": frame_end,
}
instance.data["representations"].append(plates_representation)
+ # deal with retimed clip
+ if instance.data.get("retime"):
+ version_data.update({
+ "retime": True,
+ "speed": instance.data.get("speed", 1),
+ "timewarps": instance.data.get("timeWarpNodes", []),
+ "frameStart": frame_start,
+ "frameEnd": frame_end,
+ })
+
+ instance.data["versionData"] = version_data
+
# testing families
family = instance.data["family"]
families = instance.data["families"]
diff --git a/pype/plugins/nukestudio/publish/collect_tag_framestart.py b/pype/plugins/nukestudio/publish/collect_tag_framestart.py
index c2778ea680..c73a2dd1ee 100644
--- a/pype/plugins/nukestudio/publish/collect_tag_framestart.py
+++ b/pype/plugins/nukestudio/publish/collect_tag_framestart.py
@@ -1,5 +1,5 @@
from pyblish import api
-
+import os
class CollectClipTagFrameStart(api.InstancePlugin):
"""Collect FrameStart from Tags of selected track items."""
@@ -19,8 +19,20 @@ class CollectClipTagFrameStart(api.InstancePlugin):
# gets only task family tags and collect labels
if "frameStart" in t_family:
+ t_value = t_metadata.get("tag.value", "")
+
+ # backward compatibility
t_number = t_metadata.get("tag.number", "")
- start_frame = int(t_number)
+
+ try:
+ start_frame = int(t_number) or int(t_value)
+ except ValueError:
+ if "source" in t_value:
+ source_first = instance.data["sourceFirst"]
+ source_in = instance.data["sourceIn"]
+ handle_start = instance.data["handleStart"]
+ start_frame = (source_first + source_in) - handle_start
+
instance.data["startingFrame"] = start_frame
self.log.info("Start frame on `{0}` set to `{1}`".format(
instance, start_frame
diff --git a/pype/plugins/nukestudio/publish/collect_tag_retime.py b/pype/plugins/nukestudio/publish/collect_tag_retime.py
new file mode 100644
index 0000000000..32e49e1b2a
--- /dev/null
+++ b/pype/plugins/nukestudio/publish/collect_tag_retime.py
@@ -0,0 +1,32 @@
+from pyblish import api
+
+
+class CollectTagRetime(api.InstancePlugin):
+ """Collect Retiming from Tags of selected track items."""
+
+ order = api.CollectorOrder + 0.014
+ label = "Collect Retiming Tag"
+ hosts = ["nukestudio"]
+ families = ['clip']
+
+ def process(self, instance):
+ # gets tags
+ tags = instance.data["tags"]
+
+ for t in tags:
+ t_metadata = dict(t["metadata"])
+ t_family = t_metadata.get("tag.family", "")
+
+ # gets only task family tags and collect labels
+ if "retiming" in t_family:
+ margin_in = t_metadata.get("tag.marginIn", "")
+ margin_out = t_metadata.get("tag.marginOut", "")
+
+ instance.data["retimeMarginIn"] = int(margin_in)
+ instance.data["retimeMarginOut"] = int(margin_out)
+ instance.data["retime"] = True
+
+ self.log.info("retimeMarginIn: `{}`".format(margin_in))
+ self.log.info("retimeMarginOut: `{}`".format(margin_out))
+
+ instance.data["families"] += ["retime"]
diff --git a/res/ftrack/action_icons/AttributesRemapper.svg b/res/ftrack/action_icons/AttributesRemapper.svg
deleted file mode 100644
index 94bf8c4f14..0000000000
--- a/res/ftrack/action_icons/AttributesRemapper.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/res/ftrack/action_icons/CustomAttributes.svg b/res/ftrack/action_icons/CustomAttributes.svg
deleted file mode 100644
index ee1af3378e..0000000000
--- a/res/ftrack/action_icons/CustomAttributes.svg
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
diff --git a/res/ftrack/action_icons/JobKiller.svg b/res/ftrack/action_icons/JobKiller.svg
deleted file mode 100644
index 595c780a9b..0000000000
--- a/res/ftrack/action_icons/JobKiller.svg
+++ /dev/null
@@ -1,374 +0,0 @@
-
-
diff --git a/res/ftrack/action_icons/PrepareProject.svg b/res/ftrack/action_icons/PrepareProject.svg
new file mode 100644
index 0000000000..bd6b460ce3
--- /dev/null
+++ b/res/ftrack/action_icons/PrepareProject.svg
@@ -0,0 +1,88 @@
+
+
diff --git a/res/ftrack/action_icons/PypeAdmin.svg b/res/ftrack/action_icons/PypeAdmin.svg
new file mode 100644
index 0000000000..c95a29dacb
--- /dev/null
+++ b/res/ftrack/action_icons/PypeAdmin.svg
@@ -0,0 +1,173 @@
+
+
+
+
diff --git a/res/ftrack/action_icons/PypeDoctor.svg b/res/ftrack/action_icons/PypeDoctor.svg
new file mode 100644
index 0000000000..e921d99ee5
--- /dev/null
+++ b/res/ftrack/action_icons/PypeDoctor.svg
@@ -0,0 +1,114 @@
+
+
diff --git a/res/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg b/res/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg
deleted file mode 100644
index f58448ac06..0000000000
--- a/res/ftrack/action_icons/SyncHierarchicalAttrsLocal.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/res/ftrack/action_icons/SyncToAvalon-local.svg b/res/ftrack/action_icons/SyncToAvalon-local.svg
deleted file mode 100644
index bf4708e8a5..0000000000
--- a/res/ftrack/action_icons/SyncToAvalon-local.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/res/ftrack/action_icons/Thumbnail.svg b/res/ftrack/action_icons/Thumbnail.svg
new file mode 100644
index 0000000000..a8780b9a04
--- /dev/null
+++ b/res/ftrack/action_icons/Thumbnail.svg
@@ -0,0 +1,136 @@
+
+
diff --git a/res/ftrack/action_icons/thumbToChildren.svg b/res/ftrack/action_icons/thumbToChildren.svg
deleted file mode 100644
index 709b9549f3..0000000000
--- a/res/ftrack/action_icons/thumbToChildren.svg
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
diff --git a/res/ftrack/action_icons/thumbToParent.svg b/res/ftrack/action_icons/thumbToParent.svg
deleted file mode 100644
index 3efa426332..0000000000
--- a/res/ftrack/action_icons/thumbToParent.svg
+++ /dev/null
@@ -1,130 +0,0 @@
-
-
-
-
diff --git a/setup/nuke/nuke_path/write_to_read.py b/setup/nuke/nuke_path/write_to_read.py
new file mode 100644
index 0000000000..9667dccab6
--- /dev/null
+++ b/setup/nuke/nuke_path/write_to_read.py
@@ -0,0 +1,141 @@
+import re
+import os
+import glob
+import nuke
+from pype import api as pype
+log = pype.Logger().get_logger(__name__, "nuke")
+
+SINGLE_FILE_FORMATS = ['avi', 'mp4', 'mxf', 'mov', 'mpg', 'mpeg', 'wmv', 'm4v',
+ 'm2v']
+
+
+def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame):
+ # get combined relative path
+ combined_relative_path = None
+ if k_eval is not None and project_dir is not None:
+ combined_relative_path = os.path.abspath(
+ os.path.join(project_dir, k_eval))
+ combined_relative_path = combined_relative_path.replace('\\', '/')
+ filetype = combined_relative_path.split('.')[-1]
+ frame_number = re.findall(r'\d+', combined_relative_path)[-1]
+ basename = combined_relative_path[: combined_relative_path.rfind(
+ frame_number)]
+ filepath_glob = basename + '*' + filetype
+ glob_search_results = glob.glob(filepath_glob)
+ if len(glob_search_results) <= 0:
+ combined_relative_path = None
+
+ try:
+ k_value = k_value % first_frame
+ if os.path.exists(k_value):
+ filepath = k_value
+ elif os.path.exists(k_eval):
+ filepath = k_eval
+ elif not isinstance(project_dir, type(None)) and \
+ not isinstance(combined_relative_path, type(None)):
+ filepath = combined_relative_path
+
+ filepath = os.path.abspath(filepath)
+ except Exception as E:
+ log.error("Cannot create Read node. Perhaps it needs to be rendered first :) Error: `{}`".format(E))
+ return
+
+ filepath = filepath.replace('\\', '/')
+ current_frame = re.findall(r'\d+', filepath)[-1]
+ padding = len(current_frame)
+ basename = filepath[: filepath.rfind(current_frame)]
+ filetype = filepath.split('.')[-1]
+
+ # sequence or not?
+ if filetype in SINGLE_FILE_FORMATS:
+ pass
+ else:
+ # Image sequence needs hashes
+ filepath = basename + '#' * padding + '.' + filetype
+
+ # relative path? make it relative again
+ if not isinstance(project_dir, type(None)):
+ filepath = filepath.replace(project_dir, '.')
+
+ # get first and last frame from disk
+ frames = []
+ firstframe = 0
+ lastframe = 0
+ filepath_glob = basename + '*' + filetype
+ glob_search_results = glob.glob(filepath_glob)
+ for f in glob_search_results:
+ frame = re.findall(r'\d+', f)[-1]
+ frames.append(frame)
+ frames = sorted(frames)
+ firstframe = frames[0]
+ lastframe = frames[len(frames) - 1]
+ if lastframe < 0:
+ lastframe = firstframe
+
+ return filepath, firstframe, lastframe
+
+
+def create_read_node(ndata, comp_start):
+ read = nuke.createNode('Read', 'file ' + ndata['filepath'])
+ read.knob('colorspace').setValue(int(ndata['colorspace']))
+ read.knob('raw').setValue(ndata['rawdata'])
+ read.knob('first').setValue(int(ndata['firstframe']))
+ read.knob('last').setValue(int(ndata['lastframe']))
+ read.knob('origfirst').setValue(int(ndata['firstframe']))
+ read.knob('origlast').setValue(int(ndata['lastframe']))
+ if comp_start == int(ndata['firstframe']):
+ read.knob('frame_mode').setValue("1")
+ read.knob('frame').setValue(str(comp_start))
+ else:
+ read.knob('frame_mode').setValue("0")
+ read.knob('xpos').setValue(ndata['new_xpos'])
+ read.knob('ypos').setValue(ndata['new_ypos'])
+ nuke.inputs(read, 0)
+ return
+
+
+def write_to_read(gn):
+ comp_start = nuke.Root().knob('first_frame').value()
+ comp_end = nuke.Root().knob('last_frame').value()
+ project_dir = nuke.Root().knob('project_directory').getValue()
+ if not os.path.exists(project_dir):
+ project_dir = nuke.Root().knob('project_directory').evaluate()
+
+ group_read_nodes = []
+
+ with gn:
+ height = gn.screenHeight() # get group height and position
+ new_xpos = int(gn.knob('xpos').value())
+ new_ypos = int(gn.knob('ypos').value()) + height + 20
+ group_writes = [n for n in nuke.allNodes() if n.Class() == "Write"]
+ print("__ group_writes: {}".format(group_writes))
+ if group_writes != []:
+ # there can be only 1 write node, taking first
+ n = group_writes[0]
+
+ if n.knob('file') is not None:
+ myfiletranslated, firstFrame, lastFrame = evaluate_filepath_new(
+ n.knob('file').getValue(),
+ n.knob('file').evaluate(),
+ project_dir,
+ comp_start
+ )
+ # get node data
+ ndata = {
+ 'filepath': myfiletranslated,
+ 'firstframe': firstFrame,
+ 'lastframe': lastFrame,
+ 'new_xpos': new_xpos,
+ 'new_ypos': new_ypos,
+ 'colorspace': n.knob('colorspace').getValue(),
+ 'rawdata': n.knob('raw').value(),
+ 'write_frame_mode': str(n.knob('frame_mode').value()),
+ 'write_frame': n.knob('frame').value()
+ }
+ group_read_nodes.append(ndata)
+
+
+ # create reads in one go
+ for oneread in group_read_nodes:
+ # create read node
+ create_read_node(oneread, comp_start)
diff --git a/setup/nukestudio/hiero_plugin_path/Icons/retiming.png b/setup/nukestudio/hiero_plugin_path/Icons/retiming.png
new file mode 100644
index 0000000000..4487ac0422
Binary files /dev/null and b/setup/nukestudio/hiero_plugin_path/Icons/retiming.png differ
diff --git a/setup/nukestudio/hiero_plugin_path/Icons/retiming.psd b/setup/nukestudio/hiero_plugin_path/Icons/retiming.psd
new file mode 100644
index 0000000000..bac6fc6b58
Binary files /dev/null and b/setup/nukestudio/hiero_plugin_path/Icons/retiming.psd differ