From af41c4e90a552e4e3dc2310e5e2acec479c4ca96 Mon Sep 17 00:00:00 2001 From: antirotor Date: Mon, 3 Dec 2018 18:42:25 +0100 Subject: [PATCH 001/210] cleaned Houdini support --- setup/houdini/MainMenuCommon.XML | 68 ++++++++++++++++++++++++++++++++ setup/houdini/scripts/123.py | 10 +++++ 2 files changed, 78 insertions(+) create mode 100644 setup/houdini/MainMenuCommon.XML create mode 100644 setup/houdini/scripts/123.py diff --git a/setup/houdini/MainMenuCommon.XML b/setup/houdini/MainMenuCommon.XML new file mode 100644 index 0000000000..0d7cf5ab23 --- /dev/null +++ b/setup/houdini/MainMenuCommon.XML @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup/houdini/scripts/123.py b/setup/houdini/scripts/123.py new file mode 100644 index 0000000000..499a0e2931 --- /dev/null +++ b/setup/houdini/scripts/123.py @@ -0,0 +1,10 @@ +from avalon import pipeline, houdini +import hou + + +def main(): + print("Installing Avalon ...") + pipeline.install(houdini) + + +main() From 72412ad863f56729f075753cd78734c9c72336d5 Mon Sep 17 00:00:00 2001 From: antirotor Date: Tue, 8 Jan 2019 13:47:54 +0100 Subject: [PATCH 002/210] better deadline handling --- .../maya/create/create_renderglobals.py | 28 ++++++++++--------- .../publish/validate_deadline_connection.py | 11 ++------ 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/pype/plugins/maya/create/create_renderglobals.py b/pype/plugins/maya/create/create_renderglobals.py index 61fd76f6ef..b9e4f99ad5 100644 --- a/pype/plugins/maya/create/create_renderglobals.py +++ b/pype/plugins/maya/create/create_renderglobals.py @@ -20,18 +20,22 @@ class CreateRenderGlobals(avalon.maya.Creator): self.data["id"] = "avalon.renderglobals" # get pools - try: - deadline_url = os.environ["DEADLINE_REST_URL"] - except KeyError: - self.log.error("Deadline REST API url not found.") + pools = [] + data = OrderedDict(**self.data) - argument = "{}/api/pools?NamesOnly=true".format(deadline_url) - response = requests.get(argument) - if not response.ok: - self.log.warning("No pools retrieved") - pools = [] + deadline_url = os.environ.get('DEADLINE_REST_URL', None) + if deadline_url is None: + self.log.warning("Deadline REST API url not found.") else: - pools = response.json() + argument = "{}/api/pools?NamesOnly=true".format(deadline_url) + response = requests.get(argument) + if not response.ok: + self.log.warning("No pools retrieved") + else: + pools = response.json() + data["primaryPool"] = pools + # We add a string "-" to allow the user to not set any secondary pools + data["secondaryPool"] = ["-"] + pools # We don't need subset or asset attributes self.data.pop("subset", None) @@ -49,9 +53,7 @@ class CreateRenderGlobals(avalon.maya.Creator): data["whitelist"] = False data["machineList"] = "" data["useMayaBatch"] = True - data["primaryPool"] = pools - # We add a string "-" to allow the user to not set any secondary pools - data["secondaryPool"] = ["-"] + pools + self.data = data self.options = {"useSelection": False} # Force no content diff --git a/pype/plugins/maya/publish/validate_deadline_connection.py b/pype/plugins/maya/publish/validate_deadline_connection.py index d0f6e9e54d..e4b0eebb60 100644 --- a/pype/plugins/maya/publish/validate_deadline_connection.py +++ b/pype/plugins/maya/publish/validate_deadline_connection.py @@ -15,15 +15,10 @@ class ValidateDeadlineConnection(pyblish.api.ContextPlugin): def process(self, instance): - # AVALON_DEADLINE = api.Session.get("AVALON_DEADLINE", - # "http://localhost:8082") - # - # assert AVALON_DEADLINE is not None, "Requires AVALON_DEADLINE" - - try: - deadline_url = os.environ["DEADLINE_REST_URL"] - except KeyError: + deadline_url = os.environ.get('DEADLINE_REST_URL', None) + if deadline_url is None: self.log.error("Deadline REST API url not found.") + raise ValueError("Deadline REST API url not found.") # Check response response = requests.get(deadline_url) From 856cb1e87092fcf144e4b5a6d71125635a44c785 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 12 Feb 2019 18:29:32 +0100 Subject: [PATCH 003/210] djv action is redone, reads from presets possible paths to djv --- pype/ftrack/actions/action_djvview.py | 151 ++++++++++---------------- 1 file changed, 59 insertions(+), 92 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 0be3624f2d..e4798f2aa4 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -1,23 +1,30 @@ +import os +import sys +import re +import json import logging import subprocess -import sys -import os -import re from operator import itemgetter import ftrack_api from pype.ftrack import BaseHandler +from app.api import Logger +from pype import lib + +log = Logger.getLogger(__name__) class DJVViewAction(BaseHandler): """Launch DJVView action.""" identifier = "djvview-launch-action" - # label = "DJV View" - # icon = "http://a.fsdn.com/allura/p/djv/icon" + label = "DJV View" + icon = "http://a.fsdn.com/allura/p/djv/icon" type = 'Application' def __init__(self, session): '''Expects a ftrack_api.Session instance''' super().__init__(session) + self.variant = None + self.djv_path = None if self.identifier is None: raise ValueError( @@ -44,23 +51,21 @@ class DJVViewAction(BaseHandler): return items = [] - applications = self.get_applications() - applications = sorted( - applications, key=lambda application: application["label"] - ) + application = self.get_application() + if application is None: + log.debug("DVJ View application was not found") + return - for application in applications: - self.djv_path = application.get("path", None) - applicationIdentifier = application["identifier"] - label = application["label"] - items.append({ - "actionIdentifier": self.identifier, - "label": label, - "variant": application.get("variant", None), - "description": application.get("description", None), - "icon": application.get("icon", "default"), - "applicationIdentifier": applicationIdentifier - }) + applicationIdentifier = application["identifier"] + label = application["label"] + items.append({ + "actionIdentifier": self.identifier, + "label": label, + "variant": application.get("variant", None), + "description": application.get("description", None), + "icon": application.get("icon", "default"), + "applicationIdentifier": applicationIdentifier + }) return { "items": items @@ -86,88 +91,50 @@ class DJVViewAction(BaseHandler): self.launch ) - def get_applications(self): - applications = [] + def get_application(self): - label = "DJVView {version}" - versionExpression = re.compile(r"(?P\d+.\d+.\d+)") applicationIdentifier = "djvview" description = "DJV View Launcher" - icon = "http://a.fsdn.com/allura/p/djv/icon" - expression = [] - if sys.platform == "win32": - expression = ["C:\\", "Program Files", "djv-\d.+", - "bin", "djv_view.exe"] - elif sys.platform == "darwin": - expression = ["Application", "DJV.app", "Contents", "MacOS", "DJV"] - # Linuxs - else: - expression = ["usr", "local", "djv", "djv_view"] + path_items = [lib.get_presets_path(), 'djv_view', 'app_paths.json'] + filepath = os.path.sep.join(path_items) - pieces = expression[:] - start = pieces.pop(0) - - if sys.platform == 'win32': - # On Windows C: means current directory so convert roots that look - # like drive letters to the C:\ format. - if start and start[-1] == ':': - start += '\\' - - if not os.path.exists(start): - raise ValueError( - 'First part "{0}" of expression "{1}" must match exactly to an' - ' existing entry on the filesystem.' - .format(start, expression) + data = dict() + try: + with open(filepath) as data_file: + data = json.load(data_file) + except Exception as e: + log.warning( + 'Failed to load data from DJV presets file ({})'.format(e) ) + return None - expressions = list(map(re.compile, pieces)) - expressionsCount = len(expression)-1 + possible_paths = data.get("djv_paths", []) + for path in possible_paths: + if os.path.exists(path): + self.djv_path = path + break - for location, folders, files in os.walk( - start, topdown=True, followlinks=True - ): - level = location.rstrip(os.path.sep).count(os.path.sep) - expression = expressions[level] + if self.djv_path is None: + log.warning('Any path from presets match your DJV View path') + return None - if level < (expressionsCount - 1): - # If not yet at final piece then just prune directories. - folders[:] = [folder for folder in folders - if expression.match(folder)] - else: - # Match executable. Note that on OSX executable might equate to - # a folder (.app). - for entry in folders + files: - match = expression.match(entry) - if match: - # Extract version from full matching path. - path = os.path.join(start, location, entry) - versionMatch = versionExpression.search(path) - if versionMatch: - version = versionMatch.group('version') + application = { + 'identifier': applicationIdentifier, + 'label': self.label, + 'icon': self.icon, + 'description': description + } - applications.append({ - 'identifier': applicationIdentifier.format( - version=version - ), - 'path': path, - 'version': version, - 'label': label.format(version=version), - 'icon': icon, - # 'variant': variant.format(version=version), - 'description': description - }) - else: - self.logger.debug( - 'Discovered application executable, but it ' - 'does not appear to o contain required version' - ' information: {0}'.format(path) - ) + versionExpression = re.compile(r"(?P\d+.\d+.\d+)") + versionMatch = versionExpression.search(self.djv_path) + if versionMatch: + new_label = '{} {}'.format( + application['label'], versionMatch.group('version') + ) + application['label'] = new_label - # Don't descend any further as out of patterns to match. - del folders[:] - - return applications + return application def translate_event(self, session, event): '''Return *event* translated structure to be used with the API.''' From 9e8180ee5d995aa6d0fe345919258a78f3975fb7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 14 Feb 2019 10:43:20 +0100 Subject: [PATCH 004/210] file ext loaded from presets --- pype/ftrack/actions/action_djvview.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index e4798f2aa4..3356ffd7a3 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -25,6 +25,7 @@ class DJVViewAction(BaseHandler): super().__init__(session) self.variant = None self.djv_path = None + self.config_data = None if self.identifier is None: raise ValueError( @@ -51,6 +52,9 @@ class DJVViewAction(BaseHandler): return items = [] + if self.config_data is None: + self.load_config_data() + application = self.get_application() if application is None: log.debug("DVJ View application was not found") @@ -91,12 +95,8 @@ class DJVViewAction(BaseHandler): self.launch ) - def get_application(self): - - applicationIdentifier = "djvview" - description = "DJV View Launcher" - - path_items = [lib.get_presets_path(), 'djv_view', 'app_paths.json'] + def load_config_data(self): + path_items = [lib.get_presets_path(), 'djv_view', 'config.json'] filepath = os.path.sep.join(path_items) data = dict() @@ -107,9 +107,14 @@ class DJVViewAction(BaseHandler): log.warning( 'Failed to load data from DJV presets file ({})'.format(e) ) - return None - possible_paths = data.get("djv_paths", []) + self.config_data = data + + def get_application(self): + applicationIdentifier = "djvview" + description = "DJV View Launcher" + + possible_paths = self.config_data.get("djv_paths", []) for path in possible_paths: if os.path.exists(path): self.djv_path = path @@ -269,7 +274,9 @@ class DJVViewAction(BaseHandler): try: for entity in entities: versions = [] - allowed_types = ["img", "mov", "exr"] + self.load_config_data() + default_types = ["img", "mov", "exr"] + allowed_types = self.config_data.get('file_ext', default_types) if entity.entity_type.lower() == "assetversion": if entity['components'][0]['file_type'] in allowed_types: From ea7dacb9c82578a7358e228358fe72e192530578 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 15 Feb 2019 15:48:14 +0100 Subject: [PATCH 005/210] version_up cut from filename everything after version in name --- pype/lib.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index f0ffba3085..cbff032e2b 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -206,6 +206,11 @@ def version_up(filepath): new_label = label.replace(version, new_version, 1) new_basename = _rreplace(basename, label, new_label) + if not new_basename.endswith(new_label): + index = (new_basename.find(new_label)) + index += len(new_label) + new_basename = new_basename[:index] + new_filename = "{}{}".format(new_basename, ext) new_filename = os.path.join(dirname, new_filename) new_filename = os.path.normpath(new_filename) @@ -214,9 +219,10 @@ def version_up(filepath): raise RuntimeError("Created path is the same as current file," "this is a bug") - if os.path.exists(new_filename): - log.info("Skipping existing version %s" % new_label) - return version_up(new_filename) + for file in os.listdir(dirname): + if file.endswith(ext) and file.startswith(new_basename): + log.info("Skipping existing version %s" % new_label) + return version_up(new_filename) log.info("New version %s" % new_label) return new_filename From fdc3700f6261365edb8bd283aec97ea564486822 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sun, 17 Feb 2019 20:26:34 +0100 Subject: [PATCH 006/210] remove `.` from extension when comparing to preset --- pype/ftrack/actions/action_djvview.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 3356ffd7a3..f4e8277401 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -279,7 +279,7 @@ class DJVViewAction(BaseHandler): allowed_types = self.config_data.get('file_ext', default_types) if entity.entity_type.lower() == "assetversion": - if entity['components'][0]['file_type'] in allowed_types: + if entity['components'][0]['file_type'][1:] in allowed_types: versions.append(entity) elif entity.entity_type.lower() == "task": @@ -293,7 +293,7 @@ class DJVViewAction(BaseHandler): continue # Get only components with allowed type filetype = version['components'][0]['file_type'] - if filetype in allowed_types: + if filetype[1:] in allowed_types: versions.append(version) # Raise error if no components were found @@ -301,6 +301,7 @@ class DJVViewAction(BaseHandler): raise ValueError('There are no Asset Versions to open.') for version in versions: + logging.info(version['components']) for component in version['components']: label = "v{0} - {1} - {2}" From 01111e2fb10110be7dbb627ba2fb8aad9a2595ef Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 18 Feb 2019 16:35:42 +0100 Subject: [PATCH 007/210] removed 'return' so registration of app won't exit if name don't include underscore --- pype/ftrack/actions/action_application_loader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index aebe745b6a..3202c19d40 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -18,7 +18,6 @@ def registerApp(app, session): '"{0}" - App "name" and "variant" is not separated by "_"' ' (variant is not set)' ).format(app['name'])) - return abspath = lib.which_app(app['name']) if abspath is None: From 6abc46eaa344176e94e948e996a65a2d8b32e4fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Feb 2019 11:58:26 +0100 Subject: [PATCH 008/210] log of not found DJV was changed from warning to debug --- pype/ftrack/actions/action_djvview.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index f4e8277401..ded361dc14 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -57,7 +57,6 @@ class DJVViewAction(BaseHandler): application = self.get_application() if application is None: - log.debug("DVJ View application was not found") return applicationIdentifier = application["identifier"] @@ -121,7 +120,7 @@ class DJVViewAction(BaseHandler): break if self.djv_path is None: - log.warning('Any path from presets match your DJV View path') + log.debug("DJV View application was not found") return None application = { From db17e1853da70e32d3cb05d53d4f48c959961925 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Feb 2019 11:59:43 +0100 Subject: [PATCH 009/210] added component dir check so unaccessible asset versions are not shown --- pype/ftrack/actions/action_djvview.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index ded361dc14..0e306d48a0 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -329,9 +329,17 @@ class DJVViewAction(BaseHandler): 'component_locations' ][0]['resource_identifier'] - event["data"]["items"].append( - {"label": label, "value": file_path} - ) + dirpath = os.path.dirname(file_path) + if os.path.isdir(dirpath): + event["data"]["items"].append( + {"label": label, "value": file_path} + ) + + # Raise error if any component is playable + if len(event["data"]["items"]) == 0: + raise ValueError( + 'There are no Asset Versions with accessible path.' + ) except Exception as e: return { From c485a6405af805603984bbb0f2cee2c7774a3f7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Feb 2019 11:59:55 +0100 Subject: [PATCH 010/210] formatting cleanup --- pype/ftrack/actions/action_djvview.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 0e306d48a0..3fefc9ed4c 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -278,7 +278,11 @@ class DJVViewAction(BaseHandler): allowed_types = self.config_data.get('file_ext', default_types) if entity.entity_type.lower() == "assetversion": - if entity['components'][0]['file_type'][1:] in allowed_types: + if ( + entity[ + 'components' + ][0]['file_type'][1:] in allowed_types + ): versions.append(entity) elif entity.entity_type.lower() == "task": From a641d1b2c56bf043d7add138cf35d05151aa7301 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 19 Feb 2019 12:06:44 +0100 Subject: [PATCH 011/210] show user message when file is not found --- pype/ftrack/actions/action_djvview.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 3fefc9ed4c..7861f29be1 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -259,8 +259,16 @@ class DJVViewAction(BaseHandler): # PATH TO COMPONENT cmd.append(os.path.normpath(filename)) - # Run DJV with these commands - subprocess.Popen(' '.join(cmd)) + try: + # Run DJV with these commands + subprocess.Popen(' '.join(cmd)) + except FileNotFoundError: + return { + 'success': False, + 'message': 'File "{}" was not found.'.format( + os.path.basename(filename) + ) + } return { 'success': True, From 685069a9ab63ecb047819bb0e4fa20bcbd293ae4 Mon Sep 17 00:00:00 2001 From: antirotor Date: Tue, 19 Feb 2019 23:47:33 +0100 Subject: [PATCH 012/210] new: validator for overlapping mesh uvs --- .../publish/validate_mesh_overlapping_uvs.py | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py diff --git a/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py b/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py new file mode 100644 index 0000000000..ca8faf60a9 --- /dev/null +++ b/pype/plugins/maya/publish/validate_mesh_overlapping_uvs.py @@ -0,0 +1,128 @@ +from maya import cmds + +import pyblish.api +import pype.api +import pype.maya.action + + +class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin): + """Validate the current mesh overlapping UVs. + + It validates whether the current UVs are overlapping or not. + It is optional to warn publisher about it. + """ + + order = pype.api.ValidateMeshOrder + hosts = ['maya'] + families = ['model'] + category = 'geometry' + label = 'Mesh Has Overlapping UVs' + actions = [pype.maya.action.SelectInvalidAction] + optional = True + + @classmethod + def _has_overlapping_uvs(cls, node): + + allUvSets = cmds.polyUVSet(q=1, auv=1) + # print allUvSets + currentTool = cmds.currentCtx() + cmds.setToolTo('selectSuperContext') + biglist = cmds.ls( + cmds.polyListComponentConversion(node, tf=True), fl=True) + shells = [] + overlappers = [] + bounds = [] + for uvset in allUvSets: + # print uvset + while len(biglist) > 0: + cmds.select(biglist[0], r=True) + # cmds.polySelectConstraint(t=0) + # cmds.polySelectConstraint(sh=1,m=2) + # cmds.polySelectConstraint(sh=0,m=0) + aShell = cmds.ls(sl=True, fl=True) + shells.append(aShell) + biglist = list(set(biglist) - set(aShell)) + cmds.setToolTo(currentTool) + cmds.select(clear=True) + # shells = [ [faces in uv shell 1], [faces in shell 2], [etc] ] + + for faces in shells: + shellSets = cmds.polyListComponentConversion( + faces, ff=True, tuv=True) + if shellSets != []: + uv = cmds.polyEditUV(shellSets, q=True) + + uMin = uv[0] + uMax = uv[0] + vMin = uv[1] + vMax = uv[1] + for i in range(len(uv)/2): + if uv[i*2] < uMin: + uMin = uv[i*2] + if uv[i*2] > uMax: + uMax = uv[i*2] + if uv[i*2+1] < vMin: + vMin = uv[i*2+1] + if uv[i*2+1] > vMax: + vMax = uv[i*2+1] + bounds.append([[uMin, uMax], [vMin, vMax]]) + else: + return False + + for a in range(len(shells)): + for b in range(a): + # print "b",b + if bounds != []: + # print bounds + aL = bounds[a][0][0] + aR = bounds[a][0][1] + aT = bounds[a][1][1] + aB = bounds[a][1][0] + + bL = bounds[b][0][0] + bR = bounds[b][0][1] + bT = bounds[b][1][1] + bB = bounds[b][1][0] + + overlaps = True + if aT < bB: # A entirely below B + overlaps = False + + if aB > bT: # A entirely above B + overlaps = False + + if aR < bL: # A entirely right of B + overlaps = False + + if aL > bR: # A entirely left of B + overlaps = False + + if overlaps: + overlappers.extend(shells[a]) + overlappers.extend(shells[b]) + else: + return False + pass + + if overlappers: + return True + else: + return False + + @classmethod + def get_invalid(cls, instance): + invalid = [] + + for node in cmds.ls(instance, type='mesh'): + if cls._has_overlapping_uvs(node): + invalid.append(node) + + return invalid + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + raise RuntimeError("Meshes found with overlapping " + "UVs: {0}".format(invalid)) + pass From 08238f72f4a249805683d43e078edac538065a45 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Feb 2019 12:34:41 +0100 Subject: [PATCH 013/210] User can choose if want delete whole asset or only subsets --- pype/ftrack/actions/action_delete_asset.py | 170 +++++++++++++++------ 1 file changed, 127 insertions(+), 43 deletions(-) diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index 93dc1b3824..a0eb218a0c 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -1,22 +1,21 @@ import sys import logging -import random -import string +from bson.objectid import ObjectId import argparse import ftrack_api from pype.ftrack import BaseAction from avalon.tools.libraryloader.io_nonsingleton import DbConnector -class DeleteEntity(BaseAction): +class DeleteAsset(BaseAction): '''Edit meta data action.''' #: Action identifier. - identifier = 'delete.entity' + identifier = 'delete.asset' #: Action label. - label = 'Delete entity' + label = 'Delete asset/subsets' #: Action description. - description = 'Removes assets from Ftrack and Avalon db with all childs' + description = 'Removes from Avalon with all childs and asset from Ftrack' icon = "https://www.iconsdb.com/icons/preview/white/full-trash-xxl.png" #: Db db = DbConnector() @@ -44,19 +43,98 @@ class DeleteEntity(BaseAction): return discover + def _launch(self, event): + self.reset_session() + try: + self.db.install() + args = self._translate_event( + self.session, event + ) + + interface = self._interface( + self.session, *args + ) + + if interface: + return interface + + response = self.launch( + self.session, *args + ) + finally: + self.db.uninstall() + + return self._handle_result( + self.session, response, *args + ) + def interface(self, session, entities, event): if not event['data'].get('values', {}): - entity = entities[0] - title = 'Going to delete "{}"'.format(entity['name']) - items = [] - item = { + entity = entities[0] + title = 'Choose items to delete from "{}"'.format(entity['name']) + project = entity['project'] + + self.db.Session['AVALON_PROJECT'] = project["full_name"] + + av_entity = self.db.find_one({ + 'type': 'asset', + 'name': entity['name'] + }) + + asset_label = { + 'type': 'label', + 'value': '*Delete whole asset:*' + } + asset_item = { + 'label': av_entity['name'], + 'name': 'whole_asset', + 'type': 'boolean', + 'value': False + } + delete_item = { 'label': 'Enter "DELETE" to confirm', - 'name': 'key', + 'name': 'delete_key', 'type': 'text', 'value': '' } - items.append(item) + splitter = { + 'type': 'label', + 'value': '{}'.format(200*"-") + } + subset_label = { + 'type': 'label', + 'value': '*Subsets:*' + } + if av_entity is not None: + items.append(delete_item) + items.append(splitter) + items.append(asset_label) + items.append(asset_item) + items.append(splitter) + + all_subsets = self.db.find({ + 'type': 'subset', + 'parent': av_entity['_id'] + }) + + subset_items = [] + for subset in all_subsets: + item = { + 'label': subset['name'], + 'name': str(subset['_id']), + 'type': 'boolean', + 'value': False + } + subset_items.append(item) + if len(subset_items) > 0: + items.append(subset_label) + items.extend(subset_items) + else: + return { + 'success': False, + 'message': 'Didn\'t found assets in avalon' + } return { 'items': items, @@ -69,47 +147,54 @@ class DeleteEntity(BaseAction): values = event['data']['values'] if len(values) <= 0: - return { - 'success': True, - 'message': 'No Assets to delete!' - } - elif values.get('key', '').lower() != 'delete': + return + elif values.get('delete_key', '').lower() != 'delete': return { 'success': False, - 'message': 'Entered key does not match' + 'message': 'You didn\'t enter "DELETE" properly!' } + entity = entities[0] project = entity['project'] - self.db.install() self.db.Session['AVALON_PROJECT'] = project["full_name"] - av_entity = self.db.find_one({ - 'type': 'asset', - 'name': entity['name'] - }) + all_ids = [] + if values.get('whole_asset', False) is True: + av_entity = self.db.find_one({ + 'type': 'asset', + 'name': entity['name'] + }) - if av_entity is not None: - all_ids = [] - all_ids.append(av_entity['_id']) - all_ids.extend(self.find_child(av_entity)) + if av_entity is not None: + all_ids.append(av_entity['_id']) + all_ids.extend(self.find_child(av_entity)) - if len(all_ids) == 0: - self.db.uninstall() - return { - 'success': True, - 'message': 'None of assets' - } + session.delete(entity) + session.commit() + else: + for key, value in values.items(): + if key == 'delete_key' or value is False: + continue - or_subquery = [] - for id in all_ids: - or_subquery.append({'_id': id}) - delete_query = {'$or': or_subquery} - self.db.delete_many(delete_query) + entity_id = ObjectId(key) + av_entity = self.db.find_one({'_id': entity_id}) + if av_entity is None: + continue + all_ids.append(entity_id) + all_ids.extend(self.find_child(av_entity)) - session.delete(entity) - session.commit() - self.db.uninstall() + if len(all_ids) == 0: + return { + 'success': True, + 'message': 'No entities to delete in avalon' + } + + or_subquery = [] + for id in all_ids: + or_subquery.append({'_id': id}) + delete_query = {'$or': or_subquery} + self.db.delete_many(delete_query) return { 'success': True, @@ -148,8 +233,7 @@ def register(session, **kw): if not isinstance(session, ftrack_api.session.Session): return - action_handler = DeleteEntity(session) - action_handler.register() + DeleteAsset(session).register() def main(arguments=None): From 79395b3e1f94598dc036ed68bfc3d15bcb1931cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Feb 2019 12:46:51 +0100 Subject: [PATCH 014/210] Added helper text --- pype/ftrack/actions/action_delete_asset.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index a0eb218a0c..914225e807 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -96,7 +96,8 @@ class DeleteAsset(BaseAction): 'label': 'Enter "DELETE" to confirm', 'name': 'delete_key', 'type': 'text', - 'value': '' + 'value': '', + 'empty_text': 'Type *Delete* here...' } splitter = { 'type': 'label', From 6d4b9eaaf628694c2e5f141bce8c86e14786a383 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Feb 2019 13:14:59 +0100 Subject: [PATCH 015/210] Ftrack assets are also deleting when subsets are choose --- pype/ftrack/actions/action_delete_asset.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index 914225e807..4efc5d0d93 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -97,7 +97,7 @@ class DeleteAsset(BaseAction): 'name': 'delete_key', 'type': 'text', 'value': '', - 'empty_text': 'Type *Delete* here...' + 'empty_text': 'Type Delete here...' } splitter = { 'type': 'label', @@ -174,17 +174,24 @@ class DeleteAsset(BaseAction): session.delete(entity) session.commit() else: + subset_names = [] for key, value in values.items(): if key == 'delete_key' or value is False: continue entity_id = ObjectId(key) av_entity = self.db.find_one({'_id': entity_id}) + subset_names.append(av_entity['name']) if av_entity is None: continue all_ids.append(entity_id) all_ids.extend(self.find_child(av_entity)) + for ft_asset in entity['assets']: + if ft_asset['name'] in subset_names: + session.delete(ft_asset) + session.commit() + if len(all_ids) == 0: return { 'success': True, From fef49c20ef247a454c3a10b4303b6c01c23ca9bd Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 20 Feb 2019 13:48:28 +0100 Subject: [PATCH 016/210] remove locking of parent group --- pype/plugins/maya/load/load_ass.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pype/plugins/maya/load/load_ass.py b/pype/plugins/maya/load/load_ass.py index b27cd20b5b..13ad85473c 100644 --- a/pype/plugins/maya/load/load_ass.py +++ b/pype/plugins/maya/load/load_ass.py @@ -160,10 +160,6 @@ class AssStandinLoader(api.Loader): # Set the standin filepath standinShape.dso.set(self.fname) - - # Lock parenting of the transform and standin - cmds.lockNode([root, standin], lock=True) - nodes = [root, standin] self[:] = nodes From ed4b8f9bddae37d2f4d4e4760783adb8c3827c5d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 20 Feb 2019 14:48:42 +0100 Subject: [PATCH 017/210] add look manager to maya side menu --- pype/maya/customize.py | 14 ++++++++++++++ res/icons/Thumbs.db | Bin 0 -> 6144 bytes res/icons/lookmanager.png | Bin 0 -> 2408 bytes 3 files changed, 14 insertions(+) create mode 100644 res/icons/Thumbs.db create mode 100644 res/icons/lookmanager.png diff --git a/pype/maya/customize.py b/pype/maya/customize.py index 872942bfd9..61d7c283d2 100644 --- a/pype/maya/customize.py +++ b/pype/maya/customize.py @@ -78,6 +78,8 @@ def override_toolbox_ui(): import avalon.tools.cbsceneinventory as inventory import avalon.tools.cbloader as loader from avalon.maya.pipeline import launch_workfiles_app + import mayalookassigner + # Ensure the maya web icon on toolbox exists web_button = "ToolBox|MainToolboxLayout|mayaWebButton" @@ -98,6 +100,18 @@ def override_toolbox_ui(): background_color = (0.267, 0.267, 0.267) controls = [] + control = mc.iconTextButton( + "pype_toolbox_lookmanager", + annotation="Look Manager", + label="Look Manager", + image=os.path.join(icons, "lookmanager.png"), + command=lambda: mayalookassigner.show(), + bgc=background_color, + width=icon_size, + height=icon_size, + parent=parent) + controls.append(control) + control = mc.iconTextButton( "pype_toolbox_workfiles", annotation="Work Files", diff --git a/res/icons/Thumbs.db b/res/icons/Thumbs.db new file mode 100644 index 0000000000000000000000000000000000000000..fa56c871f60da645a236c2786a1fdfe9f5693dc2 GIT binary patch literal 6144 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 literal 0 HcmV?d00001 diff --git a/res/icons/lookmanager.png b/res/icons/lookmanager.png new file mode 100644 index 0000000000000000000000000000000000000000..9ed1d3db8ebaaed66efbf4949ecbd1ee9aed198b GIT binary patch literal 2408 zcmbVO3v3f*953TB*ckE%<1q)vO&(^u-lNxRd#qsB7S@f9v5heVVb{BBd!fC%-QBis zXpomU7$6KI3lYo=(TMUkAP9melO+fX27*Bmc^NMJVu#y!X)xAbqcGG4(dO7;hb{<}c=I8r zr0|g0NFgjvnjwqLh*KugX3KyG47Z|~6~#>mPSGSr69g3f!9Y#n{B(h9LR1Sp+2Nq3 z$ux>qR#qA-O-4xxpt#LuLoos+2m~MywMx{O2qLO!9SkmBWfeiz1WAN+M#d+VYjzk| z+U`PFj?s#0G)y31XoQhb+=%HewE;OchLg*cP@6c%qI`%C^P;8#EFQzkK}nO;p!5vt z*z_X;Ahcd@OyjAxgu^ijRdZJWH=+S~Dq1b7l6ka%SEX`=<=qv)%rxB_nRY5Xqe)7U zB!xN>mEU19|2_h#|s;_oh!@C%bx5N58f)NOT72zh@OoH5{V>pdr?NG18 z3I3`lp#Vn$^dnGEH5{Wc{|V+;+Ak?#2KX$5nE;Qk))kcNK$|>u+G%^y~pXy zS0uj>0uyS%#4O0;c2c;Fq7cG}x0&np(jHOO7?I^YE;|h3V-y4qn2MQwypKRE3}!-j zoU|c6&gw@fD@$T5YauDKuVcMSV$1a^(AT#&1She8v0aE78NoWm`A2is&t06ocsI&iBPQHj9?^5A`C^a2xBG8ti^&+ z4CU7=|DkdRiv#$lf~2d_&dOo}(NRbk*Fo=e5LOJcnJ2g$B#yhOEOLUwZ84i24hpko z;m%A{Zy~S%Y9FLrIQCTqcC81q1UATs0UjJLD6IR_ zRz>Y4fIeOwZF8MA?G89N3G`v?2m%kWLyQ*zwE~W)fmbf|H5lR`kIPY1a_HizH-ame z!SOfFQpN9nnS7BAu5QTAX_{Y%)C}=d5Zt za)iozuj|aUs7*c;=k*SCJP`SSgC{QH>Mpki$U$j|Ia8LTcOSK6m-T(~{IY(h)UpE!=ZK%T_jU~I zQ#x_MpNVHuw)d&+dqVD=G9Wa!c~M|`zfTW(KF`}ZuQsJB{Tzm;1oGDfAM_e@eAVLQ zHI4H(rM`14ZRW9@+LNV+uxn$>y3O7WW5WmZ56^905dZPW#>w%n(YbNqJN2cvyX?J* zB*qnP?Oj)vl(cQ%-Qx?Z&rBAx|0;z~E;*2KSiM%;bbDC*6#R!z5*wQbH(7jf=h=P{lXE&X^)PL=gX||TVJ-p-IcJE%gZ$w7w+7;r?m!^ga zdhI;C@+zKqHQ|mo>F9!=dQohfA_MMlKC5J=$mGI zWL?jl7t%lIe&dzgxGzQwJ#_HyPbb!AeKfjH@!STIe5L?j>HSuQ+qPxp9R4E#z_M Kxi)8&F8CKzT}2}R literal 0 HcmV?d00001 From cddb68414648c31d56579dda4d714b172cfa1e8a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Feb 2019 15:15:52 +0100 Subject: [PATCH 018/210] delete confirmation is after choose --- pype/ftrack/actions/action_delete_asset.py | 109 ++++++++++++++++++--- 1 file changed, 94 insertions(+), 15 deletions(-) diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index 4efc5d0d93..e415774f23 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -20,6 +20,8 @@ class DeleteAsset(BaseAction): #: Db db = DbConnector() + value = None + def discover(self, session, entities, event): ''' Validation ''' selection = event["data"].get("selection", None) @@ -55,9 +57,16 @@ class DeleteAsset(BaseAction): self.session, *args ) + confirmation = self.confirm_delete( + True, *args + ) + if interface: return interface + if confirmation: + return confirmation + response = self.launch( self.session, *args ) @@ -70,6 +79,7 @@ class DeleteAsset(BaseAction): def interface(self, session, entities, event): if not event['data'].get('values', {}): + self.attempt = 1 items = [] entity = entities[0] title = 'Choose items to delete from "{}"'.format(entity['name']) @@ -84,7 +94,7 @@ class DeleteAsset(BaseAction): asset_label = { 'type': 'label', - 'value': '*Delete whole asset:*' + 'value': '## Delete whole asset: ##' } asset_item = { 'label': av_entity['name'], @@ -92,24 +102,15 @@ class DeleteAsset(BaseAction): 'type': 'boolean', 'value': False } - delete_item = { - 'label': 'Enter "DELETE" to confirm', - 'name': 'delete_key', - 'type': 'text', - 'value': '', - 'empty_text': 'Type Delete here...' - } splitter = { 'type': 'label', 'value': '{}'.format(200*"-") } subset_label = { 'type': 'label', - 'value': '*Subsets:*' + 'value': '## Subsets: ##' } if av_entity is not None: - items.append(delete_item) - items.append(splitter) items.append(asset_label) items.append(asset_item) items.append(splitter) @@ -142,6 +143,69 @@ class DeleteAsset(BaseAction): 'title': title } + def confirm_delete(self, first_attempt, entities, event): + if first_attempt is True: + if 'values' not in event['data']: + return + + values = event['data']['values'] + + if len(values) <= 0: + return + if 'whole_asset' not in values: + return + else: + values = self.values + + title = 'Confirmation of deleting {}' + if values['whole_asset'] is True: + title = title.format( + 'whole asset {}'.format( + entities[0]['name'] + ) + ) + else: + subsets = [] + for key, value in values.items(): + if value is True: + subsets.append(key) + len_subsets = len(subsets) + if len_subsets == 0: + return { + 'success': True, + 'message': 'Nothing was selected to delete' + } + elif len_subsets == 1: + title = title.format( + '{} subset'.format(len_subsets) + ) + else: + title = title.format( + '{} subset'.format(len_subsets) + ) + + self.values = values + items = [] + + delete_label = { + 'type': 'label', + 'value': 'Please enter "DELETE" to confirm' + } + + delete_item = { + 'name': 'delete_key', + 'type': 'text', + 'value': '', + 'empty_text': 'Type Delete here...' + } + items.append(delete_label) + items.append(delete_item) + + return { + 'items': items, + 'title': title + } + def launch(self, session, entities, event): if 'values' not in event['data']: return @@ -149,10 +213,25 @@ class DeleteAsset(BaseAction): values = event['data']['values'] if len(values) <= 0: return - elif values.get('delete_key', '').lower() != 'delete': + if 'delete_key' not in values: + return + + if values['delete_key'].lower() != 'delete': + if values['delete_key'].lower() == '': + return { + 'success': False, + 'message': 'Deleting cancelled' + } + if self.attempt < 3: + self.attempt += 1 + return_dict = self.confirm_delete(False, entities, event) + return_dict['title'] = '{} ({} attempt)'.format( + return_dict['title'], self.attempt + ) + return return_dict return { 'success': False, - 'message': 'You didn\'t enter "DELETE" properly!' + 'message': 'You didn\'t enter "DELETE" properly 3 times!' } entity = entities[0] @@ -161,7 +240,7 @@ class DeleteAsset(BaseAction): self.db.Session['AVALON_PROJECT'] = project["full_name"] all_ids = [] - if values.get('whole_asset', False) is True: + if self.values.get('whole_asset', False) is True: av_entity = self.db.find_one({ 'type': 'asset', 'name': entity['name'] @@ -175,7 +254,7 @@ class DeleteAsset(BaseAction): session.commit() else: subset_names = [] - for key, value in values.items(): + for key, value in self.values.items(): if key == 'delete_key' or value is False: continue From d68eb1655b7f8db6e291e0a51b9c2967db213a59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Feb 2019 15:17:22 +0100 Subject: [PATCH 019/210] added formatting --- pype/ftrack/actions/action_delete_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index e415774f23..7ad2655de1 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -189,7 +189,7 @@ class DeleteAsset(BaseAction): delete_label = { 'type': 'label', - 'value': 'Please enter "DELETE" to confirm' + 'value': '# Please enter "DELETE" to confirm #' } delete_item = { From 528c16558b6161d0edce85629f14ab7c4d0b79f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 20 Feb 2019 16:39:47 +0100 Subject: [PATCH 020/210] added tool (copy of project manager) --- pype/tools/assetcreator/__init__.py | 10 + pype/tools/assetcreator/__main__.py | 5 + pype/tools/assetcreator/app.py | 196 +++++++++ pype/tools/assetcreator/dialogs.py | 196 +++++++++ pype/tools/assetcreator/lib.py | 66 +++ pype/tools/assetcreator/model.py | 354 ++++++++++++++++ pype/tools/assetcreator/widget.py | 629 ++++++++++++++++++++++++++++ 7 files changed, 1456 insertions(+) create mode 100644 pype/tools/assetcreator/__init__.py create mode 100644 pype/tools/assetcreator/__main__.py create mode 100644 pype/tools/assetcreator/app.py create mode 100644 pype/tools/assetcreator/dialogs.py create mode 100644 pype/tools/assetcreator/lib.py create mode 100644 pype/tools/assetcreator/model.py create mode 100644 pype/tools/assetcreator/widget.py diff --git a/pype/tools/assetcreator/__init__.py b/pype/tools/assetcreator/__init__.py new file mode 100644 index 0000000000..3b88ebe984 --- /dev/null +++ b/pype/tools/assetcreator/__init__.py @@ -0,0 +1,10 @@ + +from .app import ( + show, + cli +) + +__all__ = [ + "show", + "cli", +] diff --git a/pype/tools/assetcreator/__main__.py b/pype/tools/assetcreator/__main__.py new file mode 100644 index 0000000000..d77bc585c5 --- /dev/null +++ b/pype/tools/assetcreator/__main__.py @@ -0,0 +1,5 @@ +from . import cli + +if __name__ == '__main__': + import sys + sys.exit(cli(sys.argv[1:])) diff --git a/pype/tools/assetcreator/app.py b/pype/tools/assetcreator/app.py new file mode 100644 index 0000000000..2797b1164e --- /dev/null +++ b/pype/tools/assetcreator/app.py @@ -0,0 +1,196 @@ +import sys + +from avalon.vendor.Qt import QtWidgets, QtCore +from avalon import io, api, style +from avalon.tools import lib as parentlib +from . import widget + +module = sys.modules[__name__] +module.window = None + + +class Window(QtWidgets.QDialog): + """Asset creator interface + + """ + + def __init__(self, parent=None): + super(Window, self).__init__(parent) + project_name = io.active_project() + self.setWindowTitle("Asset creator ({0})".format(project_name)) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + # assets + assets_widgets = QtWidgets.QWidget() + assets_widgets.setContentsMargins(0, 0, 0, 0) + assets_layout = QtWidgets.QVBoxLayout(assets_widgets) + assets = widget.AssetWidget() + assets.view.setSelectionMode(assets.view.ExtendedSelection) + assets_layout.addWidget(assets) + + # info + label_name = QtWidgets.QLabel("Name:") + input_name = QtWidgets.QLineEdit() + input_name.setPlaceholderText("") + + # Parent + label_parent = QtWidgets.QLabel("Parent:") + input_parent = QtWidgets.QLineEdit() + input_parent.setReadOnly(True) + input_parent.setStyleSheet("background-color: #333333;") # greyed out + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(label_name) + layout.addWidget(input_name) + layout.addWidget(label_parent) + layout.addWidget(input_parent) + + body = QtWidgets.QSplitter() + body.setContentsMargins(0, 0, 0, 0) + body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + body.setOrientation(QtCore.Qt.Horizontal) + body.addWidget(assets_widgets) + body.addWidget(layout) + body.setStretchFactor(0, 100) + body.setStretchFactor(1, 65) + + # statusbar + message = QtWidgets.QLabel() + message.setFixedHeight(20) + + statusbar = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(statusbar) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(message) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + layout.addWidget(statusbar) + + self.data = { + "label": { + "message": message, + }, + "model": { + "assets": assets + }, + "buttons": { + + } + } + + # signals + assets.selection_changed.connect(self.on_asset_changed) + assets.silo_changed.connect(self.on_silo_changed) + + self.resize(800, 500) + + self.echo("Connected to project: {0}".format(project_name)) + + def refresh(self): + self.data["model"]["assets"].refresh() + # set silo on start so tasks for silo are shown + current_silo = self.data["model"]["assets"].get_current_silo() + if current_silo != "": + self.on_asset_changed() + + def echo(self, message): + widget = self.data["label"]["message"] + widget.setText(str(message)) + + QtCore.QTimer.singleShot(5000, lambda: widget.setText("")) + + print(message) + + def on_add_asset(self): + """Show add asset dialog""" + + # Get parent asset (active index in selection) + model = self.data["model"]["assets"] + parent_id = model.get_active_asset() + + # Get active silo + silo = model.get_current_silo() + if not silo: + QtWidgets.QMessageBox.critical(self, "Missing silo", + "Please create a silo first.\n" + "Use the + tab at the top left.") + return + + def _on_current_asset_changed(): + """Callback on current asset changed in item widget. + + Whenever the current index changes in the item widget we want to + update under which asset we're creating *to be created* asset. + + """ + + parent = model.get_active_asset() + + # Signals + model.current_changed.connect(_on_current_asset_changed) + + def on_asset_changed(self): + """Callback on asset selection changed + + This updates the task view. + + """ + + model = self.data["model"]["assets"] + selected = model.get_selected_assets() + # Show task of silo if nothing selected + if len(selected) < 1: + silo = model.get_silo_object() + if silo: + selected = [silo['_id']] + self.data['model']['tasks'].set_assets(selected) + + def on_silo_changed(self, silo): + """Callback on asset silo changed""" + if silo: + self.echo("Silo changed to: {0}".format(silo)) + + +def show(root=None, debug=False, parent=None): + """Display Loader GUI + + Arguments: + debug (bool, optional): Run loader in debug-mode, + defaults to False + + """ + + try: + module.window.close() + del module.window + except (RuntimeError, AttributeError): + pass + + if debug is True: + io.install() + + with parentlib.application(): + window = Window(parent) + window.setStyleSheet(style.load_stylesheet()) + window.show() + window.refresh() + + module.window = window + + +def cli(args): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("project") + + args = parser.parse_args(args) + project = args.project + + io.install() + + api.Session["AVALON_PROJECT"] = project + + show() diff --git a/pype/tools/assetcreator/dialogs.py b/pype/tools/assetcreator/dialogs.py new file mode 100644 index 0000000000..0dd119fc12 --- /dev/null +++ b/pype/tools/assetcreator/dialogs.py @@ -0,0 +1,196 @@ +from ... import io +from ...vendor import qtawesome as qta +from ...vendor.Qt import QtWidgets, QtCore +from . import lib + + +class TasksCreateDialog(QtWidgets.QDialog): + """A Dialog to choose a list of Tasks to create.""" + + def __init__(self, parent=None): + super(TasksCreateDialog, self).__init__(parent=parent) + self.setWindowTitle("Add Tasks") + self.setModal(True) + + layout = QtWidgets.QVBoxLayout(self) + + label = QtWidgets.QLabel("Select Tasks to create") + + tasks = lib.list_project_tasks() + tasks = list(sorted(tasks)) # sort for readability + list_widget = QtWidgets.QListWidget() + list_widget.addItems(tasks) + list_widget.setSelectionMode(list_widget.ExtendedSelection) + + footer = QtWidgets.QHBoxLayout() + cancel = QtWidgets.QPushButton("Cancel") + create = QtWidgets.QPushButton(qta.icon("fa.plus", color="grey"), + "Create") + footer.addWidget(create) + footer.addWidget(cancel) + + layout.addWidget(label) + layout.addWidget(list_widget) + layout.addLayout(footer) + + cancel.clicked.connect(self.reject) + cancel.setAutoDefault(False) + + create.clicked.connect(self.accept) + create.setAutoDefault(True) + + self.list = list_widget + + def get_selected(self): + """Return all selected task names + + Returns: + list: Selected task names. + + """ + return [i.text() for i in self.list.selectedItems()] + + +class AssetCreateDialog(QtWidgets.QDialog): + """A Dialog to create a new asset.""" + + asset_created = QtCore.Signal(dict) + + def __init__(self, parent=None): + super(AssetCreateDialog, self).__init__(parent=parent) + self.setWindowTitle("Add asset") + + self.parent_id = None + self.parent_name = "" + self.silo = "" + + # Label + label_label = QtWidgets.QLabel("Label:") + label = QtWidgets.QLineEdit() + label.setPlaceholderText("