From cb5b4ab9485a32f3346fa6236897e82f76a51bb2 Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Wed, 10 Jun 2020 20:02:12 +0200 Subject: [PATCH 001/158] Initial commit of adding 'files' section to representation - WIP All representation integrated in DB should contain all attached resources. This will be used in future synchronization implementation. --- pype/plugins/global/publish/integrate_new.py | 139 ++++++++++++++++++- 1 file changed, 134 insertions(+), 5 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index bd908901cc..60809b864f 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -91,18 +91,28 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): default_template_name = "publish" template_name_profiles = None + integrated_file_sizes = {} # file_url : file_size + def process(self, instance): if [ef for ef in self.exclude_families if instance.data["family"] in ef]: return - self.register(instance) + self.log.info("IntegrateAssetNew.process:") + import json + self.log.info("instance: {}".format(json.dumps(instance.__dict__, default=str))) - self.log.info("Integrating Asset in to the database ...") - self.log.info("instance.data: {}".format(instance.data)) - if instance.data.get('transfer', True): - self.integrate(instance) + try: + self.register(instance) + self.log.info("Integrated Asset in to the database ...") + self.log.info("instance.data: {}".format(instance.data)) + except Exception: + # clean destination + # !TODO fix exceptions.WindowsError + e = sys.exc_info()[0] + self.log.critical("Error when registering {}".format(e)) + self.clean_destination_files(self.integrated_file_sizes) def register(self, instance): # Required environment variables @@ -472,6 +482,18 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_padding_exp % int(repre.get("frameStart")) ) + if instance.data.get('transfer', True): + # could throw exception, will be caught in 'process' + # all integration to DB is being done together lower, so no rollback needed + self.log.info("Integrating source files to destination ...") + self.integrated_file_sizes = self.integrate(instance) + self.log.debug("Integrated files {}".format(self.integrated_file_sizes)) + #TODO instance.data["transfers"].remove([src, dst]) # array needs to be changed to tuple + + # get 'files' information for representation and all attached resources + self.log.debug("Preparing files information ..") + representation["files"] = self.get_files_info(dst, instance, self.integrated_file_sizes) + self.log.debug("__ representation: {}".format(representation)) destination_list.append(dst) self.log.debug("__ destination_list: {}".format(destination_list)) @@ -509,7 +531,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Args: instance: the instance to integrate + Returns: + integrated_file_sizes: dictionary of destination file url and its size in bytes """ + integrated_file_sizes = {} # store destination url and size for reporting and rollback transfers = instance.data.get("transfers", list()) for src, dest in transfers: @@ -519,6 +544,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): transfers = instance.data.get("transfers", list()) for src, dest in transfers: self.copy_file(src, dest) + if os.path.exists(dest): + # TODO needs to be updated during site implementation + integrated_file_sizes[dest] = os.path.getsize(dest) + # Produce hardlinked copies # Note: hardlink can only be produced between two files on the same @@ -529,6 +558,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): for src, dest in hardlinks: self.log.debug("Hardlinking file .. {} -> {}".format(src, dest)) self.hardlink_file(src, dest) + if os.path.exists(dest): + # TODO needs to be updated during site implementation + integrated_file_sizes[dest] = os.path.getsize(dest) + + return integrated_file_sizes def copy_file(self, src, dst): """ Copy given source to destination @@ -764,3 +798,98 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ).format(family, task_name, template_name)) return template_name + + def get_files_info(self, dst, instance, integrated_file_sizes): + """ Prepare 'files' portion of representation to store all attached files (representatio, textures, json etc.) + This information is used in synchronization of mentioned files + + Args: + dst: destination path for representation + instance: the instance to integrate + Returns: + files: list of dictionaries with representation and all resources + """ + self.log.debug("get_files_info:") + # add asset and all resources to 'files' + files = [] + # file info for representation - should be always + rec = self.prepare_file_info(dst, integrated_file_sizes[dst], 'temphash') # TODO + files.append(rec) + + # file info for resources + resource_files = self.get_resource_files_info(instance, integrated_file_sizes) + if resource_files: # do not append empty list + files.extend(resource_files) + + return files + + def get_resource_files_info(self, instance, integrated_file_sizes): + """ Prepare 'files' portion for attached resources, + could be empty if no resources (like textures) present + + Arguments: + instance: the current instance being published + Returns: + output_resources: array of dictionaries to be added to 'files' key in representation + """ + # TODO check if sourceHashes is viable or transfers (they are updated during loop though) + resources = instance.data.get("sourceHashes", {}) + self.log.debug("get_resource_files_info: {}".format(resources)) + output_resources = [] + for resource_info, resource_path in resources.items(): + # TODO - hash or use self.integrated_file_size + file_name,file_time,file_size,file_args = resource_info.split("|") + output_resources.append(self.prepare_file_info(resource_path, file_size, 'temphash')) + + return output_resources + + def prepare_file_info(self, path, size = None, hash = None, sites = None): + """ Prepare information for one file (asset or resource) + + Arguments: + path: destination url of published file + size(optional): size of file in bytes + hash(optional): hash of file for synchronization validation + sites(optional): array of published locations, ['studio'] by default, + expected ['studio', 'site1', 'gdrive1'] + Returns: + rec: dictionary with filled info + """ + + rec = { # TODO update collection step to extend with necessary values + "_id": io.ObjectId(), + "path": path + } + if size: + rec["size"] = size + + if hash: + rec["hash"] = hash + + if sites: + rec["sites"] = sites + else: + rec["sites"] = ["studio"] + + self.log.debug("prepare_file_info: {}".format(rec)) + + return rec + + def clean_destination_files(self, integrated_file_sizes): + """ Clean destination files + Called when error happened during integrating to DB or to disk + Used to clean unwanted files + + Arguments: + integrated_file_sizes: disctionary of uploaded files, file urls as keys + """ + if integrated_file_sizes: + for file_url, file_size in integrated_file_sizes.items(): + try: + self.log.debug("Removing file...{}".format(file_url)) + os.remove(file_url) # needs to be changed to Factory when sites implemented + except FileNotFoundError: + pass # file not there, nothing to delete + except OSError as e: + self.log.critical("Cannot remove file {}".format(file_url)) + self.log.critical(e) \ No newline at end of file From f1ea60f98312627af0bdc9d24a938d0c74315c94 Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Thu, 11 Jun 2020 09:43:57 +0200 Subject: [PATCH 002/158] Fix - removed double copy of already copied file Fix - remove double creation of hardlink resulting in WindowError --- pype/plugins/global/publish/integrate_new.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 60809b864f..740de4d930 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -544,9 +544,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): transfers = instance.data.get("transfers", list()) for src, dest in transfers: self.copy_file(src, dest) - if os.path.exists(dest): - # TODO needs to be updated during site implementation - integrated_file_sizes[dest] = os.path.getsize(dest) + # TODO needs to be updated during site implementation + integrated_file_sizes[dest] = os.path.getsize(dest) + # already copied, delete from transfers to limit double copy TODO double check + instance.data.get("transfers", list()).remove([src, dest]) # Produce hardlinked copies @@ -557,10 +558,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): hardlinks = instance.data.get("hardlinks", list()) for src, dest in hardlinks: self.log.debug("Hardlinking file .. {} -> {}".format(src, dest)) - self.hardlink_file(src, dest) - if os.path.exists(dest): - # TODO needs to be updated during site implementation - integrated_file_sizes[dest] = os.path.getsize(dest) + if not os.path.exists(dest): + self.hardlink_file(src, dest) + + # TODO needs to be updated during site implementation + integrated_file_sizes[dest] = os.path.getsize(dest) return integrated_file_sizes From 91c3f479239779da46cae5349b06b2408df83efd Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Thu, 11 Jun 2020 18:32:36 +0200 Subject: [PATCH 003/158] Added creation of temporary files in destination first Fix - integrated_file_sizes got carried over Fix - create orig_transfers to fall back to original transfers Fix - updated get_files_info logic - using transfers and hardlinks Fix - added rootless_path into representation Fix - updated file handling if errors --- pype/plugins/global/publish/integrate_new.py | 182 +++++++++++-------- 1 file changed, 110 insertions(+), 72 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 740de4d930..3f906e3455 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -91,28 +91,30 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): default_template_name = "publish" template_name_profiles = None - integrated_file_sizes = {} # file_url : file_size + integrated_file_sizes = {} # file_url : file_size of all published and uploaded files + + TMP_FILE_EXT = 'tmp' # suffix to denote temporary files, use without '.' def process(self, instance): - + self.integrated_file_sizes = {} if [ef for ef in self.exclude_families if instance.data["family"] in ef]: return self.log.info("IntegrateAssetNew.process:") import json - self.log.info("instance: {}".format(json.dumps(instance.__dict__, default=str))) + self.log.debug("instance: {}".format(json.dumps(instance.__dict__, default=str))) try: self.register(instance) self.log.info("Integrated Asset in to the database ...") self.log.info("instance.data: {}".format(instance.data)) - except Exception: + self.handle_destination_files(self.integrated_file_sizes, instance, 'finalize') + except Exception as e: # clean destination - # !TODO fix exceptions.WindowsError - e = sys.exc_info()[0] - self.log.critical("Error when registering {}".format(e)) - self.clean_destination_files(self.integrated_file_sizes) + self.log.critical("Error when registering", exc_info=True) + self.handle_destination_files(self.integrated_file_sizes, instance, 'remove') + raise def register(self, instance): # Required environment variables @@ -273,13 +275,20 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): representations = [] destination_list = [] + orig_transfers = [] if 'transfers' not in instance.data: instance.data['transfers'] = [] + else: + orig_transfers = list(instance.data['transfers']) template_name = self.template_name_from_instance(instance) published_representations = {} for idx, repre in enumerate(instance.data["representations"]): + # reset transfers for next representation + # instance.data['transfers'] is used as a global variable in current codebase + instance.data['transfers'] = list(orig_transfers) + published_files = [] # create template data for Anatomy @@ -482,17 +491,22 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_padding_exp % int(repre.get("frameStart")) ) - if instance.data.get('transfer', True): + # any file that should be physically copied is expected in 'transfers' or 'hardlinks' + # both have same interface [[source_url, destination_url], [source_url...]] + if instance.data.get('transfers', False) or instance.data.get('hardlinks', False): # could throw exception, will be caught in 'process' # all integration to DB is being done together lower, so no rollback needed - self.log.info("Integrating source files to destination ...") - self.integrated_file_sizes = self.integrate(instance) + self.log.debug("Integrating source files to destination ...") + self.integrated_file_sizes.update(self.integrate(instance)) self.log.debug("Integrated files {}".format(self.integrated_file_sizes)) - #TODO instance.data["transfers"].remove([src, dst]) # array needs to be changed to tuple + + import random + if random.choice([True, False, True, True]): + raise Exception("Monkey attack!!!") # get 'files' information for representation and all attached resources self.log.debug("Preparing files information ..") - representation["files"] = self.get_files_info(dst, instance, self.integrated_file_sizes) + representation["files"] = self.get_files_info(instance, self.integrated_file_sizes) self.log.debug("__ representation: {}".format(representation)) destination_list.append(dst) @@ -535,20 +549,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): integrated_file_sizes: dictionary of destination file url and its size in bytes """ integrated_file_sizes = {} # store destination url and size for reporting and rollback - transfers = instance.data.get("transfers", list()) - + transfers = list(instance.data.get("transfers", list())) for src, dest in transfers: if os.path.normpath(src) != os.path.normpath(dest): + dest = self.get_dest_temp_url(dest) self.copy_file(src, dest) - - transfers = instance.data.get("transfers", list()) - for src, dest in transfers: - self.copy_file(src, dest) - # TODO needs to be updated during site implementation - integrated_file_sizes[dest] = os.path.getsize(dest) - # already copied, delete from transfers to limit double copy TODO double check - instance.data.get("transfers", list()).remove([src, dest]) - + # TODO needs to be updated during site implementation + integrated_file_sizes[dest] = os.path.getsize(dest) # Produce hardlinked copies # Note: hardlink can only be produced between two files on the same @@ -557,6 +564,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # to ensure publishes remain safe and non-edited. hardlinks = instance.data.get("hardlinks", list()) for src, dest in hardlinks: + dest = self.get_dest_temp_url(dest) self.log.debug("Hardlinking file .. {} -> {}".format(src, dest)) if not os.path.exists(dest): self.hardlink_file(src, dest) @@ -692,16 +700,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: source = context.data["currentFile"] anatomy = instance.context.data["anatomy"] - success, rootless_path = ( - anatomy.find_root_template_from_path(source) - ) - if success: - source = rootless_path - else: - self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(source)) + source = self.get_rootless_path(anatomy, source) self.log.debug("Source: {}".format(source)) version_data = { @@ -801,55 +800,75 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return template_name - def get_files_info(self, dst, instance, integrated_file_sizes): - """ Prepare 'files' portion of representation to store all attached files (representatio, textures, json etc.) - This information is used in synchronization of mentioned files - Args: - dst: destination path for representation - instance: the instance to integrate - Returns: - files: list of dictionaries with representation and all resources + def get_rootless_path(self, anatomy, path): + """ Returns, if possible, path without absolute portion from host (eg. 'c:\' or '/opt/..') + This information is host dependent and shouldn't be captured. + Example: + 'c:/projects/MyProject1/Assets/publish...' > '{root}/MyProject1/Assets...' + + Args: + anatomy: anatomy part from instance + path: path (absolute) + Returns: + path: modified path if possible, or unmodified path + warning logged """ - self.log.debug("get_files_info:") - # add asset and all resources to 'files' - files = [] - # file info for representation - should be always - rec = self.prepare_file_info(dst, integrated_file_sizes[dst], 'temphash') # TODO - files.append(rec) + success, rootless_path = ( + anatomy.find_root_template_from_path(path) + ) + if success: + path = rootless_path + else: + self.log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(path)) + return path - # file info for resources - resource_files = self.get_resource_files_info(instance, integrated_file_sizes) - if resource_files: # do not append empty list - files.extend(resource_files) - - return files - - def get_resource_files_info(self, instance, integrated_file_sizes): - """ Prepare 'files' portion for attached resources, - could be empty if no resources (like textures) present + def get_files_info(self, instance, integrated_file_sizes): + """ Prepare 'files' portion for attached resources and main asset. + Combining records from 'transfers' and 'hardlinks' parts from instance. + All attached resources should be added, currently without Context info. Arguments: instance: the current instance being published + integrated_file_sizes: dictionary of destination path (absolute) and its file size Returns: output_resources: array of dictionaries to be added to 'files' key in representation """ - # TODO check if sourceHashes is viable or transfers (they are updated during loop though) - resources = instance.data.get("sourceHashes", {}) - self.log.debug("get_resource_files_info: {}".format(resources)) + resources = list(instance.data.get("transfers", [])) + resources.extend(list(instance.data.get("hardlinks", []))) + + self.log.debug("get_resource_files_info.resources: {}".format(resources)) + output_resources = [] - for resource_info, resource_path in resources.items(): + anatomy = instance.context.data["anatomy"] + for src, dest in resources: # TODO - hash or use self.integrated_file_size - file_name,file_time,file_size,file_args = resource_info.split("|") - output_resources.append(self.prepare_file_info(resource_path, file_size, 'temphash')) + path = self.get_rootless_path(anatomy, dest) + dest = self.get_dest_temp_url(dest) + output_resources.append(self.prepare_file_info(path, integrated_file_sizes[dest], 'temphash')) return output_resources + def get_dest_temp_url(self, dest): + """ Enhance destination path with TMP_FILE_EXT to denote temporary file. + Temporary files will be renamed after successful registration into DB and full copy to destination + + Arguments: + dest: destination url of published file (absolute) + Returns: + dest: destination path + '.TMP_FILE_EXT' + """ + if self.TMP_FILE_EXT and '.{}'.format(self.TMP_FILE_EXT) not in dest: + dest += '.{}'.format(self.TMP_FILE_EXT) + return dest + def prepare_file_info(self, path, size = None, hash = None, sites = None): """ Prepare information for one file (asset or resource) Arguments: - path: destination url of published file + path: destination url of published file (rootless) size(optional): size of file in bytes hash(optional): hash of file for synchronization validation sites(optional): array of published locations, ['studio'] by default, @@ -873,25 +892,44 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: rec["sites"] = ["studio"] - self.log.debug("prepare_file_info: {}".format(rec)) - return rec - def clean_destination_files(self, integrated_file_sizes): + def handle_destination_files(self, integrated_file_sizes, instance, mode): """ Clean destination files Called when error happened during integrating to DB or to disk Used to clean unwanted files Arguments: integrated_file_sizes: disctionary of uploaded files, file urls as keys + instance: processed instance - for publish directories + mode: 'remove' - clean files,'finalize' - rename files, remove TMP_FILE_EXT suffix denoting temp file """ if integrated_file_sizes: for file_url, file_size in integrated_file_sizes.items(): try: - self.log.debug("Removing file...{}".format(file_url)) - os.remove(file_url) # needs to be changed to Factory when sites implemented + if mode == 'remove': + self.log.debug("Removing file...{}".format(file_url)) + os.remove(file_url) # needs to be changed to Factory when sites implemented + if mode == 'finalize': + self.log.debug("Renaming file...{}".format(file_url)) + import re + os.rename(file_url, re.sub('\.{}$'.format(self.TMP_FILE_EXT), '', file_url)) # needs to be changed to Factory when sites implemented + except FileNotFoundError: pass # file not there, nothing to delete except OSError as e: - self.log.critical("Cannot remove file {}".format(file_url)) - self.log.critical(e) \ No newline at end of file + self.log.critical("Cannot {} file {}".format(mode, file_url), exc_info=True) + raise + + if mode == 'remove': + try: + publishDir = instance.data.get('publishDir', '') + resourcesDir = instance.data.get('resourcesDir', '') + if resourcesDir: + os.remove(resourcesDir) + if publishDir: + os.remove(publishDir) + except OSError as e: + self.log.critical("Cannot remove destination directory {} or {}".format(publishDir, resourcesDir), exc_info=True) + raise + From 417358dc7fff007eec1b14efdd21ded9a7053dc4 Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Thu, 11 Jun 2020 18:34:25 +0200 Subject: [PATCH 004/158] Housekeeping - removed parts for testing and debugging Removed delete of publishDir and resourceDir, probably better to keep there empty then deleting --- pype/plugins/global/publish/integrate_new.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 3f906e3455..e3e71f2dc1 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -101,10 +101,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if instance.data["family"] in ef]: return - self.log.info("IntegrateAssetNew.process:") - import json - self.log.debug("instance: {}".format(json.dumps(instance.__dict__, default=str))) - try: self.register(instance) self.log.info("Integrated Asset in to the database ...") @@ -500,10 +496,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.integrated_file_sizes.update(self.integrate(instance)) self.log.debug("Integrated files {}".format(self.integrated_file_sizes)) - import random - if random.choice([True, False, True, True]): - raise Exception("Monkey attack!!!") - # get 'files' information for representation and all attached resources self.log.debug("Preparing files information ..") representation["files"] = self.get_files_info(instance, self.integrated_file_sizes) @@ -921,15 +913,3 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.critical("Cannot {} file {}".format(mode, file_url), exc_info=True) raise - if mode == 'remove': - try: - publishDir = instance.data.get('publishDir', '') - resourcesDir = instance.data.get('resourcesDir', '') - if resourcesDir: - os.remove(resourcesDir) - if publishDir: - os.remove(publishDir) - except OSError as e: - self.log.critical("Cannot remove destination directory {} or {}".format(publishDir, resourcesDir), exc_info=True) - raise - From 2e811e448fc1265c0ba1793550dc9cba592e3cfc Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Thu, 11 Jun 2020 18:58:45 +0200 Subject: [PATCH 005/158] Taming of Hound --- pype/plugins/global/publish/integrate_new.py | 86 +++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index e3e71f2dc1..0fa12f965d 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -91,7 +91,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): default_template_name = "publish" template_name_profiles = None - integrated_file_sizes = {} # file_url : file_size of all published and uploaded files + # file_url : file_size of all published and uploaded files + integrated_file_sizes = {} TMP_FILE_EXT = 'tmp' # suffix to denote temporary files, use without '.' @@ -105,11 +106,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.register(instance) self.log.info("Integrated Asset in to the database ...") self.log.info("instance.data: {}".format(instance.data)) - self.handle_destination_files(self.integrated_file_sizes, instance, 'finalize') + self.handle_destination_files(self.integrated_file_sizes, + instance, 'finalize') except Exception as e: # clean destination self.log.critical("Error when registering", exc_info=True) - self.handle_destination_files(self.integrated_file_sizes, instance, 'remove') + self.handle_destination_files(self.integrated_file_sizes, + instance, 'remove') raise def register(self, instance): @@ -282,7 +285,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): published_representations = {} for idx, repre in enumerate(instance.data["representations"]): # reset transfers for next representation - # instance.data['transfers'] is used as a global variable in current codebase + # instance.data['transfers'] is used as a global variable + # in current codebase instance.data['transfers'] = list(orig_transfers) published_files = [] @@ -487,16 +491,18 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_padding_exp % int(repre.get("frameStart")) ) - # any file that should be physically copied is expected in 'transfers' or 'hardlinks' - # both have same interface [[source_url, destination_url], [source_url...]] - if instance.data.get('transfers', False) or instance.data.get('hardlinks', False): + # any file that should be physically copied is expected in + # 'transfers' or 'hardlinks' + if instance.data.get('transfers', False) or \ + instance.data.get('hardlinks', False): # could throw exception, will be caught in 'process' - # all integration to DB is being done together lower, so no rollback needed + # all integration to DB is being done together lower, + # so no rollback needed self.log.debug("Integrating source files to destination ...") self.integrated_file_sizes.update(self.integrate(instance)) self.log.debug("Integrated files {}".format(self.integrated_file_sizes)) - # get 'files' information for representation and all attached resources + # get 'files' info for representation and all attached resources self.log.debug("Preparing files information ..") representation["files"] = self.get_files_info(instance, self.integrated_file_sizes) @@ -538,9 +544,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Args: instance: the instance to integrate Returns: - integrated_file_sizes: dictionary of destination file url and its size in bytes + integrated_file_sizes: dictionary of destination file url and + its size in bytes """ - integrated_file_sizes = {} # store destination url and size for reporting and rollback + # store destination url and size for reporting and rollback + integrated_file_sizes = {} transfers = list(instance.data.get("transfers", list())) for src, dest in transfers: if os.path.normpath(src) != os.path.normpath(dest): @@ -794,16 +802,19 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def get_rootless_path(self, anatomy, path): - """ Returns, if possible, path without absolute portion from host (eg. 'c:\' or '/opt/..') + """ Returns, if possible, path without absolute portion from host + (eg. 'c:\' or '/opt/..') This information is host dependent and shouldn't be captured. Example: - 'c:/projects/MyProject1/Assets/publish...' > '{root}/MyProject1/Assets...' + 'c:/projects/MyProject1/Assets/publish...' > + '{root}/MyProject1/Assets...' Args: anatomy: anatomy part from instance path: path (absolute) Returns: - path: modified path if possible, or unmodified path + warning logged + path: modified path if possible, or unmodified path + + warning logged """ success, rootless_path = ( anatomy.find_root_template_from_path(path) @@ -812,21 +823,25 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): path = rootless_path else: self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." ).format(path)) return path def get_files_info(self, instance, integrated_file_sizes): """ Prepare 'files' portion for attached resources and main asset. - Combining records from 'transfers' and 'hardlinks' parts from instance. - All attached resources should be added, currently without Context info. + Combining records from 'transfers' and 'hardlinks' parts from + instance. + All attached resources should be added, currently without + Context info. Arguments: instance: the current instance being published - integrated_file_sizes: dictionary of destination path (absolute) and its file size + integrated_file_sizes: dictionary of destination path (absolute) + and its file size Returns: - output_resources: array of dictionaries to be added to 'files' key in representation + output_resources: array of dictionaries to be added to 'files' key + in representation """ resources = list(instance.data.get("transfers", [])) resources.extend(list(instance.data.get("hardlinks", []))) @@ -845,7 +860,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def get_dest_temp_url(self, dest): """ Enhance destination path with TMP_FILE_EXT to denote temporary file. - Temporary files will be renamed after successful registration into DB and full copy to destination + Temporary files will be renamed after successful registration + into DB and full copy to destination Arguments: dest: destination url of published file (absolute) @@ -863,7 +879,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): path: destination url of published file (rootless) size(optional): size of file in bytes hash(optional): hash of file for synchronization validation - sites(optional): array of published locations, ['studio'] by default, + sites(optional): array of published locations, ['studio'] by default expected ['studio', 'site1', 'gdrive1'] Returns: rec: dictionary with filled info @@ -889,27 +905,35 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def handle_destination_files(self, integrated_file_sizes, instance, mode): """ Clean destination files Called when error happened during integrating to DB or to disk + OR called to rename uploaded files from temporary name to final to + highlight publishing in progress/broken Used to clean unwanted files Arguments: - integrated_file_sizes: disctionary of uploaded files, file urls as keys + integrated_file_sizes: dictionary, file urls as keys, size as value instance: processed instance - for publish directories - mode: 'remove' - clean files,'finalize' - rename files, remove TMP_FILE_EXT suffix denoting temp file + mode: 'remove' - clean files, + 'finalize' - rename files, + remove TMP_FILE_EXT suffix denoting temp file """ if integrated_file_sizes: for file_url, file_size in integrated_file_sizes.items(): try: if mode == 'remove': self.log.debug("Removing file...{}".format(file_url)) - os.remove(file_url) # needs to be changed to Factory when sites implemented + os.remove(file_url) if mode == 'finalize': self.log.debug("Renaming file...{}".format(file_url)) import re - os.rename(file_url, re.sub('\.{}$'.format(self.TMP_FILE_EXT), '', file_url)) # needs to be changed to Factory when sites implemented + os.rename(file_url, + re.sub('\.{}$'.format(self.TMP_FILE_EXT), + '', + file_url) + ) except FileNotFoundError: - pass # file not there, nothing to delete - except OSError as e: - self.log.critical("Cannot {} file {}".format(mode, file_url), exc_info=True) - raise - + pass # file not there, nothing to delete + except OSError: + self.log.critical("Cannot {} file {}".format(mode, file_url) + , exc_info=True) + raise \ No newline at end of file From a908e2867ec0ac0f1b3e11cbc8b21965cae2207b Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Fri, 12 Jun 2020 20:42:58 +0200 Subject: [PATCH 006/158] Performance testing script Could be deleted later, no real functionality --- pype/tests/test_mongo_performance.py | 232 +++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 pype/tests/test_mongo_performance.py diff --git a/pype/tests/test_mongo_performance.py b/pype/tests/test_mongo_performance.py new file mode 100644 index 0000000000..6aa8e2ca43 --- /dev/null +++ b/pype/tests/test_mongo_performance.py @@ -0,0 +1,232 @@ +import pytest +import logging +from pprint import pprint +import os +import re +import random +import timeit + +import pymongo +import bson + +class TestPerformance(): + ''' + Class for testing performance of representation and their 'files' parts. + Discussion is if embedded array: + 'files' : [ {'_id': '1111', 'path':'....}, + {'_id'...}] + OR documents: + 'files' : { + '1111': {'path':'....'}, + '2222': {'path':'...'} + } + is faster. + + Current results: without additional partial index documents is 3x faster + With index is array 50x faster then document + + Partial index something like: + db.getCollection('performance_test').createIndex + ({'files._id': 1}, + {partialFilterExpresion: {'files': {'$exists': true}}) + !DIDNT work for me, had to create manually in Compass + + ''' + + MONGO_URL = 'mongodb://localhost:27017' + MONGO_DB = 'performance_test' + MONGO_COLLECTION = 'performance_test' + + inserted_ids = [] + + def __init__(self, version='array'): + ''' + It creates and fills collection, based on value of 'version'. + + :param version: 'array' - files as embedded array, + 'doc' - as document + ''' + self.client = pymongo.MongoClient(self.MONGO_URL) + self.db = self.client[self.MONGO_DB] + self.collection_name = self.MONGO_COLLECTION + + self.version = version + + if self.version != 'array': + self.collection_name = self.MONGO_COLLECTION + '_doc' + + self.collection = self.db[self.collection_name] + + self.ids = [] # for testing + self.inserted_ids = [] + + def prepare(self, no_of_records=100000): + ''' + Produce 'no_of_records' of representations with 'files' segment. + It depends on 'version' value in constructor, 'arrray' or 'doc' + :return: + ''' + print('Purging {} collection'.format(self.collection_name)) + self.collection.delete_many({}) + + id = bson.objectid.ObjectId() + + insert_recs = [] + for i in range(no_of_records): + file_id = bson.objectid.ObjectId() + file_id2 = bson.objectid.ObjectId() + file_id3 = bson.objectid.ObjectId() + + self.inserted_ids.extend([file_id, file_id2, file_id3]) + + document = {"files": self.get_files(self.version, i, + file_id, file_id2, file_id3) + , + "context": { + "subset": "workfileLookdev", + "username": "petrk", + "task": "lookdev", + "family": "workfile", + "hierarchy": "Assets", + "project": {"code": "test", "name": "Test"}, + "version": 1, + "asset": "Cylinder", + "representation": "mb", + "root": "C:/projects" + }, + "dependencies": [], + "name": "mb", + "parent": {"oid": '{}'.format(id)}, + "data": { + "path": "C:\\projects\\Test\\Assets\\Cylinder\\publish\\workfile\\workfileLookdev\\v001\\test_Cylinder_workfileLookdev_v001.mb", + "template": "{root}\\{project[name]}\\{hierarchy}\\{asset}\\publish\\{family}\\{subset}\\v{version:0>3}\\{project[code]}_{asset}_{subset}_v{version:0>3}<_{output}><.{frame:0>4}>.{representation}" + }, + "type": "representation", + "schema": "pype:representation-2.0" + } + + insert_recs.append(document) + + print('Prepared {} records in {} collection'.format(no_of_records, self.collection_name)) + id = self.collection.insert_many(insert_recs) + # TODO refactore to produce real array and not needeing ugly regex + self.collection.insert_one({"inserted_id" : self.inserted_ids}) + print('-' * 50) + + def run(self, queries=1000, loops=3): + ''' + Run X'queries' that are searching collection Y'loops' times + :param queries: how many times do ..find(...) + :param loops: loop of testing X queries + :return: None + ''' + print('Testing version {} on {}'.format(self.version, self.collection_name)) + + inserted_ids = list(self.collection.find({"inserted_id":{"$exists":True}})) + import re + self.ids = re.findall("'[0-9a-z]*'", str(inserted_ids)) + + import time + + found_cnt = 0 + for _ in range(loops): + start = time.time() + for i in range(queries): + val = random.choice(self.ids) + val = val.replace("'",'') + #print(val) + if (self.version == 'array'): + # prepared for partial index, without 'files': exists + # wont engage + found = self.collection.find_one({'files': {"$exists": True}, + 'files._id': "{}".format(val)}) + else: + key = "files.{}".format(val) + found = self.collection.find_one({key: {"$exists": True}}) + if found: + found_cnt += 1 + + end = time.time() + print('duration per loop {}'.format(end - start)) + print("found_cnt {}".format(found_cnt)) + + def get_files(self, mode, i, file_id, file_id2, file_id3): + ''' + Wrapper to decide if 'array' or document version should be used + :param mode: 'array'|'doc' + :param i: step number + :param file_id: ObjectId of first dummy file + :param file_id2: .. + :param file_id3: .. + :return: + ''' + if mode == 'array': + return self.get_files_array(i, file_id, file_id2, file_id3) + else: + return self.get_files_doc(i, file_id, file_id2, file_id3) + + def get_files_array(self, i, file_id, file_id2, file_id3): + return [ + { + "path":"c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderA_workfileLookdev_v{0:03}.mb".format(i), + "_id": '{}'.format(file_id), + "hash":"temphash", + "sites":["studio"], + "size":87236 + }, + { + "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderB_workfileLookdev_v{0:03}.mb".format( + i), + "_id": '{}'.format(file_id2), + "hash": "temphash", + "sites": ["studio"], + "size": 87236 + }, + { + "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderC_workfileLookdev_v{0:03}.mb".format( + i), + "_id": '{}'.format(file_id3), + "hash": "temphash", + "sites": ["studio"], + "size": 87236 + } + + ] + + + def get_files_doc(self, i, file_id, file_id2, file_id3): + ret = {} + ret['{}'.format(file_id)] = { + "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderA_workfileLookdev_v{0:03}.mb".format( + i), + "hash": "temphash", + "sites": ["studio"], + "size": 87236 + } + + + ret['{}'.format(file_id2)] = { + "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderB_workfileLookdev_v{0:03}.mb".format(i), + "hash": "temphash", + "sites": ["studio"], + "size": 87236 + } + ret['{}'.format(file_id3)] = { + "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderC_workfileLookdev_v{0:03}.mb".format(i), + "hash": "temphash", + "sites": ["studio"], + "size": 87236 + } + + return ret + +if __name__ == '__main__': + tp = TestPerformance('array') + tp.prepare() # enable to prepare data + tp.run(1000, 3) + + print('-'*50) + + tp = TestPerformance('doc') + tp.prepare() # enable to prepare data + tp.run(1000, 3) From b1383876564297e8c710dce89653f875cbbadef2 Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Thu, 18 Jun 2020 11:09:20 +0200 Subject: [PATCH 007/158] Added implementation of hash Yanked source_hash function from extract_look into lib (and api) --- pype/api.py | 21 ++++++-- pype/lib.py | 23 ++++++++- pype/plugins/global/publish/integrate_new.py | 54 ++++++++++++-------- pype/plugins/maya/publish/extract_look.py | 25 +-------- 4 files changed, 74 insertions(+), 49 deletions(-) diff --git a/pype/api.py b/pype/api.py index 2c227b5b4b..0cf2573298 100644 --- a/pype/api.py +++ b/pype/api.py @@ -1,5 +1,12 @@ -from .plugin import ( +from pypeapp import ( + Logger, + Anatomy, + project_overrides_dir_path, + config, + execute +) +from .plugin import ( Extractor, ValidatePipelineOrder, @@ -16,8 +23,6 @@ from .action import ( RepairContextAction ) -from pypeapp import Logger - from .lib import ( version_up, get_asset, @@ -26,13 +31,20 @@ from .lib import ( get_subsets, get_version_from_path, modified_environ, - add_tool_to_environment + add_tool_to_environment, + source_hash ) # Special naming case for subprocess since its a built-in method. from .lib import _subprocess as subprocess __all__ = [ + "Logger", + "Anatomy", + "project_overrides_dir_path", + "config", + "execute", + # plugin classes "Extractor", # ordering @@ -58,6 +70,7 @@ __all__ = [ "get_version_from_path", "modified_environ", "add_tool_to_environment", + "source_hash", "subprocess" ] diff --git a/pype/lib.py b/pype/lib.py index 12d4706af8..006a396720 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -15,7 +15,7 @@ from abc import ABCMeta, abstractmethod from avalon import io, pipeline import six import avalon.api -from pypeapp import config +from .api import config log = logging.getLogger(__name__) @@ -1349,3 +1349,24 @@ def ffprobe_streams(path_to_file): popen_output = popen.communicate()[0] log.debug("FFprobe output: {}".format(popen_output)) return json.loads(popen_output)["streams"] + + +def source_hash(filepath, *args): + """Generate simple identifier for a source file. + This is used to identify whether a source file has previously been + processe into the pipeline, e.g. a texture. + The hash is based on source filepath, modification time and file size. + This is only used to identify whether a specific source file was already + published before from the same location with the same modification date. + We opt to do it this way as opposed to Avalanch C4 hash as this is much + faster and predictable enough for all our production use cases. + Args: + filepath (str): The source file path. + You can specify additional arguments in the function + to allow for specific 'processing' values to be included. + """ + # We replace dots with comma because . cannot be a key in a pymongo dict. + file_name = os.path.basename(filepath) + time = str(os.path.getmtime(filepath)) + size = str(os.path.getsize(filepath)) + return "|".join([file_name, time, size] + list(args)).replace(".", ",") diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 0fa12f965d..7104611909 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -9,8 +9,9 @@ import six from pymongo import DeleteOne, InsertOne import pyblish.api -from avalon import api, io +from avalon import io from avalon.vendor import filelink +import pype.api # this is needed until speedcopy for linux is fixed if sys.platform == "win32": @@ -44,6 +45,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "frameStart" "frameEnd" 'fps' + "data": additional metadata for each representation. """ label = "Integrate Asset New" @@ -76,12 +78,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "gizmo", "source", "matchmove", - "image" + "image", "source", "assembly", "fbx", "textures", - "action" + "action", + "harmony.template" ] exclude_families = ["clip"] db_representation_context_keys = [ @@ -94,7 +97,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # file_url : file_size of all published and uploaded files integrated_file_sizes = {} - TMP_FILE_EXT = 'tmp' # suffix to denote temporary files, use without '.' + TMP_FILE_EXT = 'tmp' # suffix to denote temporary files, use without '.' def process(self, instance): self.integrated_file_sizes = {} @@ -107,12 +110,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.info("Integrated Asset in to the database ...") self.log.info("instance.data: {}".format(instance.data)) self.handle_destination_files(self.integrated_file_sizes, - instance, 'finalize') + 'finalize') except Exception as e: # clean destination self.log.critical("Error when registering", exc_info=True) - self.handle_destination_files(self.integrated_file_sizes, - instance, 'remove') + self.handle_destination_files(self.integrated_file_sizes, 'remove') raise def register(self, instance): @@ -394,9 +396,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): index_frame_start += 1 dst = "{0}{1}{2}".format( - dst_head, - dst_padding, - dst_tail).replace("..", ".") + dst_head, + dst_padding, + dst_tail + ).replace("..", ".") self.log.debug("destination: `{}`".format(dst)) src = os.path.join(stagingdir, src_file_name) @@ -469,13 +472,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre_id is None: repre_id = io.ObjectId() + data = repre.get("data") or {} + data.update({'path': dst, 'template': template}) representation = { "_id": repre_id, "schema": "pype:representation-2.0", "type": "representation", "parent": version_id, "name": repre['name'], - "data": {'path': dst, 'template': template}, + "data": data, "dependencies": instance.data.get("dependencies", "").split(), # Imprint shortcut to context @@ -500,11 +505,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # so no rollback needed self.log.debug("Integrating source files to destination ...") self.integrated_file_sizes.update(self.integrate(instance)) - self.log.debug("Integrated files {}".format(self.integrated_file_sizes)) + self.log.debug("Integrated files {}". + format(self.integrated_file_sizes)) # get 'files' info for representation and all attached resources self.log.debug("Preparing files information ..") - representation["files"] = self.get_files_info(instance, self.integrated_file_sizes) + representation["files"] = self.get_files_info( + instance, + self.integrated_file_sizes) self.log.debug("__ representation: {}".format(representation)) destination_list.append(dst) @@ -800,7 +808,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return template_name - def get_rootless_path(self, anatomy, path): """ Returns, if possible, path without absolute portion from host (eg. 'c:\' or '/opt/..') @@ -846,15 +853,21 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): resources = list(instance.data.get("transfers", [])) resources.extend(list(instance.data.get("hardlinks", []))) - self.log.debug("get_resource_files_info.resources: {}".format(resources)) + self.log.debug("get_resource_files_info.resources:{}".format(resources)) output_resources = [] anatomy = instance.context.data["anatomy"] for src, dest in resources: - # TODO - hash or use self.integrated_file_size path = self.get_rootless_path(anatomy, dest) dest = self.get_dest_temp_url(dest) - output_resources.append(self.prepare_file_info(path, integrated_file_sizes[dest], 'temphash')) + hash = pype.api.source_hash(dest) + if self.TMP_FILE_EXT and ',{}'.format(self.TMP_FILE_EXT) in hash: + hash = hash.replace(',{}'.format(self.TMP_FILE_EXT), '') + + file_info = self.prepare_file_info(path, + integrated_file_sizes[dest], + hash) + output_resources.append(file_info) return output_resources @@ -872,7 +885,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dest += '.{}'.format(self.TMP_FILE_EXT) return dest - def prepare_file_info(self, path, size = None, hash = None, sites = None): + def prepare_file_info(self, path, size=None, hash=None, sites=None): """ Prepare information for one file (asset or resource) Arguments: @@ -902,7 +915,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return rec - def handle_destination_files(self, integrated_file_sizes, instance, mode): + def handle_destination_files(self, integrated_file_sizes, mode): """ Clean destination files Called when error happened during integrating to DB or to disk OR called to rename uploaded files from temporary name to final to @@ -911,7 +924,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): Arguments: integrated_file_sizes: dictionary, file urls as keys, size as value - instance: processed instance - for publish directories mode: 'remove' - clean files, 'finalize' - rename files, remove TMP_FILE_EXT suffix denoting temp file @@ -936,4 +948,4 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): except OSError: self.log.critical("Cannot {} file {}".format(mode, file_url) , exc_info=True) - raise \ No newline at end of file + raise diff --git a/pype/plugins/maya/publish/extract_look.py b/pype/plugins/maya/publish/extract_look.py index 58196433aa..6bd202093f 100644 --- a/pype/plugins/maya/publish/extract_look.py +++ b/pype/plugins/maya/publish/extract_look.py @@ -14,34 +14,13 @@ import avalon.maya from avalon import io, api import pype.api -import pype.maya.lib as lib +from pype.hosts.maya import lib # Modes for transfer COPY = 1 HARDLINK = 2 -def source_hash(filepath, *args): - """Generate simple identifier for a source file. - This is used to identify whether a source file has previously been - processe into the pipeline, e.g. a texture. - The hash is based on source filepath, modification time and file size. - This is only used to identify whether a specific source file was already - published before from the same location with the same modification date. - We opt to do it this way as opposed to Avalanch C4 hash as this is much - faster and predictable enough for all our production use cases. - Args: - filepath (str): The source file path. - You can specify additional arguments in the function - to allow for specific 'processing' values to be included. - """ - # We replace dots with comma because . cannot be a key in a pymongo dict. - file_name = os.path.basename(filepath) - time = str(os.path.getmtime(filepath)) - size = str(os.path.getsize(filepath)) - return "|".join([file_name, time, size] + list(args)).replace(".", ",") - - def find_paths_by_hash(texture_hash): # Find the texture hash key in the dictionary and all paths that # originate from it. @@ -363,7 +342,7 @@ class ExtractLook(pype.api.Extractor): args = [] if do_maketx: args.append("maketx") - texture_hash = source_hash(filepath, *args) + texture_hash = pype.api.source_hash(filepath, *args) # If source has been published before with the same settings, # then don't reprocess but hardlink from the original From 2aaae63f80470957552571d27f0e99c719e8f82f Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Thu, 18 Jun 2020 11:41:26 +0200 Subject: [PATCH 008/158] Fix - change level of logging --- pype/plugins/global/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 7104611909..a7ff3e5748 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -946,6 +946,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): except FileNotFoundError: pass # file not there, nothing to delete except OSError: - self.log.critical("Cannot {} file {}".format(mode, file_url) + self.log.error("Cannot {} file {}".format(mode, file_url) , exc_info=True) raise From eafa79af88428867881204f4ddfcf361ef300cef Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Thu, 18 Jun 2020 11:42:20 +0200 Subject: [PATCH 009/158] Taming of Hound --- pype/plugins/global/publish/integrate_new.py | 2 +- pype/tests/test_mongo_performance.py | 134 ++++++++++--------- 2 files changed, 70 insertions(+), 66 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index a7ff3e5748..c5ef2c3530 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -947,5 +947,5 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): pass # file not there, nothing to delete except OSError: self.log.error("Cannot {} file {}".format(mode, file_url) - , exc_info=True) + , exc_info=True) raise diff --git a/pype/tests/test_mongo_performance.py b/pype/tests/test_mongo_performance.py index 6aa8e2ca43..6b62f0fd1c 100644 --- a/pype/tests/test_mongo_performance.py +++ b/pype/tests/test_mongo_performance.py @@ -1,13 +1,7 @@ -import pytest -import logging -from pprint import pprint -import os -import re -import random -import timeit - import pymongo import bson +import random + class TestPerformance(): ''' @@ -57,7 +51,7 @@ class TestPerformance(): self.collection = self.db[self.collection_name] - self.ids = [] # for testing + self.ids = [] # for testing self.inserted_ids = [] def prepare(self, no_of_records=100000): @@ -73,9 +67,9 @@ class TestPerformance(): insert_recs = [] for i in range(no_of_records): - file_id = bson.objectid.ObjectId() - file_id2 = bson.objectid.ObjectId() - file_id3 = bson.objectid.ObjectId() + file_id = bson.objectid.ObjectId() + file_id2 = bson.objectid.ObjectId() + file_id3 = bson.objectid.ObjectId() self.inserted_ids.extend([file_id, file_id2, file_id3]) @@ -98,19 +92,21 @@ class TestPerformance(): "name": "mb", "parent": {"oid": '{}'.format(id)}, "data": { - "path": "C:\\projects\\Test\\Assets\\Cylinder\\publish\\workfile\\workfileLookdev\\v001\\test_Cylinder_workfileLookdev_v001.mb", - "template": "{root}\\{project[name]}\\{hierarchy}\\{asset}\\publish\\{family}\\{subset}\\v{version:0>3}\\{project[code]}_{asset}_{subset}_v{version:0>3}<_{output}><.{frame:0>4}>.{representation}" + "path": "C:\\projects\\Test\\Assets\\Cylinder\\publish\\workfile\\workfileLookdev\\v001\\test_Cylinder_workfileLookdev_v001.mb", + "template": "{root}\\{project[name]}\\{hierarchy}\\{asset}\\publish\\{family}\\{subset}\\v{version:0>3}\\{project[code]}_{asset}_{subset}_v{version:0>3}<_{output}><.{frame:0>4}>.{representation}" }, "type": "representation", "schema": "pype:representation-2.0" - } + } insert_recs.append(document) - print('Prepared {} records in {} collection'.format(no_of_records, self.collection_name)) - id = self.collection.insert_many(insert_recs) + print('Prepared {} records in {} collection'. + format(no_of_records, self.collection_name)) + + self.collection.insert_many(insert_recs) # TODO refactore to produce real array and not needeing ugly regex - self.collection.insert_one({"inserted_id" : self.inserted_ids}) + self.collection.insert_one({"inserted_id": self.inserted_ids}) print('-' * 50) def run(self, queries=1000, loops=3): @@ -120,9 +116,11 @@ class TestPerformance(): :param loops: loop of testing X queries :return: None ''' - print('Testing version {} on {}'.format(self.version, self.collection_name)) + print('Testing version {} on {}'.format(self.version, + self.collection_name)) - inserted_ids = list(self.collection.find({"inserted_id":{"$exists":True}})) + inserted_ids = list(self.collection. + find({"inserted_id": {"$exists": True}})) import re self.ids = re.findall("'[0-9a-z]*'", str(inserted_ids)) @@ -131,15 +129,16 @@ class TestPerformance(): found_cnt = 0 for _ in range(loops): start = time.time() - for i in range(queries): + for _ in range(queries): val = random.choice(self.ids) - val = val.replace("'",'') - #print(val) + val = val.replace("'", '') + if (self.version == 'array'): # prepared for partial index, without 'files': exists # wont engage - found = self.collection.find_one({'files': {"$exists": True}, - 'files._id': "{}".format(val)}) + found = self.collection.\ + find_one({'files': {"$exists": True}, + 'files._id': "{}".format(val)}) else: key = "files.{}".format(val) found = self.collection.find_one({key: {"$exists": True}}) @@ -167,66 +166,71 @@ class TestPerformance(): def get_files_array(self, i, file_id, file_id2, file_id3): return [ - { - "path":"c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderA_workfileLookdev_v{0:03}.mb".format(i), - "_id": '{}'.format(file_id), - "hash":"temphash", - "sites":["studio"], - "size":87236 - }, - { - "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderB_workfileLookdev_v{0:03}.mb".format( - i), - "_id": '{}'.format(file_id2), - "hash": "temphash", - "sites": ["studio"], - "size": 87236 - }, - { - "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderC_workfileLookdev_v{0:03}.mb".format( - i), - "_id": '{}'.format(file_id3), - "hash": "temphash", - "sites": ["studio"], - "size": 87236 - } - - ] + { + "path": "c:/Test/Assets/Cylinder/publish/workfile/" + "workfileLookdev/v001/" + "test_CylinderA_workfileLookdev_v{0:03}.mb".format(i), + "_id": '{}'.format(file_id), + "hash": "temphash", + "sites": ["studio"], + "size":87236 + }, + { + "path": "c:/Test/Assets/Cylinder/publish/workfile/" + "workfileLookdev/v001/" + "test_CylinderB_workfileLookdev_v{0:03}.mb".format(i), + "_id": '{}'.format(file_id2), + "hash": "temphash", + "sites": ["studio"], + "size": 87236 + }, + { + "path": "c:/Test/Assets/Cylinder/publish/workfile/" + "workfileLookdev/v001/" + "test_CylinderC_workfileLookdev_v{0:03}.mb".format(i), + "_id": '{}'.format(file_id3), + "hash": "temphash", + "sites": ["studio"], + "size": 87236 + } + ] def get_files_doc(self, i, file_id, file_id2, file_id3): ret = {} ret['{}'.format(file_id)] = { - "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderA_workfileLookdev_v{0:03}.mb".format( - i), + "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/" + "v001/test_CylinderA_workfileLookdev_v{0:03}.mb".format(i), "hash": "temphash", "sites": ["studio"], "size": 87236 } - ret['{}'.format(file_id2)] = { - "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderB_workfileLookdev_v{0:03}.mb".format(i), - "hash": "temphash", - "sites": ["studio"], - "size": 87236 - } + "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/" + "v001/test_CylinderB_workfileLookdev_v{0:03}.mb".format(i), + "hash": "temphash", + "sites": ["studio"], + "size": 87236 + } ret['{}'.format(file_id3)] = { - "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/v001/test_CylinderC_workfileLookdev_v{0:03}.mb".format(i), - "hash": "temphash", - "sites": ["studio"], - "size": 87236 - } + "path": "c:/Test/Assets/Cylinder/publish/workfile/workfileLookdev/" + "v001/test_CylinderC_workfileLookdev_v{0:03}.mb".format(i), + "hash": "temphash", + "sites": ["studio"], + "size": 87236 + } return ret + if __name__ == '__main__': tp = TestPerformance('array') - tp.prepare() # enable to prepare data + tp.prepare() # enable to prepare data tp.run(1000, 3) print('-'*50) tp = TestPerformance('doc') - tp.prepare() # enable to prepare data + tp.prepare() # enable to prepare data tp.run(1000, 3) From 42c77d0e13aead8cd0d1d27285f60ed07909b377 Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Thu, 18 Jun 2020 12:04:30 +0200 Subject: [PATCH 010/158] Fix - change level of logging on proper place --- pype/plugins/global/publish/integrate_new.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index c5ef2c3530..bb58479d64 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -600,8 +600,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): except OSError as e: if e.errno == errno.EEXIST: pass - else: - self.log.critical("An unexpected error occurred.") + else:# clean destination + self.log.error("An unexpected error occurred.") raise # copy file with speedcopy and check if size of files are simetrical From fe9ed4c09e6cd04378a1b3f2887bf5a0e011c7a0 Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Thu, 18 Jun 2020 12:16:14 +0200 Subject: [PATCH 011/158] Fix - unwanted change of existing logging --- pype/plugins/global/publish/integrate_new.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index bb58479d64..c5ef2c3530 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -600,8 +600,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): except OSError as e: if e.errno == errno.EEXIST: pass - else:# clean destination - self.log.error("An unexpected error occurred.") + else: + self.log.critical("An unexpected error occurred.") raise # copy file with speedcopy and check if size of files are simetrical From ca78a5ca2a28aa0c55163b2d5fcc34e595759443 Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Thu, 18 Jun 2020 12:40:57 +0200 Subject: [PATCH 012/158] Fix - switched from raise to six.reraise Small formatting changes --- pype/plugins/global/publish/integrate_new.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index c5ef2c3530..5c48770f99 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -115,7 +115,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # clean destination self.log.critical("Error when registering", exc_info=True) self.handle_destination_files(self.integrated_file_sizes, 'remove') - raise + six.reraise(*sys.exc_info()) def register(self, instance): # Required environment variables @@ -509,7 +509,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): format(self.integrated_file_sizes)) # get 'files' info for representation and all attached resources - self.log.debug("Preparing files information ..") + self.log.debug("Preparing files information ...") representation["files"] = self.get_files_info( instance, self.integrated_file_sizes) @@ -573,7 +573,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): hardlinks = instance.data.get("hardlinks", list()) for src, dest in hardlinks: dest = self.get_dest_temp_url(dest) - self.log.debug("Hardlinking file .. {} -> {}".format(src, dest)) + self.log.debug("Hardlinking file ... {} -> {}".format(src, dest)) if not os.path.exists(dest): self.hardlink_file(src, dest) @@ -593,7 +593,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): """ src = os.path.normpath(src) dst = os.path.normpath(dst) - self.log.debug("Copying file .. {} -> {}".format(src, dst)) + self.log.debug("Copying file ... {} -> {}".format(src, dst)) dirname = os.path.dirname(dst) try: os.makedirs(dirname) @@ -602,7 +602,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): pass else: self.log.critical("An unexpected error occurred.") - raise + six.reraise(*sys.exc_info()) # copy file with speedcopy and check if size of files are simetrical while True: @@ -625,7 +625,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): pass else: self.log.critical("An unexpected error occurred.") - raise + six.reraise(*sys.exc_info()) filelink.create(src, dst, filelink.HARDLINK) @@ -638,7 +638,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): }) if subset is None: - self.log.info("Subset '%s' not found, creating.." % subset_name) + self.log.info("Subset '%s' not found, creating ..." % subset_name) self.log.debug("families. %s" % instance.data.get('families')) self.log.debug( "families. %s" % type(instance.data.get('families'))) @@ -932,10 +932,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): for file_url, file_size in integrated_file_sizes.items(): try: if mode == 'remove': - self.log.debug("Removing file...{}".format(file_url)) + self.log.debug("Removing file ...{}".format(file_url)) os.remove(file_url) if mode == 'finalize': - self.log.debug("Renaming file...{}".format(file_url)) + self.log.debug("Renaming file ...{}".format(file_url)) import re os.rename(file_url, re.sub('\.{}$'.format(self.TMP_FILE_EXT), @@ -948,4 +948,4 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): except OSError: self.log.error("Cannot {} file {}".format(mode, file_url) , exc_info=True) - raise + six.reraise(*sys.exc_info()) From 0c56ed576d2b12c41896dcc0ba436d1d549f30d3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 01:21:20 +0200 Subject: [PATCH 013/158] logging gui has working filters and sorting with better widgets size on open --- pype/modules/logging/gui/app.py | 10 +- pype/modules/logging/gui/lib.py | 94 --------------- pype/modules/logging/gui/models.py | 179 +++++++++------------------- pype/modules/logging/gui/widgets.py | 79 +++++++----- 4 files changed, 106 insertions(+), 256 deletions(-) delete mode 100644 pype/modules/logging/gui/lib.py diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/gui/app.py index 99b0b230a9..7827bdaf2e 100644 --- a/pype/modules/logging/gui/app.py +++ b/pype/modules/logging/gui/app.py @@ -8,9 +8,9 @@ class LogsWindow(QtWidgets.QWidget): super(LogsWindow, self).__init__(parent) self.setStyleSheet(style.load_stylesheet()) - self.resize(1200, 800) - logs_widget = LogsWidget(parent=self) + self.resize(1400, 800) log_detail = OutputWidget(parent=self) + logs_widget = LogsWidget(log_detail, parent=self) main_layout = QtWidgets.QHBoxLayout() @@ -18,8 +18,6 @@ class LogsWindow(QtWidgets.QWidget): log_splitter.setOrientation(QtCore.Qt.Horizontal) log_splitter.addWidget(logs_widget) log_splitter.addWidget(log_detail) - log_splitter.setStretchFactor(0, 65) - log_splitter.setStretchFactor(1, 35) main_layout.addWidget(log_splitter) @@ -33,5 +31,5 @@ class LogsWindow(QtWidgets.QWidget): def on_selection_changed(self): index = self.logs_widget.selected_log() - node = index.data(self.logs_widget.model.NodeRole) - self.log_detail.set_detail(node) + logs = index.data(self.logs_widget.model.ROLE_LOGS) + self.log_detail.set_detail(logs) diff --git a/pype/modules/logging/gui/lib.py b/pype/modules/logging/gui/lib.py deleted file mode 100644 index 85782e071e..0000000000 --- a/pype/modules/logging/gui/lib.py +++ /dev/null @@ -1,94 +0,0 @@ -import contextlib -from Qt import QtCore - - -def _iter_model_rows( - model, column, include_root=False -): - """Iterate over all row indices in a model""" - indices = [QtCore.QModelIndex()] # start iteration at root - - for index in indices: - # Add children to the iterations - child_rows = model.rowCount(index) - for child_row in range(child_rows): - child_index = model.index(child_row, column, index) - indices.append(child_index) - - if not include_root and not index.isValid(): - continue - - yield index - - -@contextlib.contextmanager -def preserve_states( - tree_view, column=0, role=None, - preserve_expanded=True, preserve_selection=True, - expanded_role=QtCore.Qt.DisplayRole, selection_role=QtCore.Qt.DisplayRole - -): - """Preserves row selection in QTreeView by column's data role. - - This function is created to maintain the selection status of - the model items. When refresh is triggered the items which are expanded - will stay expanded and vise versa. - - tree_view (QWidgets.QTreeView): the tree view nested in the application - column (int): the column to retrieve the data from - role (int): the role which dictates what will be returned - - Returns: - None - - """ - # When `role` is set then override both expanded and selection roles - if role: - expanded_role = role - selection_role = role - - model = tree_view.model() - selection_model = tree_view.selectionModel() - flags = selection_model.Select | selection_model.Rows - - expanded = set() - - if preserve_expanded: - for index in _iter_model_rows( - model, column=column, include_root=False - ): - if tree_view.isExpanded(index): - value = index.data(expanded_role) - expanded.add(value) - - selected = None - - if preserve_selection: - selected_rows = selection_model.selectedRows() - if selected_rows: - selected = set(row.data(selection_role) for row in selected_rows) - - try: - yield - finally: - if expanded: - for index in _iter_model_rows( - model, column=0, include_root=False - ): - value = index.data(expanded_role) - is_expanded = value in expanded - # skip if new index was created meanwhile - if is_expanded is None: - continue - tree_view.setExpanded(index, is_expanded) - - if selected: - # Go through all indices, select the ones with similar data - for index in _iter_model_rows( - model, column=column, include_root=False - ): - value = index.data(selection_role) - state = value in selected - if state: - tree_view.scrollTo(index) # Ensure item is visible - selection_model.select(index, flags) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index ce1fa236a9..b739739b6f 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -1,21 +1,20 @@ import collections -from Qt import QtCore +from Qt import QtCore, QtGui from pype.api import Logger from pypeapp.lib.log import _bootstrap_mongo_log, LOG_COLLECTION_NAME log = Logger().get_logger("LogModel", "LoggingModule") -class LogModel(QtCore.QAbstractItemModel): - COLUMNS = [ +class LogModel(QtGui.QStandardItemModel): + COLUMNS = ( "process_name", "hostname", "hostip", "username", "system_name", "started" - ] - + ) colums_mapping = { "process_name": "Process Name", "process_id": "Process Id", @@ -25,30 +24,52 @@ class LogModel(QtCore.QAbstractItemModel): "system_name": "System name", "started": "Started at" } - process_keys = [ + process_keys = ( "process_id", "hostname", "hostip", "username", "system_name", "process_name" - ] - log_keys = [ + ) + log_keys = ( "timestamp", "level", "thread", "threadName", "message", "loggerName", "fileName", "module", "method", "lineNumber" - ] + ) default_value = "- Not set -" - NodeRole = QtCore.Qt.UserRole + 1 + + ROLE_LOGS = QtCore.Qt.UserRole + 2 def __init__(self, parent=None): super(LogModel, self).__init__(parent) - self._root_node = Node() + self.log_by_process = None self.dbcon = None + # Crash if connection is not possible to skip this module database = _bootstrap_mongo_log() if LOG_COLLECTION_NAME in database.list_collection_names(): self.dbcon = database[LOG_COLLECTION_NAME] - def add_log(self, log): - node = Node(log) - self._root_node.add_child(node) + def headerData(self, section, orientation, role): + if ( + role == QtCore.Qt.DisplayRole + and orientation == QtCore.Qt.Horizontal + ): + if section < len(self.COLUMNS): + key = self.COLUMNS[section] + return self.colums_mapping.get(key, key) + + super(LogModel, self).headerData(section, orientation, role) + + def add_process_logs(self, process_logs): + items = [] + first_item = True + for key in self.COLUMNS: + display_value = str(process_logs[key]) + item = QtGui.QStandardItem(display_value) + if first_item: + first_item = False + item.setData(process_logs["_logs"], self.ROLE_LOGS) + + items.append(item) + self.appendRow(items) def refresh(self): self.log_by_process = collections.defaultdict(list) @@ -65,16 +86,13 @@ class LogModel(QtCore.QAbstractItemModel): continue if process_id not in self.process_info: - proc_dict = {} + proc_dict = {"_logs": []} for key in self.process_keys: proc_dict[key] = ( item.get(key) or self.default_value ) self.process_info[process_id] = proc_dict - if "_logs" not in self.process_info[process_id]: - self.process_info[process_id]["_logs"] = [] - log_item = {} for key in self.log_keys: log_item[key] = item.get(key) or self.default_value @@ -89,114 +107,29 @@ class LogModel(QtCore.QAbstractItemModel): item["_logs"], key=lambda item: item["timestamp"] ) item["started"] = item["_logs"][0]["timestamp"] - self.add_log(item) + self.add_process_logs(item) self.endResetModel() - def data(self, index, role): - if not index.isValid(): - return None - if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: - node = index.internalPointer() - column = index.column() +class LogsFilterProxy(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(LogsFilterProxy, self).__init__(*args, **kwargs) + self.col_usernames = None + self.filter_usernames = set() - key = self.COLUMNS[column] - if key == "started": - return str(node.get(key, None)) - return node.get(key, None) + def update_users_filter(self, users): + self.filter_usernames = set() + for user in users or tuple(): + self.filter_usernames.add(user) + self.invalidateFilter() - if role == self.NodeRole: - return index.internalPointer() - - def index(self, row, column, parent): - """Return index for row/column under parent""" - - if not parent.isValid(): - parent_node = self._root_node - else: - parent_node = parent.internalPointer() - - child_item = parent_node.child(row) - if child_item: - return self.createIndex(row, column, child_item) - return QtCore.QModelIndex() - - def rowCount(self, parent): - node = self._root_node - if parent.isValid(): - node = parent.internalPointer() - return node.childCount() - - def columnCount(self, parent): - return len(self.COLUMNS) - - def parent(self, index): - return QtCore.QModelIndex() - - def headerData(self, section, orientation, role): - if role == QtCore.Qt.DisplayRole: - if section < len(self.COLUMNS): - key = self.COLUMNS[section] - return self.colums_mapping.get(key, key) - - super(LogModel, self).headerData(section, orientation, role) - - def flags(self, index): - return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) - - def clear(self): - self.beginResetModel() - self._root_node = Node() - self.endResetModel() - - -class Node(dict): - """A node that can be represented in a tree view. - - The node can store data just like a dictionary. - - >>> data = {"name": "John", "score": 10} - >>> node = Node(data) - >>> assert node["name"] == "John" - - """ - - def __init__(self, data=None): - super(Node, self).__init__() - - self._children = list() - self._parent = None - - if data is not None: - assert isinstance(data, dict) - self.update(data) - - def childCount(self): - return len(self._children) - - def child(self, row): - if row >= len(self._children): - log.warning("Invalid row as child: {0}".format(row)) - return - - return self._children[row] - - def children(self): - return self._children - - def parent(self): - return self._parent - - def row(self): - """ - Returns: - int: Index of this node under parent""" - if self._parent is not None: - siblings = self.parent().children() - return siblings.index(self) - - def add_child(self, child): - """Add a child to this node""" - child._parent = self - self._children.append(child) + def filterAcceptsRow(self, source_row, source_parent): + if self.col_usernames is not None: + index = self.sourceModel().index( + source_row, self.col_usernames, source_parent + ) + user = index.data(QtCore.Qt.DisplayRole) + if user not in self.filter_usernames: + return False + return True diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index cf20066397..f567cae674 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -1,6 +1,6 @@ from Qt import QtCore, QtWidgets, QtGui from PyQt5.QtCore import QVariant -from .models import LogModel +from .models import LogModel, LogsFilterProxy class SearchComboBox(QtWidgets.QComboBox): @@ -193,54 +193,37 @@ class LogsWidget(QtWidgets.QWidget): active_changed = QtCore.Signal() - def __init__(self, parent=None): + def __init__(self, detail_widget, parent=None): super(LogsWidget, self).__init__(parent=parent) model = LogModel() + proxy_model = LogsFilterProxy() + proxy_model.setSourceModel(model) + proxy_model.col_usernames = model.COLUMNS.index("username") filter_layout = QtWidgets.QHBoxLayout() # user_filter = SearchComboBox(self, "Users") user_filter = CustomCombo("Users", self) - users = model.dbcon.distinct("user") + users = model.dbcon.distinct("username") user_filter.populate(users) user_filter.selection_changed.connect(self.user_changed) + proxy_model.update_users_filter(users) + level_filter = CustomCombo("Levels", self) # levels = [(level, True) for level in model.dbcon.distinct("level")] levels = model.dbcon.distinct("level") level_filter.addItems(levels) + level_filter.selection_changed.connect(self.level_changed) - date_from_label = QtWidgets.QLabel("From:") - date_filter_from = QtWidgets.QDateTimeEdit() - - date_from_layout = QtWidgets.QVBoxLayout() - date_from_layout.addWidget(date_from_label) - date_from_layout.addWidget(date_filter_from) - - # now = datetime.datetime.now() - # QtCore.QDateTime( - # now.year, - # now.month, - # now.day, - # now.hour, - # now.minute, - # second=0, - # msec=0, - # timeSpec=0 - # ) - date_to_label = QtWidgets.QLabel("To:") - date_filter_to = QtWidgets.QDateTimeEdit() - - date_to_layout = QtWidgets.QVBoxLayout() - date_to_layout.addWidget(date_to_label) - date_to_layout.addWidget(date_filter_to) + detail_widget.update_level_filter(levels) filter_layout.addWidget(user_filter) filter_layout.addWidget(level_filter) - filter_layout.addLayout(date_from_layout) - filter_layout.addLayout(date_to_layout) + spacer = QtWidgets.QWidget() + filter_layout.addWidget(spacer, 1) view = QtWidgets.QTreeView(self) view.setAllColumnsShowFocus(True) @@ -250,6 +233,8 @@ class LogsWidget(QtWidgets.QWidget): layout.addLayout(filter_layout) layout.addWidget(view) + view.setModel(proxy_model) + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) view.setSortingEnabled(True) view.sortByColumn( @@ -257,24 +242,36 @@ class LogsWidget(QtWidgets.QWidget): QtCore.Qt.AscendingOrder ) - view.setModel(model) view.pressed.connect(self._on_activated) # prepare model.refresh() # Store to memory self.model = model + self.proxy_model = proxy_model self.view = view self.user_filter = user_filter self.level_filter = level_filter + self.detail_widget = detail_widget + def _on_activated(self, *args, **kwargs): self.active_changed.emit() def user_changed(self): + checked_values = set() for action in self.user_filter.items(): - print(action) + if action.isChecked(): + checked_values.add(action.text()) + self.proxy_model.update_users_filter(checked_values) + + def level_changed(self): + checked_values = set() + for action in self.level_filter.items(): + if action.isChecked(): + checked_values.add(action.text()) + self.detail_widget.update_level_filter(checked_values) def on_context_menu(self, point): # TODO will be any actions? it's ready @@ -309,13 +306,29 @@ class OutputWidget(QtWidgets.QWidget): self.setLayout(layout) self.output_text = output_text + self.las_logs = None + self.filter_levels = set() + + def update_level_filter(self, levels): + self.filter_levels = set() + for level in levels or tuple(): + self.filter_levels.add(level.lower()) + + self.set_detail(self.las_logs) + def add_line(self, line): self.output_text.append(line) - def set_detail(self, node): + def set_detail(self, logs): + self.las_logs = logs self.output_text.clear() - for log in node["_logs"]: + if not logs: + return + + for log in logs: level = log["level"].lower() + if level not in self.filter_levels: + continue line_f = "{message}" if level == "debug": From a6423c97120859916b61525ebc63e043e722fe7f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 01:35:28 +0200 Subject: [PATCH 014/158] added timestamp support --- pype/modules/logging/gui/widgets.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index f567cae674..aa044946cb 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -297,18 +297,32 @@ class OutputWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(OutputWidget, self).__init__(parent=parent) layout = QtWidgets.QVBoxLayout(self) + + show_timecode_checkbox = QtWidgets.QCheckBox("Show timestamp") + output_text = QtWidgets.QTextEdit() output_text.setReadOnly(True) # output_text.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth) + layout.addWidget(show_timecode_checkbox) layout.addWidget(output_text) + show_timecode_checkbox.stateChanged.connect( + self.on_show_timecode_change + ) self.setLayout(layout) self.output_text = output_text + self.show_timecode_checkbox = show_timecode_checkbox self.las_logs = None self.filter_levels = set() + def show_timecode(self): + return self.show_timecode_checkbox.isChecked() + + def on_show_timecode_change(self): + self.set_detail(self.las_logs) + def update_level_filter(self, levels): self.filter_levels = set() for level in levels or tuple(): @@ -325,6 +339,7 @@ class OutputWidget(QtWidgets.QWidget): if not logs: return + show_timecode = self.show_timecode() for log in logs: level = log["level"].lower() if level not in self.filter_levels: @@ -366,6 +381,10 @@ class OutputWidget(QtWidgets.QWidget): line = line_f.format(**log) + if show_timecode: + timestamp = log["timestamp"] + line = timestamp.strftime("%Y-%d-%m %H:%M:%S") + " " + line + self.add_line(line) if not exc: From 80a26eb9de57c42f0dc1873cc71e2417a1e398c6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 01:59:09 +0200 Subject: [PATCH 015/158] removed unused --- pype/modules/logging/gui/widgets.py | 139 ---------------------------- 1 file changed, 139 deletions(-) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index aa044946cb..51d3095b44 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -50,37 +50,6 @@ class SearchComboBox(QtWidgets.QComboBox): return text -class CheckableComboBox2(QtWidgets.QComboBox): - def __init__(self, parent=None): - super(CheckableComboBox, self).__init__(parent) - self.view().pressed.connect(self.handleItemPressed) - self._changed = False - - def handleItemPressed(self, index): - item = self.model().itemFromIndex(index) - if item.checkState() == QtCore.Qt.Checked: - item.setCheckState(QtCore.Qt.Unchecked) - else: - item.setCheckState(QtCore.Qt.Checked) - self._changed = True - - def hidePopup(self): - if not self._changed: - super(CheckableComboBox, self).hidePopup() - self._changed = False - - def itemChecked(self, index): - item = self.model().item(index, self.modelColumn()) - return item.checkState() == QtCore.Qt.Checked - - def setItemChecked(self, index, checked=True): - item = self.model().item(index, self.modelColumn()) - if checked: - item.setCheckState(QtCore.Qt.Checked) - else: - item.setCheckState(QtCore.Qt.Unchecked) - - class SelectableMenu(QtWidgets.QMenu): selection_changed = QtCore.Signal() @@ -137,57 +106,6 @@ class CustomCombo(QtWidgets.QWidget): yield action -class CheckableComboBox(QtWidgets.QComboBox): - def __init__(self, parent=None): - super(CheckableComboBox, self).__init__(parent) - - view = QtWidgets.QTreeView() - view.header().hide() - view.setRootIsDecorated(False) - - model = QtGui.QStandardItemModel() - - view.pressed.connect(self.handleItemPressed) - self._changed = False - - self.setView(view) - self.setModel(model) - - self.view = view - self.model = model - - def handleItemPressed(self, index): - item = self.model.itemFromIndex(index) - if item.checkState() == QtCore.Qt.Checked: - item.setCheckState(QtCore.Qt.Unchecked) - else: - item.setCheckState(QtCore.Qt.Checked) - self._changed = True - - def hidePopup(self): - if not self._changed: - super(CheckableComboBox, self).hidePopup() - self._changed = False - - def itemChecked(self, index): - item = self.model.item(index, self.modelColumn()) - return item.checkState() == QtCore.Qt.Checked - - def setItemChecked(self, index, checked=True): - item = self.model.item(index, self.modelColumn()) - if checked: - item.setCheckState(QtCore.Qt.Checked) - else: - item.setCheckState(QtCore.Qt.Unchecked) - - def addItems(self, items): - for text, checked in items: - text_item = QtGui.QStandardItem(text) - checked_item = QtGui.QStandardItem() - checked_item.setData(QVariant(checked), QtCore.Qt.CheckStateRole) - self.model.appendRow([text_item, checked_item]) - - class LogsWidget(QtWidgets.QWidget): """A widget that lists the published subsets for an asset""" @@ -391,60 +309,3 @@ class OutputWidget(QtWidgets.QWidget): continue for _line in exc["stackTrace"].split("\n"): self.add_line(_line) - - -class LogDetailWidget(QtWidgets.QWidget): - """A Widget that display information about a specific version""" - data_rows = [ - "user", - "message", - "level", - "logname", - "method", - "module", - "fileName", - "lineNumber", - "host", - "timestamp" - ] - - html_text = u""" -

{user} - {timestamp}

-User
{user}
-
Level
{level}
-
Message
{message}
-
Log Name
{logname}

Method
{method}
-
File
{fileName}
-
Line
{lineNumber}
-
Host
{host}
-
Timestamp
{timestamp}
-""" - - def __init__(self, parent=None): - super(LogDetailWidget, self).__init__(parent=parent) - - layout = QtWidgets.QVBoxLayout(self) - - label = QtWidgets.QLabel("Detail") - detail_widget = QtWidgets.QTextEdit() - detail_widget.setReadOnly(True) - layout.addWidget(label) - layout.addWidget(detail_widget) - - self.detail_widget = detail_widget - - self.setEnabled(True) - - self.set_detail(None) - - def set_detail(self, detail_data): - if not detail_data: - self.detail_widget.setText("") - return - - data = dict() - for row in self.data_rows: - value = detail_data.get(row) or "< Not set >" - data[row] = value - - self.detail_widget.setHtml(self.html_text.format(**data)) From 5c57e183b0efc64e4af1f49bc116e7daf59bf14a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 01:59:35 +0200 Subject: [PATCH 016/158] added refresh --- pype/modules/logging/gui/app.py | 7 ---- pype/modules/logging/gui/models.py | 3 +- pype/modules/logging/gui/widgets.py | 50 +++++++++++++++++++---------- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/gui/app.py index 7827bdaf2e..c0e180c8a1 100644 --- a/pype/modules/logging/gui/app.py +++ b/pype/modules/logging/gui/app.py @@ -26,10 +26,3 @@ class LogsWindow(QtWidgets.QWidget): self.setLayout(main_layout) self.setWindowTitle("Logs") - - self.logs_widget.active_changed.connect(self.on_selection_changed) - - def on_selection_changed(self): - index = self.logs_widget.selected_log() - logs = index.data(self.logs_widget.model.ROLE_LOGS) - self.log_detail.set_detail(logs) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index b739739b6f..ae2666f501 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -35,6 +35,7 @@ class LogModel(QtGui.QStandardItemModel): default_value = "- Not set -" ROLE_LOGS = QtCore.Qt.UserRole + 2 + ROLE_PROCESS_ID = QtCore.Qt.UserRole + 3 def __init__(self, parent=None): super(LogModel, self).__init__(parent) @@ -67,7 +68,7 @@ class LogModel(QtGui.QStandardItemModel): if first_item: first_item = False item.setData(process_logs["_logs"], self.ROLE_LOGS) - + item.setData(process_logs["process_id"], self.ROLE_PROCESS_ID) items.append(item) self.appendRow(items) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index 51d3095b44..5304fc4d56 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -1,5 +1,5 @@ -from Qt import QtCore, QtWidgets, QtGui -from PyQt5.QtCore import QVariant +from Qt import QtCore, QtWidgets +from avalon.vendor import qtawesome from .models import LogModel, LogsFilterProxy @@ -109,8 +109,6 @@ class CustomCombo(QtWidgets.QWidget): class LogsWidget(QtWidgets.QWidget): """A widget that lists the published subsets for an asset""" - active_changed = QtCore.Signal() - def __init__(self, detail_widget, parent=None): super(LogsWidget, self).__init__(parent=parent) @@ -125,7 +123,7 @@ class LogsWidget(QtWidgets.QWidget): user_filter = CustomCombo("Users", self) users = model.dbcon.distinct("username") user_filter.populate(users) - user_filter.selection_changed.connect(self.user_changed) + user_filter.selection_changed.connect(self._user_changed) proxy_model.update_users_filter(users) @@ -133,15 +131,19 @@ class LogsWidget(QtWidgets.QWidget): # levels = [(level, True) for level in model.dbcon.distinct("level")] levels = model.dbcon.distinct("level") level_filter.addItems(levels) - level_filter.selection_changed.connect(self.level_changed) + level_filter.selection_changed.connect(self._level_changed) detail_widget.update_level_filter(levels) + spacer = QtWidgets.QWidget() + + icon = qtawesome.icon("fa.refresh", color="white") + refresh_btn = QtWidgets.QPushButton(icon, "") + filter_layout.addWidget(user_filter) filter_layout.addWidget(level_filter) - - spacer = QtWidgets.QWidget() filter_layout.addWidget(spacer, 1) + filter_layout.addWidget(refresh_btn) view = QtWidgets.QTreeView(self) view.setAllColumnsShowFocus(True) @@ -160,9 +162,8 @@ class LogsWidget(QtWidgets.QWidget): QtCore.Qt.AscendingOrder ) - view.pressed.connect(self._on_activated) - # prepare - model.refresh() + view.selectionModel().selectionChanged.connect(self._on_index_change) + refresh_btn.clicked.connect(self._on_refresh_clicked) # Store to memory self.model = model @@ -173,18 +174,33 @@ class LogsWidget(QtWidgets.QWidget): self.level_filter = level_filter self.detail_widget = detail_widget + self.refresh_btn = refresh_btn - def _on_activated(self, *args, **kwargs): - self.active_changed.emit() + # prepare + self.refresh() - def user_changed(self): + def refresh(self): + self.model.refresh() + + def _on_refresh_clicked(self): + self.refresh() + + def _on_index_change(self, to_index, from_index): + index = self._selected_log() + if index: + logs = index.data(self.model.ROLE_LOGS) + else: + logs = [] + self.detail_widget.set_detail(logs) + + def _user_changed(self): checked_values = set() for action in self.user_filter.items(): if action.isChecked(): checked_values.add(action.text()) self.proxy_model.update_users_filter(checked_values) - def level_changed(self): + def _level_changed(self): checked_values = set() for action in self.level_filter.items(): if action.isChecked(): @@ -203,7 +219,7 @@ class LogsWidget(QtWidgets.QWidget): selection = self.view.selectionModel() rows = selection.selectedRows(column=0) - def selected_log(self): + def _selected_log(self): selection = self.view.selectionModel() rows = selection.selectedRows(column=0) if len(rows) == 1: @@ -251,7 +267,7 @@ class OutputWidget(QtWidgets.QWidget): def add_line(self, line): self.output_text.append(line) - def set_detail(self, logs): + def set_detail(self, logs=None): self.las_logs = logs self.output_text.clear() if not logs: From 862a07ac4f9b513ab20b1d613709497dfad5afb6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 02:09:37 +0200 Subject: [PATCH 017/158] refresh enhancement --- pype/modules/logging/gui/widgets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index 5304fc4d56..826f32646d 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -181,6 +181,7 @@ class LogsWidget(QtWidgets.QWidget): def refresh(self): self.model.refresh() + self.detail_widget.refresh() def _on_refresh_clicked(self): self.refresh() @@ -248,8 +249,10 @@ class OutputWidget(QtWidgets.QWidget): self.output_text = output_text self.show_timecode_checkbox = show_timecode_checkbox - self.las_logs = None - self.filter_levels = set() + self.refresh() + + def refresh(self): + self.set_detail() def show_timecode(self): return self.show_timecode_checkbox.isChecked() From 5b307448c99aaea8ab41cf3261f8b83a552ea317 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 13:41:44 +0200 Subject: [PATCH 018/158] disable editing --- pype/modules/logging/gui/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index 826f32646d..9b6c0a6a62 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -147,6 +147,7 @@ class LogsWidget(QtWidgets.QWidget): view = QtWidgets.QTreeView(self) view.setAllColumnsShowFocus(True) + view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) From 04e6c06ad1bde4d3375a230f416f1b6e5d888f96 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 13:44:28 +0200 Subject: [PATCH 019/158] reverse sort order --- pype/modules/logging/gui/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index 9b6c0a6a62..cd0df283bf 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -160,7 +160,7 @@ class LogsWidget(QtWidgets.QWidget): view.setSortingEnabled(True) view.sortByColumn( model.COLUMNS.index("started"), - QtCore.Qt.AscendingOrder + QtCore.Qt.DescendingOrder ) view.selectionModel().selectionChanged.connect(self._on_index_change) From 616817df3e641ce56d6f1e33b631455c7fd768d7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 28 Jul 2020 18:02:59 +0200 Subject: [PATCH 020/158] support for tile render job submitted from elsewhere --- .../global/publish/submit_publish_job.py | 71 +++++++++++++------ pype/plugins/maya/create/create_render.py | 8 ++- .../maya/publish/submit_maya_deadline.py | 17 ++--- 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 9f89466c31..838b1717c2 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -252,7 +252,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "Plugin": "Python", "BatchName": job["Props"]["Batch"], "Name": job_name, - "JobDependency0": job["_id"], "UserName": job["Props"]["User"], "Comment": instance.context.data.get("comment", ""), @@ -275,6 +274,25 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # Mandatory for Deadline, may be empty "AuxFiles": [], } + """ + In this part we will add file dependencies instead of job dependencies. + This way we don't need to take care of tile assembly job, getting its + id or name. We expect it to produce specific file with specific name + and we are just waiting for them. + """ + if instance.data.get("tileRendering"): + asset_index = 0 + for represenation in instance.data.get("representations", []): + if isinstance(represenation["files"], [list, tuple]): + for file in represenation["files"]: + dependency = os.path.join(output_dir, file) + payload["JobInfo"]["AssetDependency{}".format(asset_index)] = dependency # noqa: E501 + asset_index += 1 + else: + dependency = os.path.join(output_dir, file) + payload["JobInfo"]["AssetDependency0"] = dependency + else: + payload["JobInfo"]["JobDependency0"] = job["_id"], # Transfer the environment from the original job to this dependent # job so they use the same environment @@ -613,25 +631,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if hasattr(instance, "_log"): data['_log'] = instance._log - render_job = data.pop("deadlineSubmissionJob", None) - submission_type = "deadline" - if not render_job: - # No deadline job. Try Muster: musterSubmissionJob - render_job = data.pop("musterSubmissionJob", None) - submission_type = "muster" - assert render_job, ( - "Can't continue without valid Deadline " - "or Muster submission prior to this " - "plug-in." - ) - - if submission_type == "deadline": - self.DEADLINE_REST_URL = os.environ.get( - "DEADLINE_REST_URL", "http://localhost:8082" - ) - assert self.DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" - - self._submit_deadline_post_job(instance, render_job) asset = data.get("asset") or api.Session["AVALON_ASSET"] subset = data.get("subset") @@ -846,6 +845,36 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): at.get("subset"), at.get("version"))) instances = new_instances + r''' SUBMiT PUBLiSH JOB 2 D34DLiN3 + ____ + ' ' .---. .---. .--. .---. .--..--..--..--. .---. + | | --= \ | . \/ _|/ \| . \ || || \ |/ _| + | JOB | --= / | | || __| .. | | | |;_ || \ || __| + | | |____./ \.__|._||_.|___./|_____|||__|\__|\.___| + ._____. + + ''' + + render_job = data.pop("deadlineSubmissionJob", None) + submission_type = "deadline" + if not render_job: + # No deadline job. Try Muster: musterSubmissionJob + render_job = data.pop("musterSubmissionJob", None) + submission_type = "muster" + assert render_job or instance.data.get("tileRendering") is False, ( + "Can't continue without valid Deadline " + "or Muster submission prior to this " + "plug-in." + ) + + if submission_type == "deadline": + self.DEADLINE_REST_URL = os.environ.get( + "DEADLINE_REST_URL", "http://localhost:8082" + ) + assert self.DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" + + self._submit_deadline_post_job(instance, render_job) + # publish job file publish_job = { "asset": asset, @@ -857,7 +886,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "version": context.data["version"], # this is workfile version "intent": context.data.get("intent"), "comment": context.data.get("comment"), - "job": render_job, + "job": render_job or None, "session": api.Session.copy(), "instances": instances } diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index 3b2048d8f0..9e5f9310ae 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -40,6 +40,9 @@ class CreateRender(avalon.maya.Creator): vrscene (bool): Submit as ``vrscene`` file for standalone V-Ray renderer. ass (bool): Submit as ``ass`` file for standalone Arnold renderer. + tileRendering (bool): Instance is set to tile rendering mode. We + won't submit actuall render, but we'll make publish job to wait + for Tile Assemly job done and then publish. See Also: https://pype.club/docs/artist_hosts_maya#creating-basic-render-setup @@ -181,6 +184,7 @@ class CreateRender(avalon.maya.Creator): self.data["machineList"] = "" self.data["useMayaBatch"] = False self.data["vrayScene"] = False + self.data["tileRendering"] = False # Disable for now as this feature is not working yet # self.data["assScene"] = False @@ -189,8 +193,8 @@ class CreateRender(avalon.maya.Creator): def _load_credentials(self): """Load Muster credentials. - Load Muster credentials from file and set ```MUSTER_USER``, - ```MUSTER_PASSWORD``, ``MUSTER_REST_URL`` is loaded from presets. + Load Muster credentials from file and set ``MUSTER_USER``, + ``MUSTER_PASSWORD``, ``MUSTER_REST_URL`` is loaded from presets. Raises: RuntimeError: If loaded credentials are invalid. diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index d81d43749c..3a6d12f623 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -392,18 +392,19 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.preflight_check(instance) # Submit job to farm ------------------------------------------------ - self.log.info("Submitting ...") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) + if not instance.data.get("tileRendering"): + self.log.info("Submitting ...") + self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(self._deadline_url) - response = self._requests_post(url, json=payload) - if not response.ok: - raise Exception(response.text) + # E.g. http://192.168.0.1:8082/api/jobs + url = "{}/api/jobs".format(self._deadline_url) + response = self._requests_post(url, json=payload) + if not response.ok: + raise Exception(response.text) + instance.data["deadlineSubmissionJob"] = response.json() # Store output dir for unified publisher (filesequence) instance.data["outputDir"] = os.path.dirname(output_filename_0) - instance.data["deadlineSubmissionJob"] = response.json() def _get_maya_payload(self, data): payload = copy.deepcopy(payload_skeleton) From 1038619bebb4aefcf65cd0c9c7cd3ae45e066dcd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 28 Jul 2020 19:39:46 +0200 Subject: [PATCH 021/158] change the way job farm type is determined --- .../publish/submit_celaction_deadline.py | 1 + pype/plugins/fusion/publish/submit_deadline.py | 2 +- .../global/publish/submit_publish_job.py | 18 +++++++++--------- pype/plugins/maya/publish/collect_render.py | 1 + .../maya/publish/submit_maya_deadline.py | 12 +++++++++--- .../plugins/maya/publish/submit_maya_muster.py | 1 + .../nuke/publish/submit_nuke_deadline.py | 1 + 7 files changed, 23 insertions(+), 13 deletions(-) diff --git a/pype/plugins/celaction/publish/submit_celaction_deadline.py b/pype/plugins/celaction/publish/submit_celaction_deadline.py index 9091b24150..de764e3b33 100644 --- a/pype/plugins/celaction/publish/submit_celaction_deadline.py +++ b/pype/plugins/celaction/publish/submit_celaction_deadline.py @@ -34,6 +34,7 @@ class ExtractCelactionDeadline(pyblish.api.InstancePlugin): ] def process(self, instance): + instance.data["toBeRenderedOn"] = "deadline" context = instance.context DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL") diff --git a/pype/plugins/fusion/publish/submit_deadline.py b/pype/plugins/fusion/publish/submit_deadline.py index e5deb1b070..0dd34ba713 100644 --- a/pype/plugins/fusion/publish/submit_deadline.py +++ b/pype/plugins/fusion/publish/submit_deadline.py @@ -22,7 +22,7 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): families = ["saver.deadline"] def process(self, instance): - + instance.data["toBeRenderedOn"] = "deadline" context = instance.context key = "__hasRun{}".format(self.__class__.__name__) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 838b1717c2..21dcf93cdb 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -855,17 +855,17 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): ''' - render_job = data.pop("deadlineSubmissionJob", None) - submission_type = "deadline" - if not render_job: - # No deadline job. Try Muster: musterSubmissionJob + if instance.data.get("toBeRenderedOn") == "deadline": + render_job = data.pop("deadlineSubmissionJob", None) + submission_type = "deadline" + + if instance.data.get("toBeRenderedOn") == "muster": render_job = data.pop("musterSubmissionJob", None) submission_type = "muster" - assert render_job or instance.data.get("tileRendering") is False, ( - "Can't continue without valid Deadline " - "or Muster submission prior to this " - "plug-in." - ) + + if not render_job and instance.data.get("tileRendering") is False: + raise AssertionError(("Cannot continue without valid Deadline " + "or Muster submission.")) if submission_type == "deadline": self.DEADLINE_REST_URL = os.environ.get( diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 03b14f76bb..75567ae216 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -242,6 +242,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "resolutionWidth": cmds.getAttr("defaultResolution.width"), "resolutionHeight": cmds.getAttr("defaultResolution.height"), "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), + "tileRendering": render_instance.data.get("tileRendering") or False # noqa: E501 } # Apply each user defined attribute as data diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 3a6d12f623..5c13f6e62d 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -20,6 +20,7 @@ import os import json import getpass import copy +import re import clique import requests @@ -85,7 +86,8 @@ def get_renderer_variables(renderlayer, root): gin="#" * int(padding), lut=True, layer=renderlayer or lib.get_current_renderlayer())[0] - filename_0 = filename_0.replace('_', '_beauty') + filename_0 = re.sub('_', '_beauty', + filename_0, flags=re.IGNORECASE) prefix_attr = "defaultRenderGlobals.imageFilePrefix" if renderer == "vray": renderlayer = renderlayer.split("_")[-1] @@ -108,8 +110,8 @@ def get_renderer_variables(renderlayer, root): # does not work for vray. scene = cmds.file(query=True, sceneName=True) scene, _ = os.path.splitext(os.path.basename(scene)) - filename_0 = filename_prefix.replace('', scene) - filename_0 = filename_0.replace('', renderlayer) + filename_0 = re.sub('', scene, filename_prefix, flags=re.IGNORECASE) # noqa: E501 + filename_0 = re.sub('', renderlayer, filename_0, flags=re.IGNORECASE) # noqa: E501 filename_0 = "{}.{}.{}".format( filename_0, "#" * int(padding), extension) filename_0 = os.path.normpath(os.path.join(root, filename_0)) @@ -164,6 +166,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): def process(self, instance): """Plugin entry point.""" + instance.data["toBeRenderedOn"] = "deadline" self._instance = instance self._deadline_url = os.environ.get( "DEADLINE_REST_URL", "http://localhost:8082") @@ -172,6 +175,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): context = instance.context workspace = context.data["workspaceDir"] anatomy = context.data['anatomy'] + instance.data["toBeRenderedOn"] = "deadline" filepath = None @@ -402,6 +406,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): if not response.ok: raise Exception(response.text) instance.data["deadlineSubmissionJob"] = response.json() + else: + self.log.info("Skipping submission, tile rendering enabled.") # Store output dir for unified publisher (filesequence) instance.data["outputDir"] = os.path.dirname(output_filename_0) diff --git a/pype/plugins/maya/publish/submit_maya_muster.py b/pype/plugins/maya/publish/submit_maya_muster.py index 5a2e578793..ffe434048a 100644 --- a/pype/plugins/maya/publish/submit_maya_muster.py +++ b/pype/plugins/maya/publish/submit_maya_muster.py @@ -249,6 +249,7 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): Authenticate with Muster, collect all data, prepare path for post render publish job and submit job to farm. """ + instance.data["toBeRenderedOn"] = "muster" # setup muster environment self.MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL") diff --git a/pype/plugins/nuke/publish/submit_nuke_deadline.py b/pype/plugins/nuke/publish/submit_nuke_deadline.py index 26d3f9b571..2c7d468d3a 100644 --- a/pype/plugins/nuke/publish/submit_nuke_deadline.py +++ b/pype/plugins/nuke/publish/submit_nuke_deadline.py @@ -28,6 +28,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): deadline_chunk_size = 1 def process(self, instance): + instance.data["toBeRenderedOn"] = "deadline" families = instance.data["families"] node = instance[0] From 37b2188919d5744c2cf515962e0aab6be3cf0c58 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 29 Jul 2020 14:20:40 +0200 Subject: [PATCH 022/158] location_path is prepared for refilling the root key --- pype/modules/ftrack/actions/action_delivery.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index d4b86d1278..7ae7de65b1 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -293,6 +293,20 @@ class Delivery(BaseAction): repres_to_deliver.append(repre) anatomy = Anatomy(project_name) + + format_dict = {} + if location_path: + location_path = location_path.replace("\\", "/") + root_names = anatomy.root_names_from_templates( + anatomy.templates["delivery"] + ) + if root_names is None: + format_dict["root"] = location_path + else: + format_dict["root"] = {} + for name in root_names: + format_dict["root"][name] = location_path + for repre in repres_to_deliver: # Get destination repre path anatomy_data = copy.deepcopy(repre["context"]) From 7b7d4aa7a8fc59fef0d9523e5b3ae763a897056e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 29 Jul 2020 14:21:26 +0200 Subject: [PATCH 023/158] processing methods format path with entered location --- .../modules/ftrack/actions/action_delivery.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index 7ae7de65b1..a50603b2eb 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -368,10 +368,15 @@ class Delivery(BaseAction): return self.report() def process_single_file( - self, repre_path, anatomy, anatomy_name, anatomy_data + self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict ): anatomy_filled = anatomy.format(anatomy_data) - delivery_path = anatomy_filled["delivery"][anatomy_name] + if format_dict: + template_result = anatomy_filled["delivery"][anatomy_name] + delivery_path = template_result.rootless.format(**format_dict) + else: + delivery_path = anatomy_filled["delivery"][anatomy_name] + delivery_folder = os.path.dirname(delivery_path) if not os.path.exists(delivery_folder): os.makedirs(delivery_folder) @@ -379,7 +384,7 @@ class Delivery(BaseAction): self.copy_file(repre_path, delivery_path) def process_sequence( - self, repre_path, anatomy, anatomy_name, anatomy_data + self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict ): dir_path, file_name = os.path.split(str(repre_path)) @@ -422,8 +427,12 @@ class Delivery(BaseAction): anatomy_data["frame"] = frame_indicator anatomy_filled = anatomy.format(anatomy_data) - delivery_path = anatomy_filled["delivery"][anatomy_name] - print(delivery_path) + if format_dict: + template_result = anatomy_filled["delivery"][anatomy_name] + delivery_path = template_result.rootless.format(**format_dict) + else: + delivery_path = anatomy_filled["delivery"][anatomy_name] + delivery_folder = os.path.dirname(delivery_path) dst_head, dst_tail = delivery_path.split(frame_indicator) dst_padding = src_collection.padding From bb9e5ef4f90d00a8a86f26ea6d195f7a8c2aaee5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 29 Jul 2020 14:21:57 +0200 Subject: [PATCH 024/158] args for proces methods are pre-pared --- pype/modules/ftrack/actions/action_delivery.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index a50603b2eb..06257f32d5 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -353,15 +353,18 @@ class Delivery(BaseAction): repre_path = self.path_from_represenation(repre, anatomy) # TODO add backup solution where root of path from component # is repalced with root - if not frame: - self.process_single_file( - repre_path, anatomy, anatomy_name, anatomy_data - ) + args = ( + repre_path, + anatomy, + anatomy_name, + anatomy_data, + format_dict + ) + if not frame: + self.process_single_file(*args) else: - self.process_sequence( - repre_path, anatomy, anatomy_name, anatomy_data - ) + self.process_sequence(*args) self.db_con.uninstall() From e2b3f7496c40b30322f0467c9505dab6c8e4c394 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 29 Jul 2020 14:47:17 +0200 Subject: [PATCH 025/158] submiter validation, respect render priority --- .../global/publish/submit_publish_job.py | 59 ++++++++++++---- pype/plugins/maya/publish/collect_render.py | 3 +- .../maya/publish/submit_maya_deadline.py | 4 ++ .../validate_deadline_tile_submission.py | 69 +++++++++++++++++++ .../maya/publish/validate_frame_range.py | 6 ++ 5 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 pype/plugins/maya/publish/validate_deadline_tile_submission.py diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 21dcf93cdb..43edc33cba 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -232,7 +232,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): return (metadata_path, roothless_mtdt_p) - def _submit_deadline_post_job(self, instance, job): + def _submit_deadline_post_job(self, instance, job, instances): """Submit publish job to Deadline. Deadline specific code separated from :meth:`process` for sake of @@ -281,24 +281,28 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): and we are just waiting for them. """ if instance.data.get("tileRendering"): + self.log.info("Adding tile assembly results as dependencies...") asset_index = 0 - for represenation in instance.data.get("representations", []): - if isinstance(represenation["files"], [list, tuple]): - for file in represenation["files"]: + for inst in instances: + for represenation in inst.get("representations", []): + self.log.debug( + "working on {}".format(represenation["name"])) + if isinstance(represenation["files"], (list, tuple)): + for file in represenation["files"]: + self.log.debug("adding {}".format(file)) + dependency = os.path.join(output_dir, file) + payload["JobInfo"]["AssetDependency{}".format(asset_index)] = dependency # noqa: E501 + else: dependency = os.path.join(output_dir, file) payload["JobInfo"]["AssetDependency{}".format(asset_index)] = dependency # noqa: E501 - asset_index += 1 - else: - dependency = os.path.join(output_dir, file) - payload["JobInfo"]["AssetDependency0"] = dependency + asset_index += 1 else: - payload["JobInfo"]["JobDependency0"] = job["_id"], + payload["JobInfo"]["JobDependency0"] = job["_id"] # Transfer the environment from the original job to this dependent # job so they use the same environment metadata_path, roothless_metadata_path = self._create_metadata_path( instance) - environment = job["Props"].get("Env", {}) environment["PYPE_METADATA_FILE"] = roothless_metadata_path environment["AVALON_PROJECT"] = io.Session["AVALON_PROJECT"] @@ -847,14 +851,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): r''' SUBMiT PUBLiSH JOB 2 D34DLiN3 ____ - ' ' .---. .---. .--. .---. .--..--..--..--. .---. - | | --= \ | . \/ _|/ \| . \ || || \ |/ _| - | JOB | --= / | | || __| .. | | | |;_ || \ || __| - | | |____./ \.__|._||_.|___./|_____|||__|\__|\.___| + ' ' .---. .---. .--. .---. .--..--..--..--. .---. + | | --= \ | . \/ _|/ \| . \ || || \ |/ _| + | JOB | --= / | | || __| .. | | | |;_ || \ || __| + | | |____./ \.__|._||_.|___./|_____|||__|\__|\.___| ._____. ''' + render_job = None if instance.data.get("toBeRenderedOn") == "deadline": render_job = data.pop("deadlineSubmissionJob", None) submission_type = "deadline" @@ -867,13 +872,37 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): raise AssertionError(("Cannot continue without valid Deadline " "or Muster submission.")) + if not render_job: + import getpass + + render_job = {} + self.log.info("Faking job data ...") + render_job["Props"] = {} + # Render job doesn't exist because we do not have prior submission. + # We still use data from it so lets fake it. + # + # Batch name reflect original scene name + render_job["Props"]["Batch"] = os.path.splitext(os.path.basename( + context.data.get("currentFile")))[0] + # User is deadline user + render_job["Props"]["User"] = context.data.get( + "deadlineUser", getpass.getuser()) + # Priority is now not handled at all + render_job["Props"]["Pri"] = instance.data.get("priority") + + render_job["Props"]["Env"] = { + "FTRACK_API_USER": os.environ.get("FTRACK_API_USER"), + "FTRACK_API_KEY": os.environ.get("FTRACK_API_KEY"), + "FTRACK_SERVER": os.environ.get("FTRACK_SERVER"), + } + if submission_type == "deadline": self.DEADLINE_REST_URL = os.environ.get( "DEADLINE_REST_URL", "http://localhost:8082" ) assert self.DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" - self._submit_deadline_post_job(instance, render_job) + self._submit_deadline_post_job(instance, render_job, instances) # publish job file publish_job = { diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 75567ae216..5ca9392080 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -242,7 +242,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "resolutionWidth": cmds.getAttr("defaultResolution.width"), "resolutionHeight": cmds.getAttr("defaultResolution.height"), "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), - "tileRendering": render_instance.data.get("tileRendering") or False # noqa: E501 + "tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501 + "priority": render_instance.data.get("priority") } # Apply each user defined attribute as data diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 5c13f6e62d..5840a7b946 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -45,6 +45,7 @@ payload_skeleton = { "Plugin": "MayaPype", "Frames": "{start}-{end}x{step}", "Comment": None, + "Priority": 50, }, "PluginInfo": { "SceneFile": None, # Input @@ -302,6 +303,9 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): payload_skeleton["JobInfo"]["Name"] = jobname # Arbitrary username, for visualisation in Monitor payload_skeleton["JobInfo"]["UserName"] = deadline_user + # Set job priority + payload_skeleton["JobInfo"]["Priority"] = self._instance.data.get( + "priority", 50) # Optional, enable double-click to preview rendered # frames from Deadline Monitor payload_skeleton["JobInfo"]["OutputDirectory0"] = \ diff --git a/pype/plugins/maya/publish/validate_deadline_tile_submission.py b/pype/plugins/maya/publish/validate_deadline_tile_submission.py new file mode 100644 index 0000000000..b0b995de3e --- /dev/null +++ b/pype/plugins/maya/publish/validate_deadline_tile_submission.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +"""Validate settings from Deadline Submitter. + +This is useful mainly for tile rendering, where jobs on farm are created by +submitter script from Maya. + +Unfortunately Deadline doesn't expose frame number for tiles job so that +cannot be validated, even if it is important setting. Also we cannot +determine if 'Region Rendering' (tile rendering) is enabled or not because +of the same thing. + +""" +import os + +from maya import mel +from maya import cmds + +import pyblish.api +from pype.hosts.maya import lib + + +class ValidateDeadlineTileSubmission(pyblish.api.InstancePlugin): + """Validate Deadline Submission settings are OK for tile rendering.""" + + label = "Validate Deadline Tile Submission" + order = pyblish.api.ValidatorOrder + hosts = ["maya"] + families = ["renderlayer"] + if not os.environ.get("DEADLINE_REST_URL"): + active = False + + def process(self, instance): + """Entry point.""" + # try if Deadline submitter was loaded + if mel.eval("exists SubmitJobToDeadline") == 0: + # if not, try to load it manually + try: + mel.eval("source DeadlineMayaClient;") + except RuntimeError: + raise AssertionError("Deadline Maya client cannot be loaded") + mel.eval("DeadlineMayaClient();") + assert mel.eval("exists SubmitJobToDeadline") == 1, ( + "Deadline Submission script cannot be initialized.") + if instance.data.get("tileRendering"): + job_name = cmds.getAttr("defaultRenderGlobals.deadlineJobName") + scene_name = os.path.splitext(os.path.basename( + instance.context.data.get("currentFile")))[0] + if job_name != scene_name: + self.log.warning(("Job submitted through Deadline submitter " + "has different name then current scene " + "{} / {}").format(job_name, scene_name)) + if cmds.getAttr("defaultRenderGlobals.deadlineTileSingleJob") == 1: + layer = instance.data['setMembers'] + anim_override = lib.get_attr_in_layer( + "defaultRenderGlobals.animation", layer=layer) + assert anim_override, ( + "Animation must be enabled in " + "Render Settings even when rendering single frame." + ) + + start_frame = cmds.getAttr("defaultRenderGlobals.startFrame") + end_frame = cmds.getAttr("defaultRenderGlobals.endFrame") + assert start_frame == end_frame, ( + "Start frame and end frame are not equals. When " + "'Submit All Tles As A Single Job' is selected, only " + "single frame is expected to be rendered. It must match " + "the one specified in Deadline Submitter under " + "'Region Rendering'" + ) diff --git a/pype/plugins/maya/publish/validate_frame_range.py b/pype/plugins/maya/publish/validate_frame_range.py index 0d51a83cf5..1ee6e2bd25 100644 --- a/pype/plugins/maya/publish/validate_frame_range.py +++ b/pype/plugins/maya/publish/validate_frame_range.py @@ -29,6 +29,12 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): def process(self, instance): context = instance.context + if instance.data.get("tileRendering"): + self.log.info(( + "Skipping frame range validation because " + "tile rendering is enabled." + )) + return frame_start_handle = int(context.data.get("frameStartHandle")) frame_end_handle = int(context.data.get("frameEndHandle")) From a39f975a8fa8ea6c75ebaf38aba2999d6d1f81fe Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 29 Jul 2020 14:53:43 +0200 Subject: [PATCH 026/158] removed debug prints, fixed single file handling --- pype/plugins/global/publish/submit_publish_job.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 4ac7d5d73a..b6d62a8fd1 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -285,15 +285,13 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): asset_index = 0 for inst in instances: for represenation in inst.get("representations", []): - self.log.debug( - "working on {}".format(represenation["name"])) if isinstance(represenation["files"], (list, tuple)): for file in represenation["files"]: - self.log.debug("adding {}".format(file)) dependency = os.path.join(output_dir, file) payload["JobInfo"]["AssetDependency{}".format(asset_index)] = dependency # noqa: E501 else: - dependency = os.path.join(output_dir, file) + dependency = os.path.join( + output_dir, represenation["files"]) payload["JobInfo"]["AssetDependency{}".format(asset_index)] = dependency # noqa: E501 asset_index += 1 else: From 3770d642e92568515fe1c9cfb6790172c3a6e6a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Aug 2020 10:57:49 +0200 Subject: [PATCH 027/158] use only keys in delivery with `{root` in value --- pype/modules/ftrack/actions/action_delivery.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index 06257f32d5..ce02f2054d 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -81,13 +81,15 @@ class Delivery(BaseAction): anatomy = Anatomy(project_name) new_anatomies = [] first = None - for key in (anatomy.templates.get("delivery") or {}): - new_anatomies.append({ - "label": key, - "value": key - }) - if first is None: - first = key + for key, template in (anatomy.templates.get("delivery") or {}).items(): + # Use only keys with `{root}` or `{root[*]}` in value + if "{root" in template: + new_anatomies.append({ + "label": key, + "value": key + }) + if first is None: + first = key skipped = False # Add message if there are any common components From def7fa7e08447a48cf76d3943b73e6c01121ba8e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Aug 2020 11:06:16 +0200 Subject: [PATCH 028/158] added check for template value that it is a string --- pype/modules/ftrack/actions/action_delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index ce02f2054d..231aebdf7a 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -83,7 +83,7 @@ class Delivery(BaseAction): first = None for key, template in (anatomy.templates.get("delivery") or {}).items(): # Use only keys with `{root}` or `{root[*]}` in value - if "{root" in template: + if isinstance(template, str) and "{root" in template: new_anatomies.append({ "label": key, "value": key From 5724f3e474cf59254d2d15c049adc761d300606b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 7 Aug 2020 12:18:54 +0200 Subject: [PATCH 029/158] using -g 1 (same as -intra) to set right keyframes of burning output --- pype/scripts/otio_burnin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py index 104ff0255c..16e24757dd 100644 --- a/pype/scripts/otio_burnin.py +++ b/pype/scripts/otio_burnin.py @@ -528,6 +528,9 @@ def burnins_from_data( if pix_fmt: ffmpeg_args.append("-pix_fmt {}".format(pix_fmt)) + # Use group one (same as `-intra` argument, which is deprecated) + ffmpeg_args.append("-g 1") + ffmpeg_args_str = " ".join(ffmpeg_args) burnin.render( output_path, args=ffmpeg_args_str, overwrite=overwrite, **data From 23caec0a76d10b660f1a11d462e825d6f2163feb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 7 Aug 2020 13:45:32 +0200 Subject: [PATCH 030/158] fix case where staging dir is not set --- pype/plugins/global/publish/cleanup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/cleanup.py b/pype/plugins/global/publish/cleanup.py index bca540078f..e891b7b7f6 100644 --- a/pype/plugins/global/publish/cleanup.py +++ b/pype/plugins/global/publish/cleanup.py @@ -71,12 +71,16 @@ class CleanUp(pyblish.api.InstancePlugin): temp_root = tempfile.gettempdir() staging_dir = instance.data.get("stagingDir", None) + if not staging_dir: + self.log.info("Staging dir not set.") + return + if not os.path.normpath(staging_dir).startswith(temp_root): self.log.info("Skipping cleanup. Staging directory is not in the " "temp folder: %s" % staging_dir) return - if not staging_dir or not os.path.exists(staging_dir): + if not os.path.exists(staging_dir): self.log.info("No staging directory found: %s" % staging_dir) return From 4b291a286a2317b926577bbdfb22010e66f14044 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 7 Aug 2020 17:17:36 +0200 Subject: [PATCH 031/158] fix collection assembly and extension determination --- pype/plugins/global/publish/submit_publish_job.py | 2 +- pype/plugins/maya/publish/submit_maya_deadline.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index b6d62a8fd1..dd9e84aade 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -441,7 +441,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # but we really expect only one collection. # Nothing else make sense. assert len(cols) == 1, "only one image sequence type is expected" # noqa: E501 - _, ext = os.path.splitext(cols[0].tail) + ext = cols[0].tail.lstrip(".") col = list(cols[0]) self.log.debug(col) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 74185164b1..7fe20c779d 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -393,7 +393,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): payload['JobInfo']['OutputFilename' + str(exp_index)] = rem[0] # noqa: E501 output_file = rem[0] else: - output_file = col.format('{head}{padding}{tail}') + output_file = col[0].format('{head}{padding}{tail}') payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 output_filenames[exp_index] = output_file exp_index += 1 @@ -407,7 +407,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "with them.") payload['JobInfo']['OutputFilename' + str(exp_index)] = rem[0] # noqa: E501 else: - output_file = col.format('{head}{padding}{tail}') + output_file = col[0].format('{head}{padding}{tail}') payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 plugin = payload["JobInfo"]["Plugin"] From 63464d14b4449502f1b8df522bd699746cec961c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Aug 2020 13:22:18 +0200 Subject: [PATCH 032/158] splitted next_task_update into more parts --- .../ftrack/events/event_next_task_update.py | 249 +++++++++++++----- 1 file changed, 183 insertions(+), 66 deletions(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index dc1ab0a0d7..df2f3cd6ed 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -1,92 +1,209 @@ -import ftrack_api -from pype.modules.ftrack import BaseEvent import operator +import collections +from pype.modules.ftrack import BaseEvent class NextTaskUpdate(BaseEvent): + def filter_entities_info(self, event): + # Filter if event contain relevant data + entities_info = event["data"].get("entities") + if not entities_info: + return - def get_next_task(self, task, session): - parent = task['parent'] - # tasks = parent['tasks'] - tasks = parent['children'] + first_filtered_entities = [] + for entity_info in entities_info: + # Care only about tasks + if entity_info.get("entityType") != "task": + continue - def sort_types(types): - data = {} - for t in types: - data[t] = t.get('sort') + # Care only about changes of status + changes = entity_info.get("changes") or {} + statusid_changes = changes.get("statusid") or {} + if ( + statusid_changes.get("new") is None + or statusid_changes.get("old") is None + ): + continue - data = sorted(data.items(), key=operator.itemgetter(1)) - results = [] - for item in data: - results.append(item[0]) - return results + first_filtered_entities.append(entity_info) - types_sorted = sort_types(session.query('Type')) - next_types = None - for t in types_sorted: - if t['id'] == task['type_id']: - next_types = types_sorted[(types_sorted.index(t) + 1):] + status_ids = [ + entity_info["changes"]["statusid"]["new"] + for entity_info in first_filtered_entities + ] + statuses_by_id = self.get_statuses_by_id(status_ids=status_ids) - for nt in next_types: - for t in tasks: - if nt['id'] == t['type_id']: - return t + # Care only about tasks having status with state `Done` + filtered_entities = [] + for entity_info in first_filtered_entities: + status_id = entity_info["changes"]["statusid"]["new"] + status_entity = statuses_by_id[status_id] + if status_entity["state"]["name"].lower() == "done": + filtered_entities.append(entities_info) - return None + return filtered_entities + + def get_parents_by_id(self, session, entities_info): + parent_ids = [ + "\"{}\"".format(entity_info["parentId"]) + for entity_info in entities_info + ] + parent_entities = session.query( + "TypedContext where id in ({})".format(", ".join(parent_ids)) + ).all() + + return { + entity["id"]: entity + for entity in parent_entities + } + + def get_tasks_by_id(self, session, parent_ids): + joined_parent_ids = ",".join([ + "\"{}\"".format(parent_id) + for parent_id in parent_ids + ]) + task_entities = session.query( + "Task where parent_id in ({})".format(", ".join(joined_parent_ids)) + ).all() + + return { + entity["id"]: entity + for entity in task_entities + } + + def get_statuses_by_id(self, session, task_entities=None, status_ids=None): + if task_entities is None and status_ids is None: + return {} + + if status_ids is None: + status_ids = [] + for task_entity in task_entities: + status_ids.append(task_entities["status_id"]) + + status_entities = session.query( + "Status where id in ({})".format(", ".join(status_ids)) + ).all() + + return { + entity["id"]: entity + for entity in status_entities + } + + def get_sorted_task_types(self, session): + data = { + _type: _type.get("sort") + for _type in session.query("Type").all() + if _type.get("sort") is not None + } + + return [ + item[0] + for item in sorted(data.items(), key=operator.itemgetter(1)) + ] def launch(self, session, event): '''Propagates status from version to task when changed''' - # self.log.info(event) - # start of event procedure ---------------------------------- + entities_info = self.filter_entities_info(event) + if not entities_info: + return - for entity in event['data'].get('entities', []): - changes = entity.get('changes', None) - if changes is None: - continue - statusid_changes = changes.get('statusid', {}) - if ( - entity['entityType'] != 'task' or - 'statusid' not in (entity.get('keys') or []) or - statusid_changes.get('new', None) is None or - statusid_changes.get('old', None) is None - ): + parents_by_id = self.get_parents_by_id(session, entities_info) + tasks_by_id = self.get_tasks_by_id( + session, tuple(parents_by_id.keys()) + ) + + tasks_to_parent_id = collections.defaultdict(list) + for task_entity in tasks_by_id.values(): + tasks_to_parent_id[task_entity["parent_id"]].append(task_entity) + + statuses_by_id = self.get_statuses_by_id(session, tasks_by_id.values()) + + # Prepare all task types + sorted_task_types = self.get_sorted_task_types(session) + sorted_task_types_len = len(sorted_task_types) + + next_status_name = "Ready" + next_status = session.query( + "Status where name is \"{}\"".format(next_status_name) + ).first() + if not next_status: + self.log.warning("Couldn't find status with name \"{}\"".format( + next_status_name + )) + return + + for entity_info in entities_info: + parent_id = entities_info["parentId"] + task_id = entity_info["entityId"] + task_entity = tasks_by_id[task_id] + + all_same_type_taks_done = True + for parents_task in tasks_to_parent_id[parent_id]: + if ( + parents_task["id"] == task_id + or parents_task["type_id"] != task_entity["type_id"] + ): + continue + + parents_task_status = statuses_by_id[parents_task["status_id"]] + if parents_task_status["state"]["name"].lower() != "done": + all_same_type_taks_done = False + break + + if not all_same_type_taks_done: continue - task = session.get('Task', entity['entityId']) + from_idx = None + for idx, task_type in enumerate(sorted_task_types): + if task_type["id"] == task_entity["type_id"]: + from_idx = idx + 1 + break - status = session.get('Status', - entity['changes']['statusid']['new']) - state = status['state']['name'] + # Current task type is last in order + if from_idx >= sorted_task_types_len: + continue - next_task = self.get_next_task(task, session) + next_task_type_id = None + next_task_type_tasks = [] + for idx in range(from_idx, sorted_task_types_len): + next_task_type = sorted_task_types[idx] + for parents_task in tasks_to_parent_id[parent_id]: + if next_task_type_id is None: + if parents_task["type_id"] != next_task_type["id"]: + continue + next_task_type_id = next_task_type["id"] - # Setting next task to Ready, if on NOT READY - if next_task and state == 'Done': - if next_task['status']['name'].lower() == 'not ready': + if parents_task["type_id"] == next_task_type_id: + next_task_type_tasks.append(parents_task) - # Get path to task - path = task['name'] - for p in task['ancestors']: - path = p['name'] + '/' + path + if next_task_type_id is not None: + break - # Setting next task status - try: - query = 'Status where name is "{}"'.format('Ready') - status_to_set = session.query(query).one() - next_task['status'] = status_to_set - session.commit() - self.log.info(( - '>>> [ {} ] updated to [ Ready ]' - ).format(path)) - except Exception as e: - session.rollback() - self.log.warning(( - '!!! [ {} ] status couldnt be set: [ {} ]' - ).format(path, str(e)), exc_info=True) + for next_task_entity in next_task_type_tasks: + if next_task_entity["status"]["name"].lower() != "not ready": + continue + + ent_path = "/".join( + [ent["name"] for ent in next_task_entity["link"]] + ) + try: + next_task_entity["status"] = next_status + session.commit() + self.log.info( + "\"{}\" updated status to \"{}\"".format( + ent_path, next_status_name + ) + ) + except Exception: + session.rollback() + self.log.warning( + "\"{}\" status couldnt be set to \"{}\"".format( + ent_path, next_status_name + ), + exc_info=True + ) def register(session, plugins_presets): - '''Register plugin. Called when used as an plugin.''' - NextTaskUpdate(session, plugins_presets).register() From 151631596ad7170e24388360096cf5bbab993160 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Aug 2020 13:23:29 +0200 Subject: [PATCH 033/158] minor fixes --- .../ftrack/events/event_next_task_update.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index df2f3cd6ed..57315a1fed 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -4,7 +4,7 @@ from pype.modules.ftrack import BaseEvent class NextTaskUpdate(BaseEvent): - def filter_entities_info(self, event): + def filter_entities_info(self, session, event): # Filter if event contain relevant data entities_info = event["data"].get("entities") if not entities_info: @@ -31,7 +31,9 @@ class NextTaskUpdate(BaseEvent): entity_info["changes"]["statusid"]["new"] for entity_info in first_filtered_entities ] - statuses_by_id = self.get_statuses_by_id(status_ids=status_ids) + statuses_by_id = self.get_statuses_by_id( + session, status_ids=status_ids + ) # Care only about tasks having status with state `Done` filtered_entities = [] @@ -39,7 +41,7 @@ class NextTaskUpdate(BaseEvent): status_id = entity_info["changes"]["statusid"]["new"] status_entity = statuses_by_id[status_id] if status_entity["state"]["name"].lower() == "done": - filtered_entities.append(entities_info) + filtered_entities.append(entity_info) return filtered_entities @@ -63,7 +65,7 @@ class NextTaskUpdate(BaseEvent): for parent_id in parent_ids ]) task_entities = session.query( - "Task where parent_id in ({})".format(", ".join(joined_parent_ids)) + "Task where parent_id in ({})".format(joined_parent_ids) ).all() return { @@ -78,7 +80,10 @@ class NextTaskUpdate(BaseEvent): if status_ids is None: status_ids = [] for task_entity in task_entities: - status_ids.append(task_entities["status_id"]) + status_ids.append(task_entity["status_id"]) + + if not status_ids: + return {} status_entities = session.query( "Status where id in ({})".format(", ".join(status_ids)) @@ -104,7 +109,7 @@ class NextTaskUpdate(BaseEvent): def launch(self, session, event): '''Propagates status from version to task when changed''' - entities_info = self.filter_entities_info(event) + entities_info = self.filter_entities_info(session, event) if not entities_info: return @@ -134,7 +139,7 @@ class NextTaskUpdate(BaseEvent): return for entity_info in entities_info: - parent_id = entities_info["parentId"] + parent_id = entity_info["parentId"] task_id = entity_info["entityId"] task_entity = tasks_by_id[task_id] From 0c67a85ce7b1558d2d236d2f3023023668282dc1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Aug 2020 14:28:58 +0200 Subject: [PATCH 034/158] moved few steps --- pype/modules/ftrack/events/event_next_task_update.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index 57315a1fed..0f84ed4b44 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -124,10 +124,6 @@ class NextTaskUpdate(BaseEvent): statuses_by_id = self.get_statuses_by_id(session, tasks_by_id.values()) - # Prepare all task types - sorted_task_types = self.get_sorted_task_types(session) - sorted_task_types_len = len(sorted_task_types) - next_status_name = "Ready" next_status = session.query( "Status where name is \"{}\"".format(next_status_name) @@ -159,6 +155,10 @@ class NextTaskUpdate(BaseEvent): if not all_same_type_taks_done: continue + # Prepare all task types + sorted_task_types = self.get_sorted_task_types(session) + sorted_task_types_len = len(sorted_task_types) + from_idx = None for idx, task_type in enumerate(sorted_task_types): if task_type["id"] == task_entity["type_id"]: @@ -166,7 +166,7 @@ class NextTaskUpdate(BaseEvent): break # Current task type is last in order - if from_idx >= sorted_task_types_len: + if from_idx is None or from_idx >= sorted_task_types_len: continue next_task_type_id = None From ca165f6139d72938983ac4f74bf601996a1853ae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Aug 2020 16:25:23 +0200 Subject: [PATCH 035/158] blocked statuses are ignored when done statuses are checked --- pype/modules/ftrack/events/event_next_task_update.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index 0f84ed4b44..2df3800d8a 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -148,7 +148,12 @@ class NextTaskUpdate(BaseEvent): continue parents_task_status = statuses_by_id[parents_task["status_id"]] - if parents_task_status["state"]["name"].lower() != "done": + low_state_name = parents_task_status["state"]["name"].lower() + # Skip if task's status is in blocked state (e.g. Omitted) + if low_state_name != "blocked": + continue + + if low_state_name != "done": all_same_type_taks_done = False break From a27eab6049c3cb910e53eda2eb0de48249ffb653 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Aug 2020 21:41:33 +0200 Subject: [PATCH 036/158] init commit --- pype/tools/launcher/__init__.py | 10 + pype/tools/launcher/__main__.py | 5 + pype/tools/launcher/actions.py | 95 ++++ pype/tools/launcher/app.py | 717 ++++++++++++++++++++++++++++++ pype/tools/launcher/flickcharm.py | 305 +++++++++++++ pype/tools/launcher/lib.py | 67 +++ pype/tools/launcher/models.py | 292 ++++++++++++ pype/tools/launcher/widgets.py | 419 +++++++++++++++++ 8 files changed, 1910 insertions(+) create mode 100644 pype/tools/launcher/__init__.py create mode 100644 pype/tools/launcher/__main__.py create mode 100644 pype/tools/launcher/actions.py create mode 100644 pype/tools/launcher/app.py create mode 100644 pype/tools/launcher/flickcharm.py create mode 100644 pype/tools/launcher/lib.py create mode 100644 pype/tools/launcher/models.py create mode 100644 pype/tools/launcher/widgets.py diff --git a/pype/tools/launcher/__init__.py b/pype/tools/launcher/__init__.py new file mode 100644 index 0000000000..3b88ebe984 --- /dev/null +++ b/pype/tools/launcher/__init__.py @@ -0,0 +1,10 @@ + +from .app import ( + show, + cli +) + +__all__ = [ + "show", + "cli", +] diff --git a/pype/tools/launcher/__main__.py b/pype/tools/launcher/__main__.py new file mode 100644 index 0000000000..50642c46cd --- /dev/null +++ b/pype/tools/launcher/__main__.py @@ -0,0 +1,5 @@ +from app import cli + +if __name__ == '__main__': + import sys + sys.exit(cli(sys.argv[1:])) diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py new file mode 100644 index 0000000000..2a2e2ab0f0 --- /dev/null +++ b/pype/tools/launcher/actions.py @@ -0,0 +1,95 @@ +import os +import importlib + +from avalon import api, lib + + +class ProjectManagerAction(api.Action): + name = "projectmanager" + label = "Project Manager" + icon = "gear" + group = "Test" + order = 999 # at the end + + def is_compatible(self, session): + return "AVALON_PROJECT" in session + + def process(self, session, **kwargs): + return lib.launch(executable="python", + args=["-u", "-m", "avalon.tools.projectmanager", + session['AVALON_PROJECT']]) + + +class LoaderAction(api.Action): + name = "loader" + label = "Loader" + icon = "cloud-download" + order = 998 # at the end + group = "Test" + + def is_compatible(self, session): + return "AVALON_PROJECT" in session + + def process(self, session, **kwargs): + return lib.launch(executable="python", + args=["-u", "-m", "avalon.tools.cbloader", + session['AVALON_PROJECT']]) + + +class LoaderLibrary(api.Action): + name = "loader_os" + label = "Library Loader" + icon = "book" + order = 997 # at the end + + def is_compatible(self, session): + return True + + def process(self, session, **kwargs): + return lib.launch(executable="python", + args=["-u", "-m", "avalon.tools.libraryloader"]) + + +def register_default_actions(): + """Register default actions for Launcher""" + api.register_plugin(api.Action, ProjectManagerAction) + api.register_plugin(api.Action, LoaderAction) + api.register_plugin(api.Action, LoaderLibrary) + + +def register_config_actions(): + """Register actions from the configuration for Launcher""" + + module_name = os.environ["AVALON_CONFIG"] + config = importlib.import_module(module_name) + if not hasattr(config, "register_launcher_actions"): + print("Current configuration `%s` has no 'register_launcher_actions'" + % config.__name__) + return + + config.register_launcher_actions() + + +def register_environment_actions(): + """Register actions from AVALON_ACTIONS for Launcher.""" + + paths = os.environ.get("AVALON_ACTIONS") + if not paths: + return + + for path in paths.split(os.pathsep): + api.register_plugin_path(api.Action, path) + + # Run "register" if found. + for module in lib.modules_from_path(path): + if "register" not in dir(module): + continue + + try: + module.register() + except Exception as e: + print( + "Register method in {0} failed: {1}".format( + module, str(e) + ) + ) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py new file mode 100644 index 0000000000..8bce705fc9 --- /dev/null +++ b/pype/tools/launcher/app.py @@ -0,0 +1,717 @@ +import sys +import copy + +from avalon.vendor.Qt import QtWidgets, QtCore, QtGui +from avalon import io, api, style + +from avalon.tools import lib as tools_lib +from avalon.tools.widgets import AssetWidget +from avalon.vendor import qtawesome +from .models import ProjectModel +from .widgets import ( + ProjectBar, ActionBar, TasksWidget, ActionHistory, SlidePageWidget +) + +from .flickcharm import FlickCharm + +module = sys.modules[__name__] +module.window = None + + +class IconListView(QtWidgets.QListView): + """Styled ListView that allows to toggle between icon and list mode. + + Toggling between the two modes is done by Right Mouse Click. + + """ + + IconMode = 0 + ListMode = 1 + + def __init__(self, parent=None, mode=ListMode): + super(IconListView, self).__init__(parent=parent) + + # Workaround for scrolling being super slow or fast when + # toggling between the two visual modes + self.setVerticalScrollMode(self.ScrollPerPixel) + + self._mode = 0 + self.set_mode(mode) + + def set_mode(self, mode): + if mode == self.IconMode: + self.setViewMode(QtWidgets.QListView.IconMode) + self.setResizeMode(QtWidgets.QListView.Adjust) + self.setWrapping(True) + self.setWordWrap(True) + self.setGridSize(QtCore.QSize(151, 90)) + self.setIconSize(QtCore.QSize(50, 50)) + self.setSpacing(0) + self.setAlternatingRowColors(False) + + self.setStyleSheet(""" + QListView { + font-size: 11px; + border: 0px; + padding: 0px; + margin: 0px; + + } + + QListView::item { + margin-top: 6px; + /* Won't work without borders set */ + border: 0px; + } + + /* For icon only */ + QListView::icon { + top: 3px; + } + """) + + self.verticalScrollBar().setSingleStep(30) + + elif self.ListMode: + self.setStyleSheet("") # clear stylesheet + self.setViewMode(QtWidgets.QListView.ListMode) + self.setResizeMode(QtWidgets.QListView.Adjust) + self.setWrapping(False) + self.setWordWrap(False) + self.setIconSize(QtCore.QSize(20, 20)) + self.setGridSize(QtCore.QSize(100, 25)) + self.setSpacing(0) + self.setAlternatingRowColors(False) + + self.verticalScrollBar().setSingleStep(33.33) + + self._mode = mode + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + self.set_mode(int(not self._mode)) + return super(IconListView, self).mousePressEvent(event) + + +class ProjectsPanel(QtWidgets.QWidget): + """Projects Page""" + + project_clicked = QtCore.Signal(str) + + def __init__(self, parent=None): + super(ProjectsPanel, self).__init__(parent=parent) + + layout = QtWidgets.QVBoxLayout(self) + + io.install() + view = IconListView(parent=self) + view.setSelectionMode(QtWidgets.QListView.NoSelection) + flick = FlickCharm(parent=self) + flick.activateOn(view) + model = ProjectModel() + model.hide_invisible = True + model.refresh() + view.setModel(model) + + layout.addWidget(view) + + view.clicked.connect(self.on_clicked) + + self.model = model + self.view = view + + def on_clicked(self, index): + if index.isValid(): + project = index.data(QtCore.Qt.DisplayRole) + self.project_clicked.emit(project) + + +class AssetsPanel(QtWidgets.QWidget): + """Assets page""" + + back_clicked = QtCore.Signal() + + def __init__(self, parent=None): + super(AssetsPanel, self).__init__(parent=parent) + + # project bar + project_bar = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(project_bar) + layout.setSpacing(4) + back = QtWidgets.QPushButton("<") + back.setFixedWidth(25) + back.setFixedHeight(23) + projects = ProjectBar() + projects.layout().setContentsMargins(0, 0, 0, 0) + layout.addWidget(back) + layout.addWidget(projects) + + # assets + _assets_widgets = QtWidgets.QWidget() + _assets_widgets.setContentsMargins(0, 0, 0, 0) + assets_layout = QtWidgets.QVBoxLayout(_assets_widgets) + assets_widgets = AssetWidget() + + # Make assets view flickable + flick = FlickCharm(parent=self) + flick.activateOn(assets_widgets.view) + assets_widgets.view.setVerticalScrollMode( + assets_widgets.view.ScrollPerPixel + ) + assets_layout.addWidget(assets_widgets) + + # tasks + tasks_widgets = TasksWidget() + 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(tasks_widgets) + body.setStretchFactor(0, 100) + body.setStretchFactor(1, 65) + + # main layout + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(project_bar) + layout.addWidget(body) + + self.data = { + "model": { + "projects": projects, + "assets": assets_widgets, + "tasks": tasks_widgets + }, + } + + # signals + projects.project_changed.connect(self.on_project_changed) + assets_widgets.selection_changed.connect(self.asset_changed) + back.clicked.connect(self.back_clicked) + + # Force initial refresh for the assets since we might not be + # trigging a Project switch if we click the project that was set + # prior to launching the Launcher + # todo: remove this behavior when AVALON_PROJECT is not required + assets_widgets.refresh() + + def set_project(self, project): + + projects = self.data["model"]["projects"] + + before = projects.get_current_project() + projects.set_project(project) + if project == before: + # Force a refresh on the assets if the project hasn't changed + self.data["model"]["assets"].refresh() + + def asset_changed(self): + tools_lib.schedule(self.on_asset_changed, 0.05, + channel="assets") + + def on_project_changed(self): + + project = self.data["model"]["projects"].get_current_project() + + api.Session["AVALON_PROJECT"] = project + self.data["model"]["assets"].refresh() + + # Force asset change callback to ensure tasks are correctly reset + self.asset_changed() + + def on_asset_changed(self): + """Callback on asset selection changed + + This updates the task view. + + """ + + print("Asset changed..") + + tasks = self.data["model"]["tasks"] + assets = self.data["model"]["assets"] + + asset = assets.get_active_asset_document() + if asset: + tasks.set_asset(asset["_id"]) + else: + tasks.set_asset(None) + + def _get_current_session(self): + + tasks = self.data["model"]["tasks"] + assets = self.data["model"]["assets"] + + asset = assets.get_active_asset_document() + session = copy.deepcopy(api.Session) + + # Clear some values that we are about to collect if available + session.pop("AVALON_SILO", None) + session.pop("AVALON_ASSET", None) + session.pop("AVALON_TASK", None) + + if asset: + session["AVALON_ASSET"] = asset["name"] + + silo = asset.get("silo") + if silo: + session["AVALON_SILO"] = silo + + task = tasks.get_current_task() + if task: + session["AVALON_TASK"] = task + + return session + + +class Window(QtWidgets.QDialog): + """Launcher interface""" + + def __init__(self, parent=None): + super(Window, self).__init__(parent) + + self.setWindowTitle("Launcher") + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + # Allow minimize + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint + ) + + project_panel = ProjectsPanel() + asset_panel = AssetsPanel() + + pages = SlidePageWidget() + pages.addWidget(project_panel) + pages.addWidget(asset_panel) + + # actions + actions = ActionBar() + + # statusbar + statusbar = QtWidgets.QWidget() + message = QtWidgets.QLabel() + message.setFixedHeight(15) + action_history = ActionHistory() + action_history.setStatusTip("Show Action History") + layout = QtWidgets.QHBoxLayout(statusbar) + layout.addWidget(message) + layout.addWidget(action_history) + + # Vertically split Pages and Actions + body = QtWidgets.QSplitter() + body.setContentsMargins(0, 0, 0, 0) + body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + body.setOrientation(QtCore.Qt.Vertical) + body.addWidget(pages) + body.addWidget(actions) + + # Set useful default sizes and set stretch + # for the pages so that is the only one that + # stretches on UI resize. + body.setStretchFactor(0, 10) + body.setSizes([580, 160]) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(body) + layout.addWidget(statusbar) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + + self.data = { + "label": { + "message": message, + }, + "pages": { + "project": project_panel, + "asset": asset_panel + }, + "model": { + "actions": actions, + "action_history": action_history + }, + } + + self.pages = pages + self._page = 0 + + # signals + actions.action_clicked.connect(self.on_action_clicked) + action_history.trigger_history.connect(self.on_history_action) + project_panel.project_clicked.connect(self.on_project_clicked) + asset_panel.back_clicked.connect(self.on_back_clicked) + + # Add some signals to propagate from the asset panel + for signal in [ + asset_panel.data["model"]["projects"].project_changed, + asset_panel.data["model"]["assets"].selection_changed, + asset_panel.data["model"]["tasks"].task_changed + ]: + signal.connect(self.on_session_changed) + + # todo: Simplify this callback connection + asset_panel.data["model"]["projects"].project_changed.connect( + self.on_project_changed + ) + + self.resize(520, 740) + + def set_page(self, page): + + current = self.pages.currentIndex() + if current == page and self._page == page: + return + + direction = "right" if page > current else "left" + self._page = page + self.pages.slide_view(page, direction=direction) + + def refresh(self): + asset = self.data["pages"]["asset"] + asset.data["model"]["assets"].refresh() + self.refresh_actions() + + def echo(self, message): + widget = self.data["label"]["message"] + widget.setText(str(message)) + + QtCore.QTimer.singleShot(5000, lambda: widget.setText("")) + + print(message) + + def on_project_changed(self): + project_name = self.data["pages"]["asset"].data["model"]["projects"].get_current_project() + io.Session["AVALON_PROJECT"] = project_name + + # Update the Action plug-ins available for the current project + actions_model = self.data["model"]["actions"].model + actions_model.discover() + + def on_session_changed(self): + self.refresh_actions() + + def refresh_actions(self, delay=1): + tools_lib.schedule(self.on_refresh_actions, delay) + + def on_project_clicked(self, project): + io.Session["AVALON_PROJECT"] = project + asset_panel = self.data["pages"]["asset"] + asset_panel.data["model"]["projects"].refresh() # Refresh projects + asset_panel.set_project(project) + self.set_page(1) + self.refresh_actions() + + def on_back_clicked(self): + + self.set_page(0) + self.data["pages"]["project"].model.refresh() # Refresh projects + self.refresh_actions() + + def on_refresh_actions(self): + session = self.get_current_session() + + actions = self.data["model"]["actions"] + actions.model.set_session(session) + actions.model.refresh() + + def on_action_clicked(self, action): + self.echo("Running action: %s" % action.name) + self.run_action(action) + + def on_history_action(self, history_data): + action, session = history_data + app = QtWidgets.QApplication.instance() + modifiers = app.keyboardModifiers() + + is_control_down = QtCore.Qt.ControlModifier & modifiers + if is_control_down: + # User is holding control, rerun the action + self.run_action(action, session=session) + else: + # Revert to that "session" location + self.set_session(session) + + def get_current_session(self): + + index = self._page + if index == 1: + # Assets page + return self.data["pages"]["asset"]._get_current_session() + + else: + session = copy.deepcopy(api.Session) + + # Remove some potential invalid session values + # that we know are not set when not browsing in + # a project. + session.pop("AVALON_PROJECT", None) + session.pop("AVALON_ASSET", None) + session.pop("AVALON_SILO", None) + session.pop("AVALON_TASK", None) + + return session + + def run_action(self, action, session=None): + + if session is None: + session = self.get_current_session() + + # Add to history + history = self.data["model"]["action_history"] + history.add_action(action, session) + + # Process the Action + action().process(session) + + def set_session(self, session): + + panel = self.data["pages"]["asset"] + + project = session.get("AVALON_PROJECT") + silo = session.get("AVALON_SILO") + asset = session.get("AVALON_ASSET") + task = session.get("AVALON_TASK") + + if project: + + # Force the "in project" view. + self.pages.slide_view(1, direction="right") + + projects = panel.data["model"]["projects"] + index = projects.view.findText(project) + if index >= 0: + projects.view.setCurrentIndex(index) + + if silo: + panel.data["model"]["assets"].set_silo(silo) + + if asset: + panel.data["model"]["assets"].select_assets([asset]) + + if task: + panel.on_asset_changed() # requires a forced refresh first + panel.data["model"]["tasks"].select_task(task) + + +class Application(QtWidgets.QApplication): + + def __init__(self, *args): + super(Application, self).__init__(*args) + + # Set app icon + icon_path = tools_lib.resource("icons", "png", "avalon-logo-16.png") + icon = QtGui.QIcon(icon_path) + + self.setWindowIcon(icon) + + # Toggles + self.toggles = {"autoHide": False} + + # Timers + keep_visible = QtCore.QTimer(self) + keep_visible.setInterval(100) + keep_visible.setSingleShot(True) + + timers = {"keepVisible": keep_visible} + + tray = QtWidgets.QSystemTrayIcon(icon) + tray.setToolTip("Avalon Launcher") + + # Signals + tray.activated.connect(self.on_tray_activated) + self.aboutToQuit.connect(self.on_quit) + + menu = self.build_menu() + tray.setContextMenu(menu) + tray.show() + + tray.showMessage("Avalon", "Launcher started.") + + # Don't close the app when we close the log window. + # self.setQuitOnLastWindowClosed(False) + + self.focusChanged.connect(self.on_focus_changed) + + window = Window() + window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) + + self.timers = timers + self._tray = tray + self._window = window + + # geometry = self.calculate_window_geometry(window) + # window.setGeometry(geometry) + + def show(self): + """Show the primary GUI + + This also activates the window and deals with platform-differences. + + """ + + self._window.show() + self._window.raise_() + self._window.activateWindow() + + self.timers["keepVisible"].start() + + def on_tray_activated(self, reason): + if self._window.isVisible(): + self._window.hide() + + elif reason == QtWidgets.QSystemTrayIcon.Trigger: + self.show() + + def on_focus_changed(self, old, new): + """Respond to window losing focus""" + window = new + keep_visible = self.timers["keepVisible"].isActive() + self._window.hide() if (self.toggles["autoHide"] and + not window and + not keep_visible) else None + + def on_autohide_changed(self, auto_hide): + """Respond to changes to auto-hide + + Auto-hide is changed in the UI and determines whether or not + the UI hides upon losing focus. + + """ + + self.toggles["autoHide"] = auto_hide + self.echo("Hiding when losing focus" if auto_hide else "Stays visible") + + def on_quit(self): + """Respond to the application quitting""" + self._tray.hide() + + def build_menu(self): + """Build the right-mouse context menu for the tray icon""" + menu = QtWidgets.QMenu() + + icon = qtawesome.icon("fa.eye", color=style.colors.default) + open = QtWidgets.QAction(icon, "Open", self) + open.triggered.connect(self.show) + + def toggle(): + self.on_autohide_changed(not self.toggles['autoHide']) + + keep_open = QtWidgets.QAction("Keep open", self) + keep_open.setCheckable(True) + keep_open.setChecked(not self.toggles['autoHide']) + keep_open.triggered.connect(toggle) + + quit = QtWidgets.QAction("Quit", self) + quit.triggered.connect(self.quit) + + menu.setStyleSheet(""" + QMenu { + padding: 0px; + margin: 0px; + } + """) + + for action in [open, keep_open, quit]: + menu.addAction(action) + + return menu + + def calculate_window_geometry(self, window): + """Respond to status changes + + On creation, align window with where the tray icon is + located. For example, if the tray icon is in the upper + right corner of the screen, then this is where the + window is supposed to appear. + + Arguments: + status (int): Provided by Qt, the status flag of + loading the input file. + + """ + + tray_x = self._tray.geometry().x() + tray_y = self._tray.geometry().y() + + width = window.width() + width = max(width, window.minimumWidth()) + + height = window.height() + height = max(height, window.sizeHint().height()) + + desktop_geometry = QtWidgets.QDesktopWidget().availableGeometry() + screen_geometry = window.geometry() + + screen_width = screen_geometry.width() + screen_height = screen_geometry.height() + + # Calculate width and height of system tray + systray_width = screen_geometry.width() - desktop_geometry.width() + systray_height = screen_geometry.height() - desktop_geometry.height() + + padding = 10 + + x = screen_width - width + y = screen_height - height + + if tray_x < (screen_width / 2): + x = 0 + systray_width + padding + else: + x -= systray_width + padding + + if tray_y < (screen_height / 2): + y = 0 + systray_height + padding + else: + y -= systray_height + padding + + return QtCore.QRect(x, y, width, height) + + +def show(root=None, debug=False, parent=None): + """Display Loader GUI + + Arguments: + debug (bool, optional): Run loader in debug-mode, + defaults to False + parent (QtCore.QObject, optional): When provided parent the interface + to this QObject. + + """ + + app = Application(sys.argv) + app.setStyleSheet(style.load_stylesheet()) + + # Show the window on launch + app.show() + + app.exec_() + + +def cli(args): + import argparse + parser = argparse.ArgumentParser() + #parser.add_argument("project") + + args = parser.parse_args(args) + #project = args.project + + import launcher.actions as actions + print("Registering default actions..") + actions.register_default_actions() + print("Registering config actions..") + actions.register_config_actions() + print("Registering environment actions..") + actions.register_environment_actions() + io.install() + + #api.Session["AVALON_PROJECT"] = project + + import traceback + sys.excepthook = lambda typ, val, tb: traceback.print_last() + + show() diff --git a/pype/tools/launcher/flickcharm.py b/pype/tools/launcher/flickcharm.py new file mode 100644 index 0000000000..b4dd69be6c --- /dev/null +++ b/pype/tools/launcher/flickcharm.py @@ -0,0 +1,305 @@ +""" +This based on the flickcharm-python code from: + https://code.google.com/archive/p/flickcharm-python/ + +Which states: + This is a Python (PyQt) port of Ariya Hidayat's elegant FlickCharm + hack which adds kinetic scrolling to any scrollable Qt widget. + + Licensed under GNU GPL version 2 or later. + +It has been altered to fix edge cases where clicks and drags would not +propagate correctly under some conditions. It also allows a small "dead zone" +threshold in which it will still propagate the user pressed click if he or she +travelled only very slightly with the cursor. + +""" + +import copy +import sys +from Qt import QtWidgets, QtCore, QtGui + + +class FlickData(object): + Steady = 0 + Pressed = 1 + ManualScroll = 2 + AutoScroll = 3 + Stop = 4 + + def __init__(self): + self.state = FlickData.Steady + self.widget = None + self.pressPos = QtCore.QPoint(0, 0) + self.offset = QtCore.QPoint(0, 0) + self.dragPos = QtCore.QPoint(0, 0) + self.speed = QtCore.QPoint(0, 0) + self.travelled = 0 + self.ignored = [] + + +class FlickCharm(QtCore.QObject): + """Make scrollable widgets flickable. + + For example: + charm = FlickCharm() + charm.activateOn(widget) + + It can `activateOn` multiple widgets with a single FlickCharm instance. + Be aware that the FlickCharm object must be kept around for it not + to get garbage collected and losing the flickable behavior. + + Flick away! + + """ + + def __init__(self, parent=None): + super(FlickCharm, self).__init__(parent=parent) + + self.flickData = {} + self.ticker = QtCore.QBasicTimer() + + # The flick button to use + self.button = QtCore.Qt.LeftButton + + # The time taken per update tick of flicking behavior + self.tick_time = 20 + + # Allow a item click/press directly when AutoScroll is slower than + # this threshold velocity + self.click_in_autoscroll_threshold = 10 + + # Allow an item click/press to propagate as opposed to scrolling + # when the cursor travelled less than this amount of pixels + # Note: back & forth motion increases the value too + self.travel_threshold = 20 + + self.max_speed = 64 # max scroll speed + self.drag = 1 # higher drag will stop autoscroll faster + + def activateOn(self, widget): + viewport = widget.viewport() + viewport.installEventFilter(self) + widget.installEventFilter(self) + self.flickData[viewport] = FlickData() + self.flickData[viewport].widget = widget + self.flickData[viewport].state = FlickData.Steady + + def deactivateFrom(self, widget): + + viewport = widget.viewport() + viewport.removeEventFilter(self) + widget.removeEventFilter(self) + self.flickData.pop(viewport) + + def eventFilter(self, obj, event): + + if not obj.isWidgetType(): + return False + + eventType = event.type() + if eventType != QtCore.QEvent.MouseButtonPress and \ + eventType != QtCore.QEvent.MouseButtonRelease and \ + eventType != QtCore.QEvent.MouseMove: + return False + + if event.modifiers() != QtCore.Qt.NoModifier: + return False + + if obj not in self.flickData: + return False + + data = self.flickData[obj] + found, newIgnored = removeAll(data.ignored, event) + if found: + data.ignored = newIgnored + return False + + if data.state == FlickData.Steady: + if eventType == QtCore.QEvent.MouseButtonPress: + if event.buttons() == self.button: + self._set_press_pos_and_offset(event, data) + data.state = FlickData.Pressed + return True + + elif data.state == FlickData.Pressed: + if eventType == QtCore.QEvent.MouseButtonRelease: + # User didn't actually scroll but clicked in + # the widget. Let the original press and release + # event be evaluated on the Widget + data.state = FlickData.Steady + event1 = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, + data.pressPos, + QtCore.Qt.LeftButton, + QtCore.Qt.LeftButton, + QtCore.Qt.NoModifier) + # Copy the current event + event2 = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonRelease, + event.pos(), + event.button(), + event.buttons(), + event.modifiers()) + data.ignored.append(event1) + data.ignored.append(event2) + QtWidgets.QApplication.postEvent(obj, event1) + QtWidgets.QApplication.postEvent(obj, event2) + return True + elif eventType == QtCore.QEvent.MouseMove: + data.state = FlickData.ManualScroll + data.dragPos = QtGui.QCursor.pos() + if not self.ticker.isActive(): + self.ticker.start(self.tick_time, self) + return True + + elif data.state == FlickData.ManualScroll: + if eventType == QtCore.QEvent.MouseMove: + pos = event.pos() + delta = pos - data.pressPos + data.travelled += delta.manhattanLength() + setScrollOffset(data.widget, data.offset - delta) + return True + elif eventType == QtCore.QEvent.MouseButtonRelease: + + if data.travelled <= self.travel_threshold: + # If the user travelled less than the threshold + # don't go into autoscroll mode but assume the user + # intended to click instead + return self._propagate_click(obj, event, data) + + data.state = FlickData.AutoScroll + return True + + elif data.state == FlickData.AutoScroll: + if eventType == QtCore.QEvent.MouseButtonPress: + + # Allow pressing when auto scroll is already slower than + # the click in autoscroll threshold + velocity = data.speed.manhattanLength() + if velocity <= self.click_in_autoscroll_threshold: + self._set_press_pos_and_offset(event, data) + data.state = FlickData.Pressed + else: + data.state = FlickData.Stop + + data.speed = QtCore.QPoint(0, 0) + return True + elif eventType == QtCore.QEvent.MouseButtonRelease: + data.state = FlickData.Steady + data.speed = QtCore.QPoint(0, 0) + return True + + elif data.state == FlickData.Stop: + if eventType == QtCore.QEvent.MouseButtonRelease: + data.state = FlickData.Steady + + # If the user had a very limited scroll smaller than the + # threshold consider it a regular press and release. + if data.travelled < self.travel_threshold: + return self._propagate_click(obj, event, data) + + return True + elif eventType == QtCore.QEvent.MouseMove: + # Reset the press position and offset to allow us to "continue" + # the scroll from the new point the user clicked and then held + # down to continue scrolling after AutoScroll. + self._set_press_pos_and_offset(event, data) + data.state = FlickData.ManualScroll + + data.dragPos = QtGui.QCursor.pos() + if not self.ticker.isActive(): + self.ticker.start(self.tick_time, self) + return True + + return False + + def _set_press_pos_and_offset(self, event, data): + """Store current event position on Press""" + data.state = FlickData.Pressed + data.pressPos = copy.copy(event.pos()) + data.offset = scrollOffset(data.widget) + data.travelled = 0 + + def _propagate_click(self, obj, event, data): + """Propagate from Pressed state with MouseButtonRelease event. + + Use only on button release in certain states to propagate a click, + for example when the user dragged only a slight distance under the + travel threshold. + + """ + + data.state = FlickData.Pressed + data.pressPos = copy.copy(event.pos()) + data.offset = scrollOffset(data.widget) + data.travelled = 0 + self.eventFilter(obj, event) + return True + + def timerEvent(self, event): + + count = 0 + for data in self.flickData.values(): + if data.state == FlickData.ManualScroll: + count += 1 + cursorPos = QtGui.QCursor.pos() + data.speed = cursorPos - data.dragPos + data.dragPos = cursorPos + elif data.state == FlickData.AutoScroll: + count += 1 + data.speed = deaccelerate(data.speed, + a=self.drag, + maxVal=self.max_speed) + p = scrollOffset(data.widget) + new_p = p - data.speed + setScrollOffset(data.widget, new_p) + + if scrollOffset(data.widget) == p: + # If this scroll resulted in no change on the widget + # we reached the end of the list and set the speed to + # zero. + data.speed = QtCore.QPoint(0, 0) + + if data.speed == QtCore.QPoint(0, 0): + data.state = FlickData.Steady + + if count == 0: + self.ticker.stop() + + super(FlickCharm, self).timerEvent(event) + + +def scrollOffset(widget): + x = widget.horizontalScrollBar().value() + y = widget.verticalScrollBar().value() + return QtCore.QPoint(x, y) + + +def setScrollOffset(widget, p): + widget.horizontalScrollBar().setValue(p.x()) + widget.verticalScrollBar().setValue(p.y()) + + +def deaccelerate(speed, a=1, maxVal=64): + + x = max(min(speed.x(), maxVal), -maxVal) + y = max(min(speed.y(), maxVal), -maxVal) + if x > 0: + x = max(0, x - a) + elif x < 0: + x = min(0, x + a) + if y > 0: + y = max(0, y - a) + elif y < 0: + y = min(0, y + a) + return QtCore.QPoint(x, y) + + +def removeAll(list, val): + found = False + ret = [] + for element in list: + if element == val: + found = True + else: + ret.append(element) + return found, ret diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py new file mode 100644 index 0000000000..8cd117074c --- /dev/null +++ b/pype/tools/launcher/lib.py @@ -0,0 +1,67 @@ +"""Utility script for updating database with configuration files + +Until assets are created entirely in the database, this script +provides a bridge between the file-based project inventory and configuration. + +- Migrating an old project: + $ python -m avalon.inventory --extract --silo-parent=f02_prod + $ python -m avalon.inventory --upload + +- Managing an existing project: + 1. Run `python -m avalon.inventory --load` + 2. Update the .inventory.toml or .config.toml + 3. Run `python -m avalon.inventory --save` + +""" + +from avalon import io, lib, pipeline + + +def list_project_tasks(): + """List the project task types available in the current project""" + project = io.find_one({"type": "project"}) + return [task["name"] for task in project["config"]["tasks"]] + + +def get_application_actions(project): + """Define dynamic Application classes for project using `.toml` files + + Args: + project (dict): project document from the database + + Returns: + list: list of dictionaries + """ + + apps = [] + for app in project["config"]["apps"]: + try: + app_name = app["name"] + app_definition = lib.get_application(app_name) + except Exception as exc: + print("Unable to load application: %s - %s" % (app['name'], exc)) + continue + + # Get from app definition, if not there from app in project + icon = app_definition.get("icon", app.get("icon", "folder-o")) + color = app_definition.get("color", app.get("color", None)) + order = app_definition.get("order", app.get("order", 0)) + label = app.get("label") or app_definition.get("label") or app["name"] + group = app.get("group") or app_definition.get("group") + + action = type( + "app_{}".format(app_name), + (pipeline.Application,), + { + "name": app_name, + "label": label, + "group": group, + "icon": icon, + "color": color, + "order": order, + "config": app_definition.copy() + } + ) + + apps.append(action) + return apps diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py new file mode 100644 index 0000000000..17c28c19b3 --- /dev/null +++ b/pype/tools/launcher/models.py @@ -0,0 +1,292 @@ +import os +import copy +import logging +import collections + +from . import lib +from Qt import QtCore, QtGui +from avalon.vendor import qtawesome +from avalon import io, style, api + + +log = logging.getLogger(__name__) + +icons_dir = "C:/Users/iLLiCiT/Desktop/Prace/pype-setup/repos/pype/pype/resources/app_icons" + + +class TaskModel(QtGui.QStandardItemModel): + """A model listing the tasks combined for a list of assets""" + + def __init__(self, parent=None): + super(TaskModel, self).__init__(parent=parent) + self._num_assets = 0 + + self.default_icon = qtawesome.icon( + "fa.male", color=style.colors.default + ) + self.no_task_icon = qtawesome.icon( + "fa.exclamation-circle", color=style.colors.mid + ) + + self._icons = {} + + self._get_task_icons() + + def _get_task_icons(self): + if io.Session.get("AVALON_PROJECT") is None: + return + + # Get the project configured icons from database + project = io.find_one({"type": "project"}) + for task in project["config"].get("tasks") or []: + icon_name = task.get("icon") + if icon_name: + self._icons[task["name"]] = qtawesome.icon( + "fa.{}".format(icon_name), color=style.colors.default + ) + + def set_assets(self, asset_ids=None, asset_docs=None): + """Set assets to track by their database id + + Arguments: + asset_ids (list): List of asset ids. + asset_docs (list): List of asset entities from MongoDB. + + """ + + if asset_docs is None and asset_ids is not None: + # find assets in db by query + asset_docs = list(io.find({ + "type": "asset", + "_id": {"$in": asset_ids} + })) + db_assets_ids = tuple(asset_doc["_id"] for asset_doc in asset_docs) + + # check if all assets were found + not_found = tuple( + str(asset_id) + for asset_id in asset_ids + if asset_id not in db_assets_ids + ) + + assert not not_found, "Assets not found by id: {0}".format( + ", ".join(not_found) + ) + + self.clear() + + if not asset_docs: + return + + task_names = collections.Counter() + for asset_doc in asset_docs: + asset_tasks = asset_doc.get("data", {}).get("tasks", []) + task_names.update(asset_tasks) + + self.beginResetModel() + + if not task_names: + item = QtGui.QStandardItem(self.no_task_icon, "No task") + item.setEnabled(False) + self.appendRow(item) + + else: + for task_name, count in sorted(task_names.items()): + icon = self._icons.get(task_name, self.default_icon) + item = QtGui.QStandardItem(icon, task_name) + self.appendRow(item) + + self.endResetModel() + + def headerData(self, section, orientation, role): + if ( + role == QtCore.Qt.DisplayRole + and orientation == QtCore.Qt.Horizontal + and section == 0 + ): + return "Tasks" + return super(TaskModel, self).headerData(section, orientation, role) + + +class ActionModel(QtGui.QStandardItemModel): + ACTION_ROLE = QtCore.Qt.UserRole + GROUP_ROLE = QtCore.Qt.UserRole + 1 + + def __init__(self, parent=None): + super(ActionModel, self).__init__(parent=parent) + self._icon_cache = {} + self._group_icon_cache = {} + self._session = {} + self._groups = {} + self.default_icon = qtawesome.icon("fa.cube", color="white") + # Cache of available actions + self._registered_actions = list() + + self.discover() + + def discover(self): + """Set up Actions cache. Run this for each new project.""" + if io.Session.get("AVALON_PROJECT") is None: + self._registered_actions = list() + return + + # Discover all registered actions + actions = api.discover(api.Action) + + # Get available project actions and the application actions + project_doc = io.find_one({"type": "project"}) + app_actions = lib.get_application_actions(project_doc) + actions.extend(app_actions) + + self._registered_actions = actions + + def get_icon(self, action, skip_default=False): + icon_name = action.icon + if not icon_name: + if skip_default: + return None + return self.default_icon + + icon = self._icon_cache.get(icon_name) + if icon: + return icon + + icon = self.default_icon + icon_path = os.path.join(icons_dir, icon_name) + if os.path.exists(icon_path): + icon = QtGui.QIcon(icon_path) + self._icon_cache[icon_name] = icon + return icon + + try: + icon_color = getattr(action, "color", None) or "white" + icon = qtawesome.icon( + "fa.{}".format(icon_name), color=icon_color + ) + + except Exception: + print("Can't load icon \"{}\"".format(icon_name)) + + self._icon_cache[icon_name] = self.default_icon + return icon + + def refresh(self): + # Validate actions based on compatibility + self.clear() + + self._groups.clear() + + actions = self.filter_compatible_actions(self._registered_actions) + + self.beginResetModel() + + single_actions = [] + grouped_actions = collections.defaultdict(list) + for action in actions: + group_name = getattr(action, "group", None) + if not group_name: + single_actions.append(action) + else: + grouped_actions[group_name].append(action) + + for group_name, actions in tuple(grouped_actions.items()): + if len(actions) == 1: + grouped_actions.pop(group_name) + single_actions.append(actions[0]) + + items_by_order = collections.defaultdict(list) + for action in single_actions: + icon = self.get_icon(action) + item = QtGui.QStandardItem( + icon, str(action.label or action.name) + ) + item.setData(action, self.ACTION_ROLE) + items_by_order[action.order].append(item) + + for group_name, actions in grouped_actions.items(): + icon = None + order = None + for action in actions: + if order is None or action.order < order: + order = action.order + + if icon is None: + _icon = self.get_icon(action) + if _icon: + icon = _icon + + if icon is None: + icon = self.default_icon + + item = QtGui.QStandardItem(icon, group_name) + item.setData(actions, self.ACTION_ROLE) + item.setData(True, self.GROUP_ROLE) + + items_by_order[order].append(item) + + for order in sorted(items_by_order.keys()): + for item in items_by_order[order]: + self.appendRow(item) + + self.endResetModel() + + def set_session(self, session): + assert isinstance(session, dict) + self._session = copy.deepcopy(session) + self.refresh() + + def filter_compatible_actions(self, actions): + """Collect all actions which are compatible with the environment + + Each compatible action will be translated to a dictionary to ensure + the action can be visualized in the launcher. + + Args: + actions (list): list of classes + + Returns: + list: collection of dictionaries sorted on order int he + """ + + compatible = [] + for action in actions: + if action().is_compatible(self._session): + compatible.append(action) + + # Sort by order and name + return sorted( + compatible, + key=lambda action: (action.order, action.name) + ) + + +class ProjectModel(QtGui.QStandardItemModel): + """List of projects""" + + def __init__(self, parent=None): + super(ProjectModel, self).__init__(parent=parent) + + self.hide_invisible = False + self.project_icon = qtawesome.icon("fa.map", color="white") + + def refresh(self): + self.clear() + self.beginResetModel() + + for project_doc in self.get_projects(): + item = QtGui.QStandardItem(self.project_icon, project_doc["name"]) + self.appendRow(item) + + self.endResetModel() + + def get_projects(self): + project_docs = [] + for project_doc in sorted(io.projects(), key=lambda x: x["name"]): + if ( + self.hide_invisible + and not project_doc["data"].get("visible", True) + ): + continue + project_docs.append(project_doc) + + return project_docs diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py new file mode 100644 index 0000000000..c48376ae91 --- /dev/null +++ b/pype/tools/launcher/widgets.py @@ -0,0 +1,419 @@ +import copy +from Qt import QtWidgets, QtCore, QtGui +from avalon.vendor import qtawesome +from avalon import api + +from .models import TaskModel, ActionModel, ProjectModel +from .flickcharm import FlickCharm + + +class ProjectBar(QtWidgets.QWidget): + project_changed = QtCore.Signal(int) + + def __init__(self, parent=None): + super(ProjectBar, self).__init__(parent) + + layout = QtWidgets.QHBoxLayout(self) + + self.model = ProjectModel() + self.model.hide_invisible = True + + self.view = QtWidgets.QComboBox() + self.view.setModel(self.model) + self.view.setRootModelIndex(QtCore.QModelIndex()) + + layout.addWidget(self.view) + + self.setSizePolicy( + QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.Maximum + ) + + # Initialize + self.refresh() + + # Signals + self.view.currentIndexChanged.connect(self.project_changed) + + # Set current project by default if it's set. + project_name = api.Session.get("AVALON_PROJECT") + if project_name: + self.set_project(project_name) + + def get_current_project(self): + return self.view.currentText() + + def set_project(self, project_name): + index = self.view.findText(project_name) + if index >= 0: + self.view.setCurrentIndex(index) + + def refresh(self): + prev_project_name = self.get_current_project() + + # Refresh without signals + self.view.blockSignals(True) + self.model.refresh() + + self.set_project(prev_project_name) + + self.view.blockSignals(False) + + self.project_changed.emit(self.view.currentIndex()) + + +class ActionDelegate(QtWidgets.QStyledItemDelegate): + extender_lines = 2 + extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100))#, 160)) + extender_fg = QtGui.QColor(255, 255, 255)#, 160) + + def __init__(self, group_role, *args, **kwargs): + super(ActionDelegate, self).__init__(*args, **kwargs) + self.group_role = group_role + + def paint(self, painter, option, index): + super(ActionDelegate, self).paint(painter, option, index) + is_group = index.data(self.group_role) + if not is_group: + return + + extender_width = int(option.decorationSize.width() / 2) + extender_height = int(option.decorationSize.height() / 2) + + exteder_rect = QtCore.QRectF( + option.rect.x() + (option.rect.width() / 10), + option.rect.y() + (option.rect.height() / 10), + extender_width, + extender_height + ) + path = QtGui.QPainterPath() + path.addRoundedRect(exteder_rect, 2, 2) + + painter.fillPath(path, self.extender_bg_brush) + + painter.setPen(self.extender_fg) + painter.drawPath(path) + + divider = (2 * self.extender_lines) + 1 + line_height = extender_height / divider + line_width = extender_width - (extender_width / 5) + pos_x = exteder_rect.x() + extender_width / 10 + pos_y = exteder_rect.y() + line_height + for _ in range(self.extender_lines): + line_rect = QtCore.QRectF( + pos_x, pos_y, line_width, round(line_height) + ) + painter.fillRect(line_rect, self.extender_fg) + pos_y += 2 * line_height + + +class ActionBar(QtWidgets.QWidget): + """Launcher interface""" + + action_clicked = QtCore.Signal(object) + + def __init__(self, parent=None): + super(ActionBar, self).__init__(parent) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(8, 0, 8, 0) + + view = QtWidgets.QListView(self) + view.setObjectName("ActionView") + view.setViewMode(QtWidgets.QListView.IconMode) + view.setResizeMode(QtWidgets.QListView.Adjust) + view.setSelectionMode(QtWidgets.QListView.NoSelection) + view.setWrapping(True) + view.setGridSize(QtCore.QSize(70, 75)) + view.setIconSize(QtCore.QSize(30, 30)) + view.setSpacing(0) + view.setWordWrap(True) + + model = ActionModel(self) + view.setModel(model) + + delegate = ActionDelegate(model.GROUP_ROLE, self) + view.setItemDelegate(delegate) + + layout.addWidget(view) + + self.model = model + self.view = view + + # Make view flickable + flick = FlickCharm(parent=view) + flick.activateOn(view) + + self.set_row_height(1) + + view.clicked.connect(self.on_clicked) + + def set_row_height(self, rows): + self.setMinimumHeight(rows * 75) + + def on_clicked(self, index): + if index.isValid(): + is_group = action = index.data(self.model.GROUP_ROLE) + if not is_group: + action = index.data(self.model.ACTION_ROLE) + self.action_clicked.emit(action) + return + + menu = QtWidgets.QMenu(self) + actions = index.data(self.model.ACTION_ROLE) + actions_mapping = {} + for action in actions: + menu_action = QtWidgets.QAction(action.label or action.name) + menu.addAction(menu_action) + actions_mapping[menu_action] = action + + result = menu.exec_(QtGui.QCursor.pos()) + if result: + action = actions_mapping[result] + self.action_clicked.emit(action) + + +class TasksWidget(QtWidgets.QWidget): + """Widget showing active Tasks""" + + task_changed = QtCore.Signal() + selection_mode = ( + QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows + ) + + def __init__(self): + super(TasksWidget, self).__init__() + + view = QtWidgets.QTreeView() + view.setIndentation(0) + model = TaskModel() + view.setModel(model) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(view) + + view.selectionModel().selectionChanged.connect(self.task_changed) + + self.model = model + self.view = view + + self._last_selected_task = None + + def set_asset(self, asset_id): + if asset_id is None: + # Asset deselected + self.model.set_assets() + return + + # Try and preserve the last selected task and reselect it + # after switching assets. If there's no currently selected + # asset keep whatever the "last selected" was prior to it. + current = self.get_current_task() + if current: + self._last_selected_task = current + + self.model.set_assets([asset_id]) + + if self._last_selected_task: + self.select_task(self._last_selected_task) + + # Force a task changed emit. + self.task_changed.emit() + + def select_task(self, task_name): + """Select a task by name. + + If the task does not exist in the current model then selection is only + cleared. + + Args: + task (str): Name of the task to select. + + """ + + # Clear selection + self.view.selectionModel().clearSelection() + + # Select the task + for row in range(self.model.rowCount()): + index = self.model.index(row, 0) + _task_name = index.data(QtCore.Qt.DisplayRole) + if _task_name == task_name: + self.view.selectionModel().select(index, self.selection_mode) + # Set the currently active index + self.view.setCurrentIndex(index) + break + + def get_current_task(self): + """Return name of task at current index (selected) + + Returns: + str: Name of the current task. + + """ + index = self.view.currentIndex() + if self.view.selectionModel().isSelected(index): + return index.data(QtCore.Qt.DisplayRole) + + +class ActionHistory(QtWidgets.QPushButton): + trigger_history = QtCore.Signal(tuple) + + def __init__(self, parent=None): + super(ActionHistory, self).__init__(parent=parent) + + self.max_history = 15 + + self.setFixedWidth(25) + self.setFixedHeight(25) + + self.setIcon(qtawesome.icon("fa.history", color="#CCCCCC")) + self.setIconSize(QtCore.QSize(15, 15)) + + self._history = [] + self.clicked.connect(self.show_history) + + def show_history(self): + # Show history popup + if not self._history: + return + + point = QtGui.QCursor().pos() + + widget = QtWidgets.QListWidget() + widget.setSelectionMode(widget.NoSelection) + + widget.setStyleSheet(""" + * { + font-family: "Courier New"; + } + """) + + largest_label_num_chars = 0 + largest_action_label = max(len(x[0].label) for x in self._history) + action_session_role = QtCore.Qt.UserRole + 1 + + for action, session in reversed(self._history): + project = session.get("AVALON_PROJECT") + asset = session.get("AVALON_ASSET") + task = session.get("AVALON_TASK") + breadcrumb = " > ".join(x for x in [project, asset, task] if x) + + m = "{{action:{0}}} | {{breadcrumb}}".format(largest_action_label) + label = m.format(action=action.label, breadcrumb=breadcrumb) + + icon_name = action.icon + color = action.color or "white" + icon = qtawesome.icon("fa.%s" % icon_name, color=color) + item = QtWidgets.QListWidgetItem(icon, label) + item.setData(action_session_role, (action, session)) + + largest_label_num_chars = max(largest_label_num_chars, len(label)) + + widget.addItem(item) + + # Show history + width = 40 + (largest_label_num_chars * 7) # padding + icon + text + entry_height = 21 + height = entry_height * len(self._history) + + dialog = QtWidgets.QDialog(parent=self) + dialog.setWindowTitle("Action History") + dialog.setWindowFlags(QtCore.Qt.FramelessWindowHint | + QtCore.Qt.Popup) + dialog.setSizePolicy(QtWidgets.QSizePolicy.Ignored, + QtWidgets.QSizePolicy.Ignored) + + layout = QtWidgets.QVBoxLayout(dialog) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(widget) + + def on_clicked(index): + data = index.data(action_session_role) + self.trigger_history.emit(data) + dialog.close() + + widget.clicked.connect(on_clicked) + + dialog.setGeometry(point.x() - width, + point.y() - height, + width, + height) + dialog.exec_() + + self.widget_popup = widget + + def add_action(self, action, session): + key = (action, copy.deepcopy(session)) + + # Remove entry if already exists + try: + index = self._history.index(key) + self._history.pop(index) + except ValueError: + pass + + self._history.append(key) + + # Slice the end of the list if we exceed the max history + if len(self._history) > self.max_history: + self._history = self._history[-self.max_history:] + + def clear_history(self): + self._history[:] = [] + + +class SlidePageWidget(QtWidgets.QStackedWidget): + """Stacked widget that nicely slides between its pages""" + + directions = { + "left": QtCore.QPoint(-1, 0), + "right": QtCore.QPoint(1, 0), + "up": QtCore.QPoint(0, 1), + "down": QtCore.QPoint(0, -1) + } + + def slide_view(self, index, direction="right"): + + if self.currentIndex() == index: + return + + offset = self.directions.get(direction) + assert offset is not None, "invalid slide direction: %s" % (direction,) + + width = self.frameRect().width() + height = self.frameRect().height() + offset = QtCore.QPoint(offset.x() * width, offset.y() * height) + + new_page = self.widget(index) + new_page.setGeometry(0, 0, width, height) + curr_pos = new_page.pos() + new_page.move(curr_pos + offset) + new_page.show() + new_page.raise_() + + current_page = self.currentWidget() + + b_pos = QtCore.QByteArray(b"pos") + + anim_old = QtCore.QPropertyAnimation(current_page, b_pos, self) + anim_old.setDuration(250) + anim_old.setStartValue(curr_pos) + anim_old.setEndValue(curr_pos - offset) + anim_old.setEasingCurve(QtCore.QEasingCurve.OutQuad) + + anim_new = QtCore.QPropertyAnimation(new_page, b_pos, self) + anim_new.setDuration(250) + anim_new.setStartValue(curr_pos + offset) + anim_new.setEndValue(curr_pos) + anim_new.setEasingCurve(QtCore.QEasingCurve.OutQuad) + + anim_group = QtCore.QParallelAnimationGroup(self) + anim_group.addAnimation(anim_old) + anim_group.addAnimation(anim_new) + + def slide_finished(): + self.setCurrentWidget(new_page) + + anim_group.finished.connect(slide_finished) + anim_group.start() From 1a13997eb9c3edcb495b5c4149b23dec54eeaf93 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Aug 2020 21:53:13 +0200 Subject: [PATCH 037/158] use resources --- pype/tools/launcher/models.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index 17c28c19b3..ae29c65297 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -7,12 +7,10 @@ from . import lib from Qt import QtCore, QtGui from avalon.vendor import qtawesome from avalon import io, style, api - +from pype.api import resources log = logging.getLogger(__name__) -icons_dir = "C:/Users/iLLiCiT/Desktop/Prace/pype-setup/repos/pype/pype/resources/app_icons" - class TaskModel(QtGui.QStandardItemModel): """A model listing the tasks combined for a list of assets""" @@ -152,7 +150,7 @@ class ActionModel(QtGui.QStandardItemModel): return icon icon = self.default_icon - icon_path = os.path.join(icons_dir, icon_name) + icon_path = resources.get_resource(icon_name) if os.path.exists(icon_path): icon = QtGui.QIcon(icon_path) self._icon_cache[icon_name] = icon From 80ce7dc738d3af53c5002ba0f8cc650fb2e412a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Aug 2020 22:02:38 +0200 Subject: [PATCH 038/158] replaced `<` with icon --- pype/tools/launcher/app.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index 8bce705fc9..9cef313bf5 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -138,8 +138,11 @@ class AssetsPanel(QtWidgets.QWidget): project_bar = QtWidgets.QWidget() layout = QtWidgets.QHBoxLayout(project_bar) layout.setSpacing(4) - back = QtWidgets.QPushButton("<") - back.setFixedWidth(25) + + icon = qtawesome.icon("fa.angle-left", color="white") + back = QtWidgets.QPushButton() + back.setIcon(icon) + back.setFixedWidth(23) back.setFixedHeight(23) projects = ProjectBar() projects.layout().setContentsMargins(0, 0, 0, 0) @@ -147,9 +150,9 @@ class AssetsPanel(QtWidgets.QWidget): layout.addWidget(projects) # assets - _assets_widgets = QtWidgets.QWidget() - _assets_widgets.setContentsMargins(0, 0, 0, 0) - assets_layout = QtWidgets.QVBoxLayout(_assets_widgets) + assets_proxy_widgets = QtWidgets.QWidget() + assets_proxy_widgets.setContentsMargins(0, 0, 0, 0) + assets_layout = QtWidgets.QVBoxLayout(assets_proxy_widgets) assets_widgets = AssetWidget() # Make assets view flickable @@ -167,7 +170,7 @@ class AssetsPanel(QtWidgets.QWidget): body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) body.setOrientation(QtCore.Qt.Horizontal) - body.addWidget(_assets_widgets) + body.addWidget(assets_proxy_widgets) body.addWidget(tasks_widgets) body.setStretchFactor(0, 100) body.setStretchFactor(1, 65) From 0c3076c931eab668a486226f4dd23a45d8eef5ae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 11 Aug 2020 11:30:35 +0200 Subject: [PATCH 039/158] it is not checked blocked state on parents tasks but if status name is omitted --- pype/modules/ftrack/events/event_next_task_update.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/modules/ftrack/events/event_next_task_update.py b/pype/modules/ftrack/events/event_next_task_update.py index 2df3800d8a..1f8407e559 100644 --- a/pype/modules/ftrack/events/event_next_task_update.py +++ b/pype/modules/ftrack/events/event_next_task_update.py @@ -148,11 +148,12 @@ class NextTaskUpdate(BaseEvent): continue parents_task_status = statuses_by_id[parents_task["status_id"]] - low_state_name = parents_task_status["state"]["name"].lower() - # Skip if task's status is in blocked state (e.g. Omitted) - if low_state_name != "blocked": + low_status_name = parents_task_status["name"].lower() + # Skip if task's status name "Omitted" + if low_status_name == "omitted": continue + low_state_name = parents_task_status["state"]["name"].lower() if low_state_name != "done": all_same_type_taks_done = False break From a6203f06daa9471897bc5382c531a8c3831f4aee Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 11 Aug 2020 12:56:36 +0100 Subject: [PATCH 040/158] Multiple reviews where being overwritten to one. --- pype/plugins/maya/publish/collect_review.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pype/plugins/maya/publish/collect_review.py b/pype/plugins/maya/publish/collect_review.py index 063a854bd1..0575d90452 100644 --- a/pype/plugins/maya/publish/collect_review.py +++ b/pype/plugins/maya/publish/collect_review.py @@ -69,7 +69,12 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data['remove'] = True i += 1 else: - instance.data['subset'] = task + 'Review' + subset = "{}{}{}".format( + task, + instance.data["subset"][0].upper(), + instance.data["subset"][1:] + ) + instance.data['subset'] = subset instance.data['review_camera'] = camera instance.data['frameStartFtrack'] = instance.data["frameStartHandle"] instance.data['frameEndFtrack'] = instance.data["frameEndHandle"] From 0b6ef8c6f51b8a427a2e782547bc7476474cdd9e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 11 Aug 2020 13:52:21 +0100 Subject: [PATCH 041/158] Isolate view on instance members if more than camera (one object). --- pype/plugins/maya/publish/extract_playblast.py | 5 +++++ pype/plugins/maya/publish/extract_thumbnail.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/pype/plugins/maya/publish/extract_playblast.py b/pype/plugins/maya/publish/extract_playblast.py index 8d45f98b90..3c9811d4c4 100644 --- a/pype/plugins/maya/publish/extract_playblast.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -77,6 +77,11 @@ class ExtractPlayblast(pype.api.Extractor): pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) + # Isolate view is requested by having objects in the set besides a + # camera. + if len(instance.data["setMembers"]) > 1: + preset["isolate"] = instance.data["setMembers"] + with maintained_time(): filename = preset.get("filename", "%TEMP%") diff --git a/pype/plugins/maya/publish/extract_thumbnail.py b/pype/plugins/maya/publish/extract_thumbnail.py index c0eb2a608e..2edd19a559 100644 --- a/pype/plugins/maya/publish/extract_thumbnail.py +++ b/pype/plugins/maya/publish/extract_thumbnail.py @@ -77,6 +77,11 @@ class ExtractThumbnail(pype.api.Extractor): pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) + # Isolate view is requested by having objects in the set besides a + # camera. + if len(instance.data["setMembers"]) > 1: + preset["isolate"] = instance.data["setMembers"] + with maintained_time(): filename = preset.get("filename", "%TEMP%") From 14093da98f6c68b7a3e238d51c9c62cec7c16f2c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 11 Aug 2020 21:55:11 +0100 Subject: [PATCH 042/158] Option to keep the review files. - Disabled by default to maintain backwards compatibility. - Open loading review image sequences into Nuke. --- pype/plugins/maya/create/create_review.py | 2 ++ pype/plugins/maya/publish/extract_playblast.py | 7 +++++-- pype/plugins/nuke/load/load_sequence.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/create/create_review.py b/pype/plugins/maya/create/create_review.py index 3e513032e1..c488a7559c 100644 --- a/pype/plugins/maya/create/create_review.py +++ b/pype/plugins/maya/create/create_review.py @@ -21,4 +21,6 @@ class CreateReview(avalon.maya.Creator): for key, value in animation_data.items(): data[key] = value + data["keepImages"] = False + self.data = data diff --git a/pype/plugins/maya/publish/extract_playblast.py b/pype/plugins/maya/publish/extract_playblast.py index 8d45f98b90..80a4dadf28 100644 --- a/pype/plugins/maya/publish/extract_playblast.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -53,7 +53,6 @@ class ExtractPlayblast(pype.api.Extractor): preset['camera'] = camera preset['format'] = "image" - # preset['compression'] = "qt" preset['quality'] = 95 preset['compression'] = "png" preset['start_frame'] = start @@ -102,6 +101,10 @@ class ExtractPlayblast(pype.api.Extractor): if "representations" not in instance.data: instance.data["representations"] = [] + tags = ["review"] + if not instance.data.get("keepImages"): + tags.append("delete") + representation = { 'name': 'png', 'ext': 'png', @@ -111,7 +114,7 @@ class ExtractPlayblast(pype.api.Extractor): "frameEnd": end, 'fps': fps, 'preview': True, - 'tags': ['review', 'delete'] + 'tags': tags } instance.data["representations"].append(representation) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index aa79d8736a..601e28c7c1 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -70,7 +70,7 @@ def loader_shift(node, frame, relative=True): class LoadSequence(api.Loader): """Load image sequence into Nuke""" - families = ["render2d", "source", "plate", "render", "prerender"] + families = ["render2d", "source", "plate", "render", "prerender", "review"] representations = ["exr", "dpx", "jpg", "jpeg", "png"] label = "Load sequence" From 7c21e2da7f4d57f707d718a816dfa6507f70aab1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 12 Aug 2020 08:13:25 +0100 Subject: [PATCH 043/158] Fix backwards compatibility with legacy switch. --- pype/plugins/maya/create/create_review.py | 2 ++ pype/plugins/maya/publish/collect_review.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/create/create_review.py b/pype/plugins/maya/create/create_review.py index 3e513032e1..f05271aeb2 100644 --- a/pype/plugins/maya/create/create_review.py +++ b/pype/plugins/maya/create/create_review.py @@ -21,4 +21,6 @@ class CreateReview(avalon.maya.Creator): for key, value in animation_data.items(): data[key] = value + data["legacy"] = True + self.data = data diff --git a/pype/plugins/maya/publish/collect_review.py b/pype/plugins/maya/publish/collect_review.py index 0575d90452..4d86c6031d 100644 --- a/pype/plugins/maya/publish/collect_review.py +++ b/pype/plugins/maya/publish/collect_review.py @@ -69,12 +69,16 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data['remove'] = True i += 1 else: - subset = "{}{}{}".format( - task, - instance.data["subset"][0].upper(), - instance.data["subset"][1:] - ) - instance.data['subset'] = subset + if instance.data.get("legacy", True): + instance.data['subset'] = task + 'Review' + else: + subset = "{}{}{}".format( + task, + instance.data["subset"][0].upper(), + instance.data["subset"][1:] + ) + instance.data['subset'] = subset + instance.data['review_camera'] = camera instance.data['frameStartFtrack'] = instance.data["frameStartHandle"] instance.data['frameEndFtrack'] = instance.data["frameEndHandle"] From 96f7cb395979d293fb1fe514e329b56e3e16ccd4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 12 Aug 2020 13:50:15 +0200 Subject: [PATCH 044/158] Fix conflicts from 'develop' --- pype/api.py | 22 ++++- pype/lib.py | 84 +++++++++++++++++--- pype/plugins/global/publish/integrate_new.py | 46 ++++++----- 3 files changed, 122 insertions(+), 30 deletions(-) diff --git a/pype/api.py b/pype/api.py index 0cf2573298..c722757a3c 100644 --- a/pype/api.py +++ b/pype/api.py @@ -6,6 +6,14 @@ from pypeapp import ( execute ) +from pypeapp.lib.mongo import ( + decompose_url, + compose_url, + get_default_components +) + +from . import resources + from .plugin import ( Extractor, @@ -30,9 +38,11 @@ from .lib import ( get_hierarchy, get_subsets, get_version_from_path, + get_last_version_from_path, modified_environ, add_tool_to_environment, - source_hash + source_hash, + get_latest_version ) # Special naming case for subprocess since its a built-in method. @@ -44,6 +54,12 @@ __all__ = [ "project_overrides_dir_path", "config", "execute", + "decompose_url", + "compose_url", + "get_default_components", + + # Resources + "resources", # plugin classes "Extractor", @@ -68,9 +84,11 @@ __all__ = [ "get_asset", "get_subsets", "get_version_from_path", + "get_last_version_from_path", "modified_environ", "add_tool_to_environment", "source_hash", - "subprocess" + "subprocess", + "get_latest_version" ] diff --git a/pype/lib.py b/pype/lib.py index 006a396720..ec0fb84f84 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -469,6 +469,43 @@ def get_version_from_path(file): ) +def get_last_version_from_path(path_dir, filter): + """ + Finds last version of given directory content + + Args: + path_dir (string): directory path + filter (list): list of strings used as file name filter + + Returns: + string: file name with last version + + Example: + last_version_file = get_last_version_from_path( + "/project/shots/shot01/work", ["shot01", "compositing", "nk"]) + """ + + assert os.path.isdir(path_dir), "`path_dir` argument needs to be directory" + assert isinstance(filter, list) and ( + len(filter) != 0), "`filter` argument needs to be list and not empty" + + filtred_files = list() + + # form regex for filtering + patern = r".*".join(filter) + + for f in os.listdir(path_dir): + if not re.findall(patern, f): + continue + filtred_files.append(f) + + if filtred_files: + sorted(filtred_files) + return filtred_files[-1] + else: + return None + + def get_avalon_database(): if io._database is None: set_io_database() @@ -482,14 +519,6 @@ def set_io_database(): io.install() -def get_all_avalon_projects(): - db = get_avalon_database() - projects = [] - for name in db.collection_names(): - projects.append(db[name].find_one({'type': 'project'})) - return projects - - def filter_pyblish_plugins(plugins): """ This servers as plugin filter / modifier for pyblish. It will load plugin @@ -610,7 +639,7 @@ def get_subsets(asset_name, if len(repres_out) > 0: output_dict[subset["name"]] = {"version": version_sel, - "representaions": repres_out} + "representations": repres_out} return output_dict @@ -1350,7 +1379,6 @@ def ffprobe_streams(path_to_file): log.debug("FFprobe output: {}".format(popen_output)) return json.loads(popen_output)["streams"] - def source_hash(filepath, *args): """Generate simple identifier for a source file. This is used to identify whether a source file has previously been @@ -1370,3 +1398,39 @@ def source_hash(filepath, *args): time = str(os.path.getmtime(filepath)) size = str(os.path.getsize(filepath)) return "|".join([file_name, time, size] + list(args)).replace(".", ",") + +def get_latest_version(asset_name, subset_name): + """Retrieve latest version from `asset_name`, and `subset_name`. + + Args: + asset_name (str): Name of asset. + subset_name (str): Name of subset. + """ + # Get asset + asset_name = io.find_one( + {"type": "asset", "name": asset_name}, projection={"name": True} + ) + + subset = io.find_one( + {"type": "subset", "name": subset_name, "parent": asset_name["_id"]}, + projection={"_id": True, "name": True}, + ) + + # Check if subsets actually exists. + assert subset, "No subsets found." + + # Get version + version_projection = { + "name": True, + "parent": True, + } + + version = io.find_one( + {"type": "version", "parent": subset["_id"]}, + projection=version_projection, + sort=[("name", -1)], + ) + + assert version, "No version found, this is a bug" + + return version diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 5c48770f99..2cf09474b8 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -84,7 +84,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "fbx", "textures", "action", - "harmony.template" + "harmony.template", + "harmony.palette", + "editorial" ] exclude_families = ["clip"] db_representation_context_keys = [ @@ -111,7 +113,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.info("instance.data: {}".format(instance.data)) self.handle_destination_files(self.integrated_file_sizes, 'finalize') - except Exception as e: + except Exception: # clean destination self.log.critical("Error when registering", exc_info=True) self.handle_destination_files(self.integrated_file_sizes, 'remove') @@ -155,6 +157,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if task_name: anatomy_data["task"] = task_name + anatomy_data["family"] = instance.data.get("family") + stagingdir = instance.data.get("stagingDir") if not stagingdir: self.log.info(( @@ -398,8 +402,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst = "{0}{1}{2}".format( dst_head, dst_padding, - dst_tail - ).replace("..", ".") + dst_tail).replace("..", ".") self.log.debug("destination: `{}`".format(dst)) src = os.path.join(stagingdir, src_file_name) @@ -606,12 +609,19 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # copy file with speedcopy and check if size of files are simetrical while True: + import shutil try: copyfile(src, dst) - except OSError as e: - self.log.critical("Cannot copy {} to {}".format(src, dst)) - self.log.critical(e) - six.reraise(*sys.exc_info()) + except shutil.SameFileError as sfe: + self.log.critical("files are the same {} to {}".format(src, dst)) + os.remove(dst) + try: + shutil.copyfile(src, dst) + self.log.debug("Copying files with shutil...") + except (OSError) as e: + self.log.critical("Cannot copy {} to {}".format(src, dst)) + self.log.critical(e) + six.reraise(*sys.exc_info()) if str(getsize(src)) in str(getsize(dst)): break @@ -648,7 +658,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "type": "subset", "name": subset_name, "data": { - "families": instance.data.get('families') + "families": instance.data.get("families", []) }, "parent": asset["_id"] }).inserted_id @@ -761,7 +771,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): task_name = io.Session.get("AVALON_TASK") family = self.main_family_from_instance(instance) - matching_profiles = None + matching_profiles = {} highest_value = -1 self.log.info(self.template_name_profiles) for name, filters in self.template_name_profiles.items(): @@ -860,13 +870,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): for src, dest in resources: path = self.get_rootless_path(anatomy, dest) dest = self.get_dest_temp_url(dest) - hash = pype.api.source_hash(dest) - if self.TMP_FILE_EXT and ',{}'.format(self.TMP_FILE_EXT) in hash: - hash = hash.replace(',{}'.format(self.TMP_FILE_EXT), '') + file_hash = pype.api.source_hash(dest) + if self.TMP_FILE_EXT and ',{}'.format(self.TMP_FILE_EXT) in file_hash: + file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), '') file_info = self.prepare_file_info(path, integrated_file_sizes[dest], - hash) + file_hash) output_resources.append(file_info) return output_resources @@ -885,13 +895,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dest += '.{}'.format(self.TMP_FILE_EXT) return dest - def prepare_file_info(self, path, size=None, hash=None, sites=None): + def prepare_file_info(self, path, size=None, file_hash=None, sites=None): """ Prepare information for one file (asset or resource) Arguments: path: destination url of published file (rootless) size(optional): size of file in bytes - hash(optional): hash of file for synchronization validation + file_hash(optional): hash of file for synchronization validation sites(optional): array of published locations, ['studio'] by default expected ['studio', 'site1', 'gdrive1'] Returns: @@ -905,8 +915,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if size: rec["size"] = size - if hash: - rec["hash"] = hash + if file_hash: + rec["hash"] = file_hash if sites: rec["sites"] = sites From d37eac4a226444fa5b49798f6e8b39d8b6303219 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 12 Aug 2020 13:51:28 +0200 Subject: [PATCH 045/158] Modified 'sites' element to object of objects for better syncing --- pype/plugins/global/publish/integrate_new.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 2cf09474b8..fb1588f7d8 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -12,6 +12,7 @@ import pyblish.api from avalon import io from avalon.vendor import filelink import pype.api +from datetime import datetime # this is needed until speedcopy for linux is fixed if sys.platform == "win32": @@ -902,13 +903,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): path: destination url of published file (rootless) size(optional): size of file in bytes file_hash(optional): hash of file for synchronization validation - sites(optional): array of published locations, ['studio'] by default - expected ['studio', 'site1', 'gdrive1'] + sites(optional): array of published locations, ['studio': {'created_dt':date}] by default + keys expected ['studio', 'site1', 'gdrive1'] Returns: rec: dictionary with filled info """ - rec = { # TODO update collection step to extend with necessary values + rec = { "_id": io.ObjectId(), "path": path } @@ -921,7 +922,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if sites: rec["sites"] = sites else: - rec["sites"] = ["studio"] + meta = {"created_dt": datetime.now()} + rec["sites"] = {"studio": meta} return rec From b7148c08947ca8acd20c6658f34e6562fa7a56e0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 12 Aug 2020 14:21:26 +0200 Subject: [PATCH 046/158] Hound --- pype/lib.py | 2 + pype/plugins/global/publish/integrate_new.py | 42 +++++++++++--------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index ec0fb84f84..1dd5dd9de5 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1379,6 +1379,7 @@ def ffprobe_streams(path_to_file): log.debug("FFprobe output: {}".format(popen_output)) return json.loads(popen_output)["streams"] + def source_hash(filepath, *args): """Generate simple identifier for a source file. This is used to identify whether a source file has previously been @@ -1399,6 +1400,7 @@ def source_hash(filepath, *args): size = str(os.path.getsize(filepath)) return "|".join([file_name, time, size] + list(args)).replace(".", ",") + def get_latest_version(asset_name, subset_name): """Retrieve latest version from `asset_name`, and `subset_name`. diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index fb1588f7d8..88267e1b0a 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -116,7 +116,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): 'finalize') except Exception: # clean destination - self.log.critical("Error when registering", exc_info=True) + self.log.critical("Error when registering", exc_info=True) self.handle_destination_files(self.integrated_file_sizes, 'remove') six.reraise(*sys.exc_info()) @@ -515,8 +515,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # get 'files' info for representation and all attached resources self.log.debug("Preparing files information ...") representation["files"] = self.get_files_info( - instance, - self.integrated_file_sizes) + instance, + self.integrated_file_sizes) self.log.debug("__ representation: {}".format(representation)) destination_list.append(dst) @@ -613,13 +613,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): import shutil try: copyfile(src, dst) - except shutil.SameFileError as sfe: - self.log.critical("files are the same {} to {}".format(src, dst)) + except shutil.SameFileError: + self.log.critical("files are the same {} to {}".format(src, + dst)) os.remove(dst) try: shutil.copyfile(src, dst) self.log.debug("Copying files with shutil...") - except (OSError) as e: + except OSError as e: self.log.critical("Cannot copy {} to {}".format(src, dst)) self.log.critical(e) six.reraise(*sys.exc_info()) @@ -841,9 +842,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): path = rootless_path else: self.log.warning(( - "Could not find root path for remapping \"{}\"." - " This may cause issues on farm." - ).format(path)) + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(path)) return path def get_files_info(self, instance, integrated_file_sizes): @@ -864,16 +865,19 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): resources = list(instance.data.get("transfers", [])) resources.extend(list(instance.data.get("hardlinks", []))) - self.log.debug("get_resource_files_info.resources:{}".format(resources)) + self.log.debug("get_resource_files_info.resources:{}". + format(resources)) output_resources = [] anatomy = instance.context.data["anatomy"] - for src, dest in resources: + for _src, dest in resources: path = self.get_rootless_path(anatomy, dest) dest = self.get_dest_temp_url(dest) file_hash = pype.api.source_hash(dest) - if self.TMP_FILE_EXT and ',{}'.format(self.TMP_FILE_EXT) in file_hash: - file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), '') + if self.TMP_FILE_EXT and \ + ',{}'.format(self.TMP_FILE_EXT) in file_hash: + file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT), + '') file_info = self.prepare_file_info(path, integrated_file_sizes[dest], @@ -883,7 +887,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return output_resources def get_dest_temp_url(self, dest): - """ Enhance destination path with TMP_FILE_EXT to denote temporary file. + """ Enhance destination path with TMP_FILE_EXT to denote temporary + file. Temporary files will be renamed after successful registration into DB and full copy to destination @@ -903,7 +908,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): path: destination url of published file (rootless) size(optional): size of file in bytes file_hash(optional): hash of file for synchronization validation - sites(optional): array of published locations, ['studio': {'created_dt':date}] by default + sites(optional): array of published locations, + ['studio': {'created_dt':date}] by default keys expected ['studio', 'site1', 'gdrive1'] Returns: rec: dictionary with filled info @@ -941,7 +947,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): remove TMP_FILE_EXT suffix denoting temp file """ if integrated_file_sizes: - for file_url, file_size in integrated_file_sizes.items(): + for file_url, _file_size in integrated_file_sizes.items(): try: if mode == 'remove': self.log.debug("Removing file ...{}".format(file_url)) @@ -958,6 +964,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): except FileNotFoundError: pass # file not there, nothing to delete except OSError: - self.log.error("Cannot {} file {}".format(mode, file_url) - , exc_info=True) + self.log.error("Cannot {} file {}".format(mode, file_url), + exc_info=True) six.reraise(*sys.exc_info()) From 0efc5c9c8706c170fa5ebed26c2ac4222e2a89c8 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 12 Aug 2020 17:20:29 +0100 Subject: [PATCH 047/158] Properly containerize image plane loads. --- pype/plugins/maya/load/load_image_plane.py | 66 +++++++++++++++++++--- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/pype/plugins/maya/load/load_image_plane.py b/pype/plugins/maya/load/load_image_plane.py index 653a8d4128..08f7c99156 100644 --- a/pype/plugins/maya/load/load_image_plane.py +++ b/pype/plugins/maya/load/load_image_plane.py @@ -1,4 +1,9 @@ +import pymel.core as pc +import maya.cmds as cmds + from avalon import api +from avalon.maya.pipeline import containerise +from avalon.maya import lib from Qt import QtWidgets @@ -12,10 +17,14 @@ class ImagePlaneLoader(api.Loader): color = "orange" def load(self, context, name, namespace, data): - import pymel.core as pc - new_nodes = [] image_plane_depth = 1000 + asset = context['asset']['name'] + namespace = namespace or lib.unique_namespace( + asset + "_", + prefix="_" if asset[0].isdigit() else "", + suffix="_", + ) # Getting camera from selection. selection = pc.ls(selection=True) @@ -80,6 +89,9 @@ class ImagePlaneLoader(api.Loader): # Ask user whether to use sequence or still image. if context["representation"]["name"] == "exr": + # Ensure OpenEXRLoader plugin is loaded. + pc.loadPlugin("OpenEXRLoader.mll", quiet=True) + reply = QtWidgets.QMessageBox.information( None, "Frame Hold.", @@ -93,11 +105,51 @@ class ImagePlaneLoader(api.Loader): ) image_plane_shape.frameExtension.set(start_frame) - # Ensure OpenEXRLoader plugin is loaded. - pc.loadPlugin("OpenEXRLoader.mll", quiet=True) - new_nodes.extend( - [image_plane_transform.name(), image_plane_shape.name()] + [ + image_plane_transform.longName().split("|")[-1], + image_plane_shape.longName().split("|")[-1] + ] ) - return new_nodes + for node in new_nodes: + pc.rename(node, "{}:{}".format(namespace, node)) + + return containerise( + name=name, + namespace=namespace, + nodes=new_nodes, + context=context, + loader=self.__class__.__name__ + ) + + def update(self, container, representation): + image_plane_shape = None + for node in pc.PyNode(container["objectName"]).members(): + if node.nodeType() == "imagePlane": + image_plane_shape = node + + assert image_plane_shape is not None, "Image plane not found." + + path = api.get_representation_path(representation) + image_plane_shape.imageName.set(path) + cmds.setAttr( + container["objectName"] + ".representation", + str(representation["_id"]), + type="string" + ) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + members = cmds.sets(container['objectName'], query=True) + cmds.lockNode(members, lock=False) + cmds.delete([container['objectName']] + members) + + # Clean up the namespace + try: + cmds.namespace(removeNamespace=container['namespace'], + deleteNamespaceContent=True) + except RuntimeError: + pass From 4aa995cfe7fb261402bba523d50dfd822281c1c7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 19:48:15 +0200 Subject: [PATCH 048/158] Other order is not int but None --- pype/tools/pyblish_pype/model.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index b7b300f154..fdcdffd33f 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -440,9 +440,6 @@ class PluginModel(QtGui.QStandardItemModel): if label is None: label = "Other" - if order is None: - order = 99999999999999 - group_item = self.group_items.get(label) if not group_item: group_item = GroupItem(label, order=order) From 0c1b37af823d01af459ce2f2860e07c0f5f489f6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 19:48:38 +0200 Subject: [PATCH 049/158] passed group checks order equality instead of comparison --- pype/tools/pyblish_pype/window.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 7d79e0e26c..a1e023bb0a 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -771,10 +771,10 @@ class Window(QtWidgets.QDialog): for group_item in self.plugin_model.group_items.values(): # TODO check only plugins from the group - if ( - group_item.publish_states & GroupStates.HasFinished - or (order is not None and group_item.order >= order) - ): + if group_item.publish_states & GroupStates.HasFinished: + continue + + if order != group_item.order: continue if group_item.publish_states & GroupStates.HasError: From 35731d94d8210ab9ae8a32f4cd98220f37f45213 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 19:49:03 +0200 Subject: [PATCH 050/158] controller sends group order which was processed instead of which will be processed --- pype/tools/pyblish_pype/control.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index 77badf71b6..0162848f2b 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -250,6 +250,8 @@ class Controller(QtCore.QObject): self.processing["current_group_order"] is not None and plugin.order > self.processing["current_group_order"] ): + current_group_order = self.processing["current_group_order"] + new_next_group_order = None new_current_group_order = self.processing["next_group_order"] if new_current_group_order is not None: @@ -270,12 +272,13 @@ class Controller(QtCore.QObject): if self.collect_state == 0: self.collect_state = 1 self.switch_toggleability.emit(True) - self.passed_group.emit(new_current_group_order) + self.passed_group.emit(current_group_order) yield IterationBreak("Collected") - self.passed_group.emit(new_current_group_order) - if self.errored: - yield IterationBreak("Last group errored") + else: + self.passed_group.emit(current_group_order) + if self.errored: + yield IterationBreak("Last group errored") if self.collect_state == 1: self.collect_state = 2 From 40accd1c391639a6f1a73fd125af0f21816755fc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:22:54 +0200 Subject: [PATCH 051/158] Make sure tools and applications attributes always have at least one item in to not raise an exception --- pype/modules/ftrack/actions/action_create_cust_attrs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index 0c7e311377..21c4743725 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -409,6 +409,10 @@ class CustomAttributes(BaseAction): )) ) ) + + # Make sure there is at least one item + if not app_definitions: + app_definitions.append({"empty": "< Empty >"}) return app_definitions def applications_attribute(self, event): @@ -432,6 +436,10 @@ class CustomAttributes(BaseAction): if usage: tools_data.append({tool_name: tool_name}) + # Make sure there is at least one item + if not tools_data: + tools_data.append({"empty": "< Empty >"}) + tools_custom_attr_data = { "label": "Tools", "key": "tools_env", From 18a1a5d6798dc0bf228a159bf3c299b0adce9c02 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:44:52 +0200 Subject: [PATCH 052/158] fix project check --- pype/tools/launcher/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index ae29c65297..7ad161236f 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -124,7 +124,7 @@ class ActionModel(QtGui.QStandardItemModel): def discover(self): """Set up Actions cache. Run this for each new project.""" - if io.Session.get("AVALON_PROJECT") is None: + if not io.Session.get("AVALON_PROJECT"): self._registered_actions = list() return From 6ea3c492d160cd80b98cd69feb0337d01cd573d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:45:07 +0200 Subject: [PATCH 053/158] stylesheet in code was moved to css --- pype/tools/launcher/app.py | 46 +++++++++++----------------------- pype/tools/launcher/widgets.py | 7 +++--- 2 files changed, 19 insertions(+), 34 deletions(-) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index 9cef313bf5..abc350641b 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -34,11 +34,17 @@ class IconListView(QtWidgets.QListView): # Workaround for scrolling being super slow or fast when # toggling between the two visual modes self.setVerticalScrollMode(self.ScrollPerPixel) + self.setObjectName("IconView") - self._mode = 0 + self._mode = None self.set_mode(mode) def set_mode(self, mode): + if mode == self._mode: + return + + self._mode = mode + if mode == self.IconMode: self.setViewMode(QtWidgets.QListView.IconMode) self.setResizeMode(QtWidgets.QListView.Adjust) @@ -49,31 +55,15 @@ class IconListView(QtWidgets.QListView): self.setSpacing(0) self.setAlternatingRowColors(False) - self.setStyleSheet(""" - QListView { - font-size: 11px; - border: 0px; - padding: 0px; - margin: 0px; - - } - - QListView::item { - margin-top: 6px; - /* Won't work without borders set */ - border: 0px; - } - - /* For icon only */ - QListView::icon { - top: 3px; - } - """) + self.setProperty("mode", "icon") + self.style().polish(self) self.verticalScrollBar().setSingleStep(30) elif self.ListMode: - self.setStyleSheet("") # clear stylesheet + self.setProperty("mode", "list") + self.style().polish(self) + self.setViewMode(QtWidgets.QListView.ListMode) self.setResizeMode(QtWidgets.QListView.Adjust) self.setWrapping(False) @@ -85,8 +75,6 @@ class IconListView(QtWidgets.QListView): self.verticalScrollBar().setSingleStep(33.33) - self._mode = mode - def mousePressEvent(self, event): if event.button() == QtCore.Qt.RightButton: self.set_mode(int(not self._mode)) @@ -145,7 +133,6 @@ class AssetsPanel(QtWidgets.QWidget): back.setFixedWidth(23) back.setFixedHeight(23) projects = ProjectBar() - projects.layout().setContentsMargins(0, 0, 0, 0) layout.addWidget(back) layout.addWidget(projects) @@ -216,10 +203,8 @@ class AssetsPanel(QtWidgets.QWidget): channel="assets") def on_project_changed(self): - - project = self.data["model"]["projects"].get_current_project() - - api.Session["AVALON_PROJECT"] = project + project_name = self.data["model"]["projects"].get_current_project() + api.Session["AVALON_PROJECT"] = project_name self.data["model"]["assets"].refresh() # Force asset change callback to ensure tasks are correctly reset @@ -392,8 +377,7 @@ class Window(QtWidgets.QDialog): io.Session["AVALON_PROJECT"] = project_name # Update the Action plug-ins available for the current project - actions_model = self.data["model"]["actions"].model - actions_model.discover() + self.data["model"]["actions"].model.discover() def on_session_changed(self): self.refresh_actions() diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index c48376ae91..f8f4e17691 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -13,8 +13,6 @@ class ProjectBar(QtWidgets.QWidget): def __init__(self, parent=None): super(ProjectBar, self).__init__(parent) - layout = QtWidgets.QHBoxLayout(self) - self.model = ProjectModel() self.model.hide_invisible = True @@ -22,6 +20,8 @@ class ProjectBar(QtWidgets.QWidget): self.view.setModel(self.model) self.view.setRootModelIndex(QtCore.QModelIndex()) + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.view) self.setSizePolicy( @@ -119,7 +119,8 @@ class ActionBar(QtWidgets.QWidget): layout.setContentsMargins(8, 0, 8, 0) view = QtWidgets.QListView(self) - view.setObjectName("ActionView") + view.setProperty("mode", "icon") + view.setObjectName("IconView") view.setViewMode(QtWidgets.QListView.IconMode) view.setResizeMode(QtWidgets.QListView.Adjust) view.setSelectionMode(QtWidgets.QListView.NoSelection) From 22a0c4528e90fe7379c3f48cb1519a94ae7faec6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:49:38 +0200 Subject: [PATCH 054/158] ProjectBar made clear in variable names --- pype/tools/launcher/widgets.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index f8f4e17691..f87b16ecc6 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -16,13 +16,13 @@ class ProjectBar(QtWidgets.QWidget): self.model = ProjectModel() self.model.hide_invisible = True - self.view = QtWidgets.QComboBox() - self.view.setModel(self.model) - self.view.setRootModelIndex(QtCore.QModelIndex()) + self.project_combobox = QtWidgets.QComboBox() + self.project_combobox.setModel(self.model) + self.project_combobox.setRootModelIndex(QtCore.QModelIndex()) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.view) + layout.addWidget(self.project_combobox) self.setSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, @@ -33,7 +33,7 @@ class ProjectBar(QtWidgets.QWidget): self.refresh() # Signals - self.view.currentIndexChanged.connect(self.project_changed) + self.project_combobox.currentIndexChanged.connect(self.project_changed) # Set current project by default if it's set. project_name = api.Session.get("AVALON_PROJECT") @@ -41,25 +41,25 @@ class ProjectBar(QtWidgets.QWidget): self.set_project(project_name) def get_current_project(self): - return self.view.currentText() + return self.project_combobox.currentText() def set_project(self, project_name): - index = self.view.findText(project_name) + index = self.project_combobox.findText(project_name) if index >= 0: - self.view.setCurrentIndex(index) + self.project_combobox.setCurrentIndex(index) def refresh(self): prev_project_name = self.get_current_project() # Refresh without signals - self.view.blockSignals(True) - self.model.refresh() + self.project_combobox.blockSignals(True) + self.model.refresh() self.set_project(prev_project_name) - self.view.blockSignals(False) + self.project_combobox.blockSignals(False) - self.project_changed.emit(self.view.currentIndex()) + self.project_changed.emit(self.project_combobox.currentIndex()) class ActionDelegate(QtWidgets.QStyledItemDelegate): From 8f547489da0ef31790c774205b2c93911b6cec90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:50:00 +0200 Subject: [PATCH 055/158] moved ActionDelegate to delegates.py --- pype/tools/launcher/delegates.py | 46 ++++++++++++++++++++++++++++++++ pype/tools/launcher/widgets.py | 46 +------------------------------- 2 files changed, 47 insertions(+), 45 deletions(-) create mode 100644 pype/tools/launcher/delegates.py diff --git a/pype/tools/launcher/delegates.py b/pype/tools/launcher/delegates.py new file mode 100644 index 0000000000..750301cec4 --- /dev/null +++ b/pype/tools/launcher/delegates.py @@ -0,0 +1,46 @@ +from Qt import QtCore, QtWidgets, QtGui + + +class ActionDelegate(QtWidgets.QStyledItemDelegate): + extender_lines = 2 + extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100))#, 160)) + extender_fg = QtGui.QColor(255, 255, 255)#, 160) + + def __init__(self, group_role, *args, **kwargs): + super(ActionDelegate, self).__init__(*args, **kwargs) + self.group_role = group_role + + def paint(self, painter, option, index): + super(ActionDelegate, self).paint(painter, option, index) + is_group = index.data(self.group_role) + if not is_group: + return + + extender_width = int(option.decorationSize.width() / 2) + extender_height = int(option.decorationSize.height() / 2) + + exteder_rect = QtCore.QRectF( + option.rect.x() + (option.rect.width() / 10), + option.rect.y() + (option.rect.height() / 10), + extender_width, + extender_height + ) + path = QtGui.QPainterPath() + path.addRoundedRect(exteder_rect, 2, 2) + + painter.fillPath(path, self.extender_bg_brush) + + painter.setPen(self.extender_fg) + painter.drawPath(path) + + divider = (2 * self.extender_lines) + 1 + line_height = extender_height / divider + line_width = extender_width - (extender_width / 5) + pos_x = exteder_rect.x() + extender_width / 10 + pos_y = exteder_rect.y() + line_height + for _ in range(self.extender_lines): + line_rect = QtCore.QRectF( + pos_x, pos_y, line_width, round(line_height) + ) + painter.fillRect(line_rect, self.extender_fg) + pos_y += 2 * line_height diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index f87b16ecc6..a32548154b 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -3,6 +3,7 @@ from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome from avalon import api +from .delegates import ActionDelegate from .models import TaskModel, ActionModel, ProjectModel from .flickcharm import FlickCharm @@ -62,51 +63,6 @@ class ProjectBar(QtWidgets.QWidget): self.project_changed.emit(self.project_combobox.currentIndex()) -class ActionDelegate(QtWidgets.QStyledItemDelegate): - extender_lines = 2 - extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100))#, 160)) - extender_fg = QtGui.QColor(255, 255, 255)#, 160) - - def __init__(self, group_role, *args, **kwargs): - super(ActionDelegate, self).__init__(*args, **kwargs) - self.group_role = group_role - - def paint(self, painter, option, index): - super(ActionDelegate, self).paint(painter, option, index) - is_group = index.data(self.group_role) - if not is_group: - return - - extender_width = int(option.decorationSize.width() / 2) - extender_height = int(option.decorationSize.height() / 2) - - exteder_rect = QtCore.QRectF( - option.rect.x() + (option.rect.width() / 10), - option.rect.y() + (option.rect.height() / 10), - extender_width, - extender_height - ) - path = QtGui.QPainterPath() - path.addRoundedRect(exteder_rect, 2, 2) - - painter.fillPath(path, self.extender_bg_brush) - - painter.setPen(self.extender_fg) - painter.drawPath(path) - - divider = (2 * self.extender_lines) + 1 - line_height = extender_height / divider - line_width = extender_width - (extender_width / 5) - pos_x = exteder_rect.x() + extender_width / 10 - pos_y = exteder_rect.y() + line_height - for _ in range(self.extender_lines): - line_rect = QtCore.QRectF( - pos_x, pos_y, line_width, round(line_height) - ) - painter.fillRect(line_rect, self.extender_fg) - pos_y += 2 * line_height - - class ActionBar(QtWidgets.QWidget): """Launcher interface""" From 0b83ea44e14be6ad21a66eaf0d71645b3f07de12 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:52:40 +0200 Subject: [PATCH 056/158] cleared action bar --- pype/tools/launcher/widgets.py | 36 ++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index a32548154b..dedc3a5f50 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -109,25 +109,27 @@ class ActionBar(QtWidgets.QWidget): self.setMinimumHeight(rows * 75) def on_clicked(self, index): - if index.isValid(): - is_group = action = index.data(self.model.GROUP_ROLE) - if not is_group: - action = index.data(self.model.ACTION_ROLE) - self.action_clicked.emit(action) - return + if not index.isValid(): + return - menu = QtWidgets.QMenu(self) - actions = index.data(self.model.ACTION_ROLE) - actions_mapping = {} - for action in actions: - menu_action = QtWidgets.QAction(action.label or action.name) - menu.addAction(menu_action) - actions_mapping[menu_action] = action + is_group = index.data(self.model.GROUP_ROLE) + if not is_group: + action = index.data(self.model.ACTION_ROLE) + self.action_clicked.emit(action) + return - result = menu.exec_(QtGui.QCursor.pos()) - if result: - action = actions_mapping[result] - self.action_clicked.emit(action) + menu = QtWidgets.QMenu(self) + actions = index.data(self.model.ACTION_ROLE) + actions_mapping = {} + for action in actions: + menu_action = QtWidgets.QAction(action.label or action.name) + menu.addAction(menu_action) + actions_mapping[menu_action] = action + + result = menu.exec_(QtGui.QCursor.pos()) + if result: + action = actions_mapping[result] + self.action_clicked.emit(action) class TasksWidget(QtWidgets.QWidget): From ad02f48d29103c3810271e668c2ab4d1c1c74963 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 21:56:54 +0200 Subject: [PATCH 057/158] made clear variable names --- pype/tools/launcher/widgets.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index dedc3a5f50..37c636423f 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -333,16 +333,20 @@ class SlidePageWidget(QtWidgets.QStackedWidget): } def slide_view(self, index, direction="right"): - if self.currentIndex() == index: return - offset = self.directions.get(direction) - assert offset is not None, "invalid slide direction: %s" % (direction,) + offset_direction = self.directions.get(direction) + if offset_direction is None: + print("BUG: invalid slide direction: {}".format(direction)) + return width = self.frameRect().width() height = self.frameRect().height() - offset = QtCore.QPoint(offset.x() * width, offset.y() * height) + offset = QtCore.QPoint( + offset_direction.x() * width, + offset_direction.y() * height + ) new_page = self.widget(index) new_page.setGeometry(0, 0, width, height) From 40ade937d42677ac233ee03827b7ee8aa6110f22 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 22:23:03 +0200 Subject: [PATCH 058/158] organization changes --- pype/tools/launcher/widgets.py | 43 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 37c636423f..a264466dbc 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -237,11 +237,8 @@ class ActionHistory(QtWidgets.QPushButton): if not self._history: return - point = QtGui.QCursor().pos() - widget = QtWidgets.QListWidget() widget.setSelectionMode(widget.NoSelection) - widget.setStyleSheet(""" * { font-family: "Courier New"; @@ -272,16 +269,15 @@ class ActionHistory(QtWidgets.QPushButton): widget.addItem(item) # Show history - width = 40 + (largest_label_num_chars * 7) # padding + icon + text - entry_height = 21 - height = entry_height * len(self._history) - dialog = QtWidgets.QDialog(parent=self) dialog.setWindowTitle("Action History") - dialog.setWindowFlags(QtCore.Qt.FramelessWindowHint | - QtCore.Qt.Popup) - dialog.setSizePolicy(QtWidgets.QSizePolicy.Ignored, - QtWidgets.QSizePolicy.Ignored) + dialog.setWindowFlags( + QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup + ) + dialog.setSizePolicy( + QtWidgets.QSizePolicy.Ignored, + QtWidgets.QSizePolicy.Ignored + ) layout = QtWidgets.QVBoxLayout(dialog) layout.setContentsMargins(0, 0, 0, 0) @@ -294,10 +290,18 @@ class ActionHistory(QtWidgets.QPushButton): widget.clicked.connect(on_clicked) - dialog.setGeometry(point.x() - width, - point.y() - height, - width, - height) + # padding + icon + text + width = 40 + (largest_label_num_chars * 7) + entry_height = 21 + height = entry_height * len(self._history) + + point = QtGui.QCursor().pos() + dialog.setGeometry( + point.x() - width, + point.y() - height, + width, + height + ) dialog.exec_() self.widget_popup = widget @@ -306,11 +310,8 @@ class ActionHistory(QtWidgets.QPushButton): key = (action, copy.deepcopy(session)) # Remove entry if already exists - try: - index = self._history.index(key) - self._history.pop(index) - except ValueError: - pass + if key in self._history: + self._history.remove(key) self._history.append(key) @@ -319,7 +320,7 @@ class ActionHistory(QtWidgets.QPushButton): self._history = self._history[-self.max_history:] def clear_history(self): - self._history[:] = [] + self._history.clear() class SlidePageWidget(QtWidgets.QStackedWidget): From 92122bbe03bdffe43a712691bc9f0df8e254b2aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 22:23:14 +0200 Subject: [PATCH 059/158] moved icon caching to lib --- pype/tools/launcher/lib.py | 38 +++++++++++++++++++++++++++++++++++ pype/tools/launcher/models.py | 31 ++-------------------------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index 8cd117074c..033ac33d66 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -14,7 +14,14 @@ provides a bridge between the file-based project inventory and configuration. """ +import os +from Qt import QtGui from avalon import io, lib, pipeline +from avalon.vendor import qtawesome +from pype.api import resources + +ICON_CACHE = {} +NOT_FOUND = type("NotFound", (object, ), {}) def list_project_tasks(): @@ -65,3 +72,34 @@ def get_application_actions(project): apps.append(action) return apps + + +def get_action_icon(self, action, skip_default=False): + icon_name = action.icon + if not icon_name: + return None + + global ICON_CACHE + + icon = ICON_CACHE.get(icon_name) + if icon is NOT_FOUND: + return None + elif icon: + return icon + + icon_path = resources.get_resource(icon_name) + if os.path.exists(icon_path): + icon = QtGui.QIcon(icon_path) + ICON_CACHE[icon_name] = icon + return icon + + try: + icon_color = getattr(action, "color", None) or "white" + icon = qtawesome.icon( + "fa.{}".format(icon_name), color=icon_color + ) + + except Exception: + print("Can't load icon \"{}\"".format(icon_name)) + + return icon diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index 7ad161236f..ce6e0c722e 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -7,7 +7,6 @@ from . import lib from Qt import QtCore, QtGui from avalon.vendor import qtawesome from avalon import io, style, api -from pype.api import resources log = logging.getLogger(__name__) @@ -112,8 +111,6 @@ class ActionModel(QtGui.QStandardItemModel): def __init__(self, parent=None): super(ActionModel, self).__init__(parent=parent) - self._icon_cache = {} - self._group_icon_cache = {} self._session = {} self._groups = {} self.default_icon = qtawesome.icon("fa.cube", color="white") @@ -139,33 +136,9 @@ class ActionModel(QtGui.QStandardItemModel): self._registered_actions = actions def get_icon(self, action, skip_default=False): - icon_name = action.icon - if not icon_name: - if skip_default: - return None + icon = lib.get_action_icon(action) + if not icon and not skip_default: return self.default_icon - - icon = self._icon_cache.get(icon_name) - if icon: - return icon - - icon = self.default_icon - icon_path = resources.get_resource(icon_name) - if os.path.exists(icon_path): - icon = QtGui.QIcon(icon_path) - self._icon_cache[icon_name] = icon - return icon - - try: - icon_color = getattr(action, "color", None) or "white" - icon = qtawesome.icon( - "fa.{}".format(icon_name), color=icon_color - ) - - except Exception: - print("Can't load icon \"{}\"".format(icon_name)) - - self._icon_cache[icon_name] = self.default_icon return icon def refresh(self): From 1fcb4e836f18f8e9ad6a0a5c160d812f0633c10a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Aug 2020 23:10:40 +0200 Subject: [PATCH 060/158] small tweaks --- pype/tools/launcher/actions.py | 25 ++- pype/tools/launcher/app.py | 270 +++++++++++++----------------- pype/tools/launcher/flickcharm.py | 1 - pype/tools/launcher/lib.py | 2 +- 4 files changed, 137 insertions(+), 161 deletions(-) diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py index 2a2e2ab0f0..44ba9a3a60 100644 --- a/pype/tools/launcher/actions.py +++ b/pype/tools/launcher/actions.py @@ -15,9 +15,13 @@ class ProjectManagerAction(api.Action): return "AVALON_PROJECT" in session def process(self, session, **kwargs): - return lib.launch(executable="python", - args=["-u", "-m", "avalon.tools.projectmanager", - session['AVALON_PROJECT']]) + return lib.launch( + executable="python", + args=[ + "-u", "-m", "avalon.tools.projectmanager", + session['AVALON_PROJECT'] + ] + ) class LoaderAction(api.Action): @@ -31,9 +35,12 @@ class LoaderAction(api.Action): return "AVALON_PROJECT" in session def process(self, session, **kwargs): - return lib.launch(executable="python", - args=["-u", "-m", "avalon.tools.cbloader", - session['AVALON_PROJECT']]) + return lib.launch( + executable="python", + args=[ + "-u", "-m", "avalon.tools.cbloader", session['AVALON_PROJECT'] + ] + ) class LoaderLibrary(api.Action): @@ -46,8 +53,10 @@ class LoaderLibrary(api.Action): return True def process(self, session, **kwargs): - return lib.launch(executable="python", - args=["-u", "-m", "avalon.tools.libraryloader"]) + return lib.launch( + executable="python", + args=["-u", "-m", "avalon.tools.libraryloader"] + ) def register_default_actions(): diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index abc350641b..78c5406fa8 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -110,55 +110,59 @@ class ProjectsPanel(QtWidgets.QWidget): def on_clicked(self, index): if index.isValid(): - project = index.data(QtCore.Qt.DisplayRole) - self.project_clicked.emit(project) + project_name = index.data(QtCore.Qt.DisplayRole) + self.project_clicked.emit(project_name) class AssetsPanel(QtWidgets.QWidget): """Assets page""" - back_clicked = QtCore.Signal() def __init__(self, parent=None): super(AssetsPanel, self).__init__(parent=parent) # project bar - project_bar = QtWidgets.QWidget() - layout = QtWidgets.QHBoxLayout(project_bar) + project_bar_widget = QtWidgets.QWidget() + + layout = QtWidgets.QHBoxLayout(project_bar_widget) layout.setSpacing(4) - icon = qtawesome.icon("fa.angle-left", color="white") - back = QtWidgets.QPushButton() - back.setIcon(icon) - back.setFixedWidth(23) - back.setFixedHeight(23) - projects = ProjectBar() - layout.addWidget(back) - layout.addWidget(projects) + btn_back_icon = qtawesome.icon("fa.angle-left", color="white") + btn_back = QtWidgets.QPushButton() + btn_back.setIcon(btn_back_icon) + btn_back.setFixedWidth(23) + btn_back.setFixedHeight(23) + + project_bar = ProjectBar() + + layout.addWidget(btn_back) + layout.addWidget(project_bar) # assets assets_proxy_widgets = QtWidgets.QWidget() assets_proxy_widgets.setContentsMargins(0, 0, 0, 0) assets_layout = QtWidgets.QVBoxLayout(assets_proxy_widgets) - assets_widgets = AssetWidget() + assets_widget = AssetWidget() # Make assets view flickable flick = FlickCharm(parent=self) - flick.activateOn(assets_widgets.view) - assets_widgets.view.setVerticalScrollMode( - assets_widgets.view.ScrollPerPixel + flick.activateOn(assets_widget.view) + assets_widget.view.setVerticalScrollMode( + assets_widget.view.ScrollPerPixel ) - assets_layout.addWidget(assets_widgets) + assets_layout.addWidget(assets_widget) # tasks - tasks_widgets = TasksWidget() + tasks_widget = TasksWidget() body = QtWidgets.QSplitter() body.setContentsMargins(0, 0, 0, 0) - body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + body.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) body.setOrientation(QtCore.Qt.Horizontal) body.addWidget(assets_proxy_widgets) - body.addWidget(tasks_widgets) + body.addWidget(tasks_widget) body.setStretchFactor(0, 100) body.setStretchFactor(1, 65) @@ -166,49 +170,38 @@ class AssetsPanel(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) - layout.addWidget(project_bar) + layout.addWidget(project_bar_widget) layout.addWidget(body) - self.data = { - "model": { - "projects": projects, - "assets": assets_widgets, - "tasks": tasks_widgets - }, - } + self.project_bar = project_bar + self.assets_widget = assets_widget + self.tasks_widget = tasks_widget # signals - projects.project_changed.connect(self.on_project_changed) - assets_widgets.selection_changed.connect(self.asset_changed) - back.clicked.connect(self.back_clicked) + project_bar.project_changed.connect(self.on_project_changed) + assets_widget.selection_changed.connect(self.on_asset_changed) + btn_back.clicked.connect(self.back_clicked) # Force initial refresh for the assets since we might not be # trigging a Project switch if we click the project that was set # prior to launching the Launcher # todo: remove this behavior when AVALON_PROJECT is not required - assets_widgets.refresh() + assets_widget.refresh() def set_project(self, project): - - projects = self.data["model"]["projects"] - - before = projects.get_current_project() - projects.set_project(project) + before = self.project_bar.get_current_project() + self.project_bar.set_project(project) if project == before: # Force a refresh on the assets if the project hasn't changed - self.data["model"]["assets"].refresh() - - def asset_changed(self): - tools_lib.schedule(self.on_asset_changed, 0.05, - channel="assets") + self.assets_widget.refresh() def on_project_changed(self): - project_name = self.data["model"]["projects"].get_current_project() + project_name = self.project_bar.get_current_project() api.Session["AVALON_PROJECT"] = project_name - self.data["model"]["assets"].refresh() + self.assets_widget.refresh() # Force asset change callback to ensure tasks are correctly reset - self.asset_changed() + tools_lib.schedule(self.on_asset_changed, 0.05, channel="assets") def on_asset_changed(self): """Callback on asset selection changed @@ -219,21 +212,14 @@ class AssetsPanel(QtWidgets.QWidget): print("Asset changed..") - tasks = self.data["model"]["tasks"] - assets = self.data["model"]["assets"] - - asset = assets.get_active_asset_document() - if asset: - tasks.set_asset(asset["_id"]) + asset_doc = self.assets_widget.get_active_asset_document() + if asset_doc: + self.tasks_widget.set_asset(asset_doc["_id"]) else: - tasks.set_asset(None) + self.tasks_widget.set_asset(None) - def _get_current_session(self): - - tasks = self.data["model"]["tasks"] - assets = self.data["model"]["assets"] - - asset = assets.get_active_asset_document() + def get_current_session(self): + asset_doc = self.assets_widget.get_active_asset_document() session = copy.deepcopy(api.Session) # Clear some values that we are about to collect if available @@ -241,16 +227,11 @@ class AssetsPanel(QtWidgets.QWidget): session.pop("AVALON_ASSET", None) session.pop("AVALON_TASK", None) - if asset: - session["AVALON_ASSET"] = asset["name"] - - silo = asset.get("silo") - if silo: - session["AVALON_SILO"] = silo - - task = tasks.get_current_task() - if task: - session["AVALON_TASK"] = task + if asset_doc: + session["AVALON_ASSET"] = asset_doc["name"] + task_name = self.tasks_widget.get_current_task() + if task_name: + session["AVALON_TASK"] = task_name return session @@ -273,21 +254,24 @@ class Window(QtWidgets.QDialog): project_panel = ProjectsPanel() asset_panel = AssetsPanel() - pages = SlidePageWidget() - pages.addWidget(project_panel) - pages.addWidget(asset_panel) + page_slider = SlidePageWidget() + page_slider.addWidget(project_panel) + page_slider.addWidget(asset_panel) # actions - actions = ActionBar() + actions_bar = ActionBar() # statusbar statusbar = QtWidgets.QWidget() - message = QtWidgets.QLabel() - message.setFixedHeight(15) + layout = QtWidgets.QHBoxLayout(statusbar) + + message_label = QtWidgets.QLabel() + message_label.setFixedHeight(15) + action_history = ActionHistory() action_history.setStatusTip("Show Action History") - layout = QtWidgets.QHBoxLayout(statusbar) - layout.addWidget(message) + + layout.addWidget(message_label) layout.addWidget(action_history) # Vertically split Pages and Actions @@ -296,8 +280,8 @@ class Window(QtWidgets.QDialog): body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) body.setOrientation(QtCore.Qt.Vertical) - body.addWidget(pages) - body.addWidget(actions) + body.addWidget(page_slider) + body.addWidget(actions_bar) # Set useful default sizes and set stretch # for the pages so that is the only one that @@ -311,73 +295,71 @@ class Window(QtWidgets.QDialog): layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) + self.message_label = message_label + self.project_panel = project_panel + self.asset_panel = asset_panel + self.actions_bar = actions_bar + self.action_history = action_history + self.data = { - "label": { - "message": message, - }, "pages": { "project": project_panel, "asset": asset_panel }, "model": { - "actions": actions, + "actions": actions_bar, "action_history": action_history }, } - self.pages = pages + self.page_slider = page_slider self._page = 0 # signals - actions.action_clicked.connect(self.on_action_clicked) + actions_bar.action_clicked.connect(self.on_action_clicked) action_history.trigger_history.connect(self.on_history_action) project_panel.project_clicked.connect(self.on_project_clicked) asset_panel.back_clicked.connect(self.on_back_clicked) # Add some signals to propagate from the asset panel - for signal in [ - asset_panel.data["model"]["projects"].project_changed, - asset_panel.data["model"]["assets"].selection_changed, - asset_panel.data["model"]["tasks"].task_changed - ]: + for signal in ( + asset_panel.project_bar.project_changed, + asset_panel.assets_widget.selection_changed, + asset_panel.tasks_widget.task_changed + ): signal.connect(self.on_session_changed) # todo: Simplify this callback connection - asset_panel.data["model"]["projects"].project_changed.connect( + asset_panel.project_bar.project_changed.connect( self.on_project_changed ) self.resize(520, 740) def set_page(self, page): - - current = self.pages.currentIndex() + current = self.page_slider.currentIndex() if current == page and self._page == page: return direction = "right" if page > current else "left" self._page = page - self.pages.slide_view(page, direction=direction) + self.page_slider.slide_view(page, direction=direction) def refresh(self): - asset = self.data["pages"]["asset"] - asset.data["model"]["assets"].refresh() + self.asset_panel.assets_widget.refresh() self.refresh_actions() def echo(self, message): - widget = self.data["label"]["message"] - widget.setText(str(message)) - - QtCore.QTimer.singleShot(5000, lambda: widget.setText("")) - + self.message_label.setText(str(message)) + QtCore.QTimer.singleShot(5000, lambda: self.message_label.setText("")) print(message) def on_project_changed(self): - project_name = self.data["pages"]["asset"].data["model"]["projects"].get_current_project() + project_name = self.asset_panel.project_bar.get_current_project() io.Session["AVALON_PROJECT"] = project_name # Update the Action plug-ins available for the current project - self.data["model"]["actions"].model.discover() + self.actions_bar.model.discover() def on_session_changed(self): self.refresh_actions() @@ -385,26 +367,23 @@ class Window(QtWidgets.QDialog): def refresh_actions(self, delay=1): tools_lib.schedule(self.on_refresh_actions, delay) - def on_project_clicked(self, project): - io.Session["AVALON_PROJECT"] = project - asset_panel = self.data["pages"]["asset"] - asset_panel.data["model"]["projects"].refresh() # Refresh projects - asset_panel.set_project(project) + def on_project_clicked(self, project_name): + io.Session["AVALON_PROJECT"] = project_name + # Refresh projects + self.asset_panel.project_bar.refresh() + self.asset_panel.set_project(project_name) self.set_page(1) self.refresh_actions() def on_back_clicked(self): - self.set_page(0) - self.data["pages"]["project"].model.refresh() # Refresh projects + self.project_panel.model.refresh() # Refresh projects self.refresh_actions() def on_refresh_actions(self): session = self.get_current_session() - - actions = self.data["model"]["actions"] - actions.model.set_session(session) - actions.model.refresh() + self.actions_bar.model.set_session(session) + self.actions_bar.model.refresh() def on_action_clicked(self, action): self.echo("Running action: %s" % action.name) @@ -424,69 +403,58 @@ class Window(QtWidgets.QDialog): self.set_session(session) def get_current_session(self): - - index = self._page - if index == 1: + if self._page == 1: # Assets page - return self.data["pages"]["asset"]._get_current_session() + return self.asset_panel.get_current_session() - else: - session = copy.deepcopy(api.Session) + session = copy.deepcopy(api.Session) - # Remove some potential invalid session values - # that we know are not set when not browsing in - # a project. - session.pop("AVALON_PROJECT", None) - session.pop("AVALON_ASSET", None) - session.pop("AVALON_SILO", None) - session.pop("AVALON_TASK", None) + # Remove some potential invalid session values + # that we know are not set when not browsing in + # a project. + session.pop("AVALON_PROJECT", None) + session.pop("AVALON_ASSET", None) + session.pop("AVALON_SILO", None) + session.pop("AVALON_TASK", None) - return session + return session def run_action(self, action, session=None): - if session is None: session = self.get_current_session() # Add to history - history = self.data["model"]["action_history"] - history.add_action(action, session) + self.action_history.add_action(action, session) # Process the Action action().process(session) def set_session(self, session): - - panel = self.data["pages"]["asset"] - - project = session.get("AVALON_PROJECT") + project_name = session.get("AVALON_PROJECT") silo = session.get("AVALON_SILO") - asset = session.get("AVALON_ASSET") - task = session.get("AVALON_TASK") - - if project: + asset_name = session.get("AVALON_ASSET") + task_name = session.get("AVALON_TASK") + if project_name: # Force the "in project" view. self.pages.slide_view(1, direction="right") - - projects = panel.data["model"]["projects"] - index = projects.view.findText(project) + index = self.asset_panel.project_bar.view.findText(project_name) if index >= 0: - projects.view.setCurrentIndex(index) + self.asset_panel.project_bar.view.setCurrentIndex(index) if silo: - panel.data["model"]["assets"].set_silo(silo) + self.asset_panel.assets_widget.set_silo(silo) - if asset: - panel.data["model"]["assets"].select_assets([asset]) + if asset_name: + self.asset_panel.assets_widget.select_assets([asset_name]) - if task: - panel.on_asset_changed() # requires a forced refresh first - panel.data["model"]["tasks"].select_task(task) + if task_name: + # requires a forced refresh first + self.asset_panel.on_asset_changed() + self.asset_panel.assets_widget.select_task(task_name) class Application(QtWidgets.QApplication): - def __init__(self, *args): super(Application, self).__init__(*args) diff --git a/pype/tools/launcher/flickcharm.py b/pype/tools/launcher/flickcharm.py index b4dd69be6c..a5ea5a79d8 100644 --- a/pype/tools/launcher/flickcharm.py +++ b/pype/tools/launcher/flickcharm.py @@ -16,7 +16,6 @@ travelled only very slightly with the cursor. """ import copy -import sys from Qt import QtWidgets, QtCore, QtGui diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index 033ac33d66..e7933e9843 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -74,7 +74,7 @@ def get_application_actions(project): return apps -def get_action_icon(self, action, skip_default=False): +def get_action_icon(action): icon_name = action.icon if not icon_name: return None From 6e25e483be3840a451b243fce0e88260a44a6ccf Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 13 Aug 2020 09:44:43 +0100 Subject: [PATCH 061/158] Use plugin attributes for legacy. --- pype/plugins/maya/create/create_review.py | 2 -- pype/plugins/maya/publish/collect_review.py | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/create/create_review.py b/pype/plugins/maya/create/create_review.py index f05271aeb2..3e513032e1 100644 --- a/pype/plugins/maya/create/create_review.py +++ b/pype/plugins/maya/create/create_review.py @@ -21,6 +21,4 @@ class CreateReview(avalon.maya.Creator): for key, value in animation_data.items(): data[key] = value - data["legacy"] = True - self.data = data diff --git a/pype/plugins/maya/publish/collect_review.py b/pype/plugins/maya/publish/collect_review.py index 4d86c6031d..e2df54c10b 100644 --- a/pype/plugins/maya/publish/collect_review.py +++ b/pype/plugins/maya/publish/collect_review.py @@ -13,6 +13,7 @@ class CollectReview(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.3 label = 'Collect Review Data' families = ["review"] + legacy = True def process(self, instance): @@ -69,7 +70,7 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data['remove'] = True i += 1 else: - if instance.data.get("legacy", True): + if self.legacy: instance.data['subset'] = task + 'Review' else: subset = "{}{}{}".format( From e9f4d1989019fb3f3e159eae734c21627982722f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 13 Aug 2020 17:08:46 +0100 Subject: [PATCH 062/158] Explicit optional isolate attribute. --- pype/plugins/maya/create/create_review.py | 2 ++ pype/plugins/maya/publish/extract_playblast.py | 2 +- pype/plugins/maya/publish/extract_thumbnail.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/plugins/maya/create/create_review.py b/pype/plugins/maya/create/create_review.py index 3e513032e1..6b153396d6 100644 --- a/pype/plugins/maya/create/create_review.py +++ b/pype/plugins/maya/create/create_review.py @@ -21,4 +21,6 @@ class CreateReview(avalon.maya.Creator): for key, value in animation_data.items(): data[key] = value + data["isolate"] = False + self.data = data diff --git a/pype/plugins/maya/publish/extract_playblast.py b/pype/plugins/maya/publish/extract_playblast.py index 3c9811d4c4..91849567e3 100644 --- a/pype/plugins/maya/publish/extract_playblast.py +++ b/pype/plugins/maya/publish/extract_playblast.py @@ -79,7 +79,7 @@ class ExtractPlayblast(pype.api.Extractor): # Isolate view is requested by having objects in the set besides a # camera. - if len(instance.data["setMembers"]) > 1: + if instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] with maintained_time(): diff --git a/pype/plugins/maya/publish/extract_thumbnail.py b/pype/plugins/maya/publish/extract_thumbnail.py index 2edd19a559..524fc1e17c 100644 --- a/pype/plugins/maya/publish/extract_thumbnail.py +++ b/pype/plugins/maya/publish/extract_thumbnail.py @@ -79,7 +79,7 @@ class ExtractThumbnail(pype.api.Extractor): # Isolate view is requested by having objects in the set besides a # camera. - if len(instance.data["setMembers"]) > 1: + if instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] with maintained_time(): From 8d5c2750c0c91f696717a5b5e05e085feefb4ef6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Aug 2020 13:45:33 +0200 Subject: [PATCH 063/158] implemented task to version status event handler --- .../events/event_task_to_version_status.py | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 pype/modules/ftrack/events/event_task_to_version_status.py diff --git a/pype/modules/ftrack/events/event_task_to_version_status.py b/pype/modules/ftrack/events/event_task_to_version_status.py new file mode 100644 index 0000000000..e07be67b18 --- /dev/null +++ b/pype/modules/ftrack/events/event_task_to_version_status.py @@ -0,0 +1,222 @@ +import collections +from pype.modules.ftrack import BaseEvent + + +class TaskToVersionStatus(BaseEvent): + """Changes status of task's latest AssetVersions on its status change.""" + + # Attribute for caching session user id + _cached_user_id = None + + # Presets usage + asset_types_of_focus = [] + + def register(self, *args, **kwargs): + # Skip registration if attribute `asset_types_of_focus` is not set + modified_asset_types_of_focus = list() + if self.asset_types_of_focus: + if isinstance(self.asset_types_of_focus, str): + self.asset_types_of_focus = [self.asset_types_of_focus] + + for asset_type_name in self.asset_types_of_focus: + modified_asset_types_of_focus.append( + asset_type_name.lower() + ) + + if not modified_asset_types_of_focus: + raise Exception(( + "Event handler \"{}\" does not" + " have set presets for attribute \"{}\"" + ).format(self.__class__.__name__, "asset_types_of_focus")) + + self.asset_types_of_focus = modified_asset_types_of_focus + return super(TaskToVersionStatus, self).register(*args, **kwargs) + + def is_event_invalid(self, session, event): + # Cache user id of currently running session + if self._cached_user_id is None: + session_user_entity = session.query( + "User where username is \"{}\"".format(session.api_user) + ).first() + if not session_user_entity: + self.log.warning( + "Couldn't query Ftrack user with username \"{}\"".format( + session.api_user + ) + ) + return False + self._cached_user_id = session_user_entity["id"] + + # Skip processing if current session user was the user who created + # the event + user_info = event["source"].get("user") or {} + user_id = user_info.get("id") + + # Mark as invalid if user is unknown + if user_id is None: + return True + return user_id == self._cached_user_id + + def filter_event_entities(self, event): + # Filter if event contain relevant data + entities_info = event["data"].get("entities") + if not entities_info: + return + + filtered_entities = [] + for entity_info in entities_info: + # Care only about tasks + if entity_info.get("entityType") != "task": + continue + + # Care only about changes of status + changes = entity_info.get("changes") or {} + statusid_changes = changes.get("statusid") or {} + if ( + statusid_changes.get("new") is None + or statusid_changes.get("old") is None + ): + continue + + filtered_entities.append(entity_info) + + return filtered_entities + + def _get_ent_path(self, entity): + return "/".join( + [ent["name"] for ent in entity["link"]] + ) + + def launch(self, session, event): + '''Propagates status from version to task when changed''' + if self.is_event_invalid(session, event): + return + + filtered_entity_infos = self.filter_event_entities(event) + if not filtered_entity_infos: + return + + task_ids = [ + entity_info["entityId"] + for entity_info in filtered_entity_infos + ] + joined_ids = ",".join( + ["\"{}\"".format(entity_id) for entity_id in task_ids] + ) + + # Query tasks' AssetVersions + asset_versions = session.query(( + "AssetVersion where task_id in ({}) order by version descending" + ).format(joined_ids)).all() + + last_asset_version_by_task_id = ( + self.last_asset_version_by_task_id(asset_versions, task_ids) + ) + if not last_asset_version_by_task_id: + return + + # Query Task entities for last asset versions + joined_filtered_ids = ",".join([ + "\"{}\"".format(entity_id) + for entity_id in last_asset_version_by_task_id.keys() + ]) + task_entities = session.query( + "Task where id in ({})".format(joined_filtered_ids) + ).all() + if not task_entities: + return + + # Final process of changing statuses + av_statuses_by_low_name = self.asset_version_statuses(task_entities[0]) + for task_entity in task_entities: + task_id = task_entity["id"] + task_path = self._get_ent_path(task_entity) + task_status_name = task_entity["status"]["name"] + task_status_name_low = task_status_name.lower() + + last_asset_versions = last_asset_version_by_task_id[task_id] + for last_asset_version in last_asset_versions: + self.log.debug(( + "Trying to change status of last AssetVersion {}" + " for task \"{}\"" + ).format(last_asset_version["version"], task_path)) + + new_asset_version_status = av_statuses_by_low_name.get( + task_status_name_low + ) + # Skip if tasks status is not available to AssetVersion + if not new_asset_version_status: + self.log.debug(( + "AssetVersion does not have matching status to \"{}\"" + ).format(task_status_name)) + continue + + av_ent_path = task_path + " Asset {} AssetVersion {}".format( + last_asset_version["asset"]["name"], + last_asset_version["version"] + ) + + # Skip if current AssetVersion's status is same + current_status_name = last_asset_version["status"]["name"] + if current_status_name.lower() == task_status_name_low: + self.log.debug(( + "AssetVersion already has set status \"{}\". \"{}\"" + ).format(current_status_name, av_ent_path)) + continue + + # Change the status + try: + last_asset_version["status"] = new_asset_version_status + session.commit() + self.log.info("[ {} ] Status updated to [ {} ]".format( + av_ent_path, new_asset_version_status["name"] + )) + except Exception: + session.rollback() + self.log.warning( + "[ {} ]Status couldn't be set to \"{}\"".format( + av_ent_path, new_asset_version_status["name"] + ), + exc_info=True + ) + + def asset_version_statuses(self, entity): + project_entity = self.get_project_from_entity(entity) + project_schema = project_entity["project_schema"] + # Get all available statuses for Task + statuses = project_schema.get_statuses("AssetVersion") + # map lowered status name with it's object + av_statuses_by_low_name = { + status["name"].lower(): status for status in statuses + } + return av_statuses_by_low_name + + def last_asset_version_by_task_id(self, asset_versions, task_ids): + last_asset_version_by_task_id = collections.defaultdict(list) + last_version_by_task_id = {} + poping_entity_ids = set(task_ids) + for asset_version in asset_versions: + asset_type_name_low = ( + asset_version["asset"]["type"]["name"].lower() + ) + if asset_type_name_low not in self.asset_types_of_focus: + continue + + task_id = asset_version["task_id"] + last_version = last_version_by_task_id.get(task_id) + if last_version is None: + last_version_by_task_id[task_id] = asset_version["version"] + + elif last_version != asset_version["version"]: + poping_entity_ids.remove(task_id) + + if not poping_entity_ids: + break + + if task_id in poping_entity_ids: + last_asset_version_by_task_id[task_id].append(asset_version) + return last_asset_version_by_task_id + + +def register(session, plugins_presets): + TaskToVersionStatus(session, plugins_presets).register() From 2a0d0717ca8c9a0c326e9741c017ce872fbe0f36 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 14 Aug 2020 15:35:21 +0100 Subject: [PATCH 064/158] Optional skip review on renders. --- pype/plugins/global/publish/extract_jpeg.py | 4 ++++ pype/plugins/global/publish/extract_review.py | 4 ++++ pype/plugins/maya/create/create_render.py | 1 + pype/plugins/maya/publish/collect_render.py | 1 + 4 files changed, 10 insertions(+) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 9b775f8b6f..ae74370b06 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -26,6 +26,10 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): if instance.data.get("multipartExr") is True: return + # Skip review when requested. + if not instance.data.get("review"): + return + # get representation and loop them representations = instance.data["representations"] diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index e1a0d7043a..1b003064a1 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -50,6 +50,10 @@ class ExtractReview(pyblish.api.InstancePlugin): to_height = 1080 def process(self, instance): + # Skip review when requested. + if not instance.data.get("review"): + return + # ffmpeg doesn't support multipart exrs if instance.data.get("multipartExr") is True: instance_label = ( diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index 9e5f9310ae..9f05226f69 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -175,6 +175,7 @@ class CreateRender(avalon.maya.Creator): self.data["primaryPool"] = pool_names self.data["suspendPublishJob"] = False + self.data["review"] = True self.data["extendFrames"] = False self.data["overrideExistingFrame"] = True # self.data["useLegacyRenderLayers"] = True diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 5ca9392080..dfae6ed0af 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -216,6 +216,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "attachTo": attach_to, "setMembers": layer_name, "multipartExr": ef.multipart, + "review": render_instance.data.get("review") or False, "publish": True, "handleStart": handle_start, From e00091515c3010d2d9e6531ba4e8762011525cfb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Aug 2020 17:58:42 +0200 Subject: [PATCH 065/158] standalone publer moved to tools --- pype/modules/standalonepublish/__init__.py | 11 +- .../standalonepublish_module.py | 20 +- pype/tools/config_setting/widgets/base0.py | 404 ++++ pype/tools/config_setting/widgets/inputs0.py | 2145 +++++++++++++++++ pype/tools/config_setting/widgets/inputs1.py | 2131 ++++++++++++++++ pype/tools/standalonepublish/__init__.py | 8 + .../standalonepublish/__main__.py | 0 .../standalonepublish/app.py | 0 .../standalonepublish/publish.py | 0 .../standalonepublish/resources/__init__.py | 0 .../standalonepublish/resources/edit.svg | 0 .../standalonepublish/resources/file.png | Bin .../standalonepublish/resources/files.png | Bin .../standalonepublish/resources/houdini.png | Bin .../resources/image_file.png | Bin .../resources/image_files.png | Bin .../resources/information.svg | 0 .../standalonepublish/resources/maya.png | Bin .../standalonepublish/resources/menu.png | Bin .../resources/menu_disabled.png | Bin .../resources/menu_hover.png | Bin .../resources/menu_pressed.png | Bin .../resources/menu_pressed_hover.png | Bin .../standalonepublish/resources/nuke.png | Bin .../standalonepublish/resources/premiere.png | Bin .../standalonepublish/resources/trash.png | Bin .../resources/trash_disabled.png | Bin .../resources/trash_hover.png | Bin .../resources/trash_pressed.png | Bin .../resources/trash_pressed_hover.png | Bin .../resources/video_file.png | Bin .../standalonepublish/widgets/__init__.py | 0 .../widgets/button_from_svgs.py | 0 .../standalonepublish/widgets/model_asset.py | 0 .../widgets/model_filter_proxy_exact_match.py | 0 .../model_filter_proxy_recursive_sort.py | 0 .../standalonepublish/widgets/model_node.py | 0 .../widgets/model_tasks_template.py | 0 .../standalonepublish/widgets/model_tree.py | 0 .../widgets/model_tree_view_deselectable.py | 0 .../standalonepublish/widgets/widget_asset.py | 0 .../widgets/widget_component_item.py | 0 .../widgets/widget_components.py | 0 .../widgets/widget_components_list.py | 0 .../widgets/widget_drop_empty.py | 0 .../widgets/widget_drop_frame.py | 0 .../widgets/widget_family.py | 0 .../widgets/widget_family_desc.py | 0 .../widgets/widget_shadow.py | 0 49 files changed, 4698 insertions(+), 21 deletions(-) create mode 100644 pype/tools/config_setting/widgets/base0.py create mode 100644 pype/tools/config_setting/widgets/inputs0.py create mode 100644 pype/tools/config_setting/widgets/inputs1.py create mode 100644 pype/tools/standalonepublish/__init__.py rename pype/{modules => tools}/standalonepublish/__main__.py (100%) rename pype/{modules => tools}/standalonepublish/app.py (100%) rename pype/{modules => tools}/standalonepublish/publish.py (100%) rename pype/{modules => tools}/standalonepublish/resources/__init__.py (100%) rename pype/{modules => tools}/standalonepublish/resources/edit.svg (100%) rename pype/{modules => tools}/standalonepublish/resources/file.png (100%) rename pype/{modules => tools}/standalonepublish/resources/files.png (100%) rename pype/{modules => tools}/standalonepublish/resources/houdini.png (100%) rename pype/{modules => tools}/standalonepublish/resources/image_file.png (100%) rename pype/{modules => tools}/standalonepublish/resources/image_files.png (100%) rename pype/{modules => tools}/standalonepublish/resources/information.svg (100%) rename pype/{modules => tools}/standalonepublish/resources/maya.png (100%) rename pype/{modules => tools}/standalonepublish/resources/menu.png (100%) rename pype/{modules => tools}/standalonepublish/resources/menu_disabled.png (100%) rename pype/{modules => tools}/standalonepublish/resources/menu_hover.png (100%) rename pype/{modules => tools}/standalonepublish/resources/menu_pressed.png (100%) rename pype/{modules => tools}/standalonepublish/resources/menu_pressed_hover.png (100%) rename pype/{modules => tools}/standalonepublish/resources/nuke.png (100%) rename pype/{modules => tools}/standalonepublish/resources/premiere.png (100%) rename pype/{modules => tools}/standalonepublish/resources/trash.png (100%) rename pype/{modules => tools}/standalonepublish/resources/trash_disabled.png (100%) rename pype/{modules => tools}/standalonepublish/resources/trash_hover.png (100%) rename pype/{modules => tools}/standalonepublish/resources/trash_pressed.png (100%) rename pype/{modules => tools}/standalonepublish/resources/trash_pressed_hover.png (100%) rename pype/{modules => tools}/standalonepublish/resources/video_file.png (100%) rename pype/{modules => tools}/standalonepublish/widgets/__init__.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/button_from_svgs.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/model_asset.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/model_filter_proxy_exact_match.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/model_filter_proxy_recursive_sort.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/model_node.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/model_tasks_template.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/model_tree.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/model_tree_view_deselectable.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/widget_asset.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/widget_component_item.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/widget_components.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/widget_components_list.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/widget_drop_empty.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/widget_drop_frame.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/widget_family.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/widget_family_desc.py (100%) rename pype/{modules => tools}/standalonepublish/widgets/widget_shadow.py (100%) diff --git a/pype/modules/standalonepublish/__init__.py b/pype/modules/standalonepublish/__init__.py index 8e615afbea..4038b696d9 100644 --- a/pype/modules/standalonepublish/__init__.py +++ b/pype/modules/standalonepublish/__init__.py @@ -1,14 +1,5 @@ -PUBLISH_PATHS = [] - from .standalonepublish_module import StandAlonePublishModule -from .app import ( - show, - cli -) -__all__ = [ - "show", - "cli" -] + def tray_init(tray_widget, main_widget): return StandAlonePublishModule(main_widget, tray_widget) diff --git a/pype/modules/standalonepublish/standalonepublish_module.py b/pype/modules/standalonepublish/standalonepublish_module.py index 64195bc271..b528642e8d 100644 --- a/pype/modules/standalonepublish/standalonepublish_module.py +++ b/pype/modules/standalonepublish/standalonepublish_module.py @@ -1,21 +1,19 @@ import os -from .app import show -from .widgets import QtWidgets import pype -from . import PUBLISH_PATHS class StandAlonePublishModule: - def __init__(self, main_parent=None, parent=None): self.main_parent = main_parent self.parent_widget = parent - PUBLISH_PATHS.clear() - PUBLISH_PATHS.append(os.path.sep.join( - [pype.PLUGINS_DIR, "standalonepublisher", "publish"] - )) + self.publish_paths = [ + os.path.join( + pype.PLUGINS_DIR, "standalonepublisher", "publish" + ) + ] def tray_menu(self, parent_menu): + from Qt import QtWidgets self.run_action = QtWidgets.QAction( "Publish", parent_menu ) @@ -24,9 +22,9 @@ class StandAlonePublishModule: def process_modules(self, modules): if "FtrackModule" in modules: - PUBLISH_PATHS.append(os.path.sep.join( - [pype.PLUGINS_DIR, "ftrack", "publish"] + self.publish_paths.append(os.path.join( + pype.PLUGINS_DIR, "ftrack", "publish" )) def show(self): - show(self.main_parent, False) + print("Running") diff --git a/pype/tools/config_setting/widgets/base0.py b/pype/tools/config_setting/widgets/base0.py new file mode 100644 index 0000000000..7f01f27ca8 --- /dev/null +++ b/pype/tools/config_setting/widgets/base0.py @@ -0,0 +1,404 @@ +import os +import json +import copy +from Qt import QtWidgets, QtCore, QtGui +from . import config +from .widgets import UnsavedChangesDialog +from .lib import NOT_SET +from avalon import io +from queue import Queue + + +class TypeToKlass: + types = {} + + +class PypeConfigurationWidget: + default_state = "" + + def config_value(self): + raise NotImplementedError( + "Method `config_value` is not implemented for `{}`.".format( + self.__class__.__name__ + ) + ) + + def value_from_values(self, values, keys=None): + if not values: + return NOT_SET + + if keys is None: + keys = self.keys + + value = values + for key in keys: + if not isinstance(value, dict): + raise TypeError( + "Expected dictionary got {}.".format(str(type(value))) + ) + + if key not in value: + return NOT_SET + value = value[key] + return value + + def style_state(self, is_overriden, is_modified): + items = [] + if is_overriden: + items.append("overriden") + if is_modified: + items.append("modified") + return "-".join(items) or self.default_state + + def add_children_gui(self, child_configuration, values): + raise NotImplementedError(( + "Method `add_children_gui` is not implemented for `{}`." + ).format(self.__class__.__name__)) + + +class StudioWidget(QtWidgets.QWidget, PypeConfigurationWidget): + is_overidable = False + is_overriden = False + is_group = False + any_parent_is_group = False + ignore_value_changes = False + + def __init__(self, parent=None): + super(StudioWidget, self).__init__(parent) + + self.input_fields = [] + + scroll_widget = QtWidgets.QScrollArea(self) + content_widget = QtWidgets.QWidget(scroll_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(3, 3, 3, 3) + content_layout.setSpacing(0) + content_layout.setAlignment(QtCore.Qt.AlignTop) + content_widget.setLayout(content_layout) + + # scroll_widget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + # scroll_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + scroll_widget.setWidgetResizable(True) + scroll_widget.setWidget(content_widget) + + self.scroll_widget = scroll_widget + self.content_layout = content_layout + self.content_widget = content_widget + + footer_widget = QtWidgets.QWidget() + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + + save_btn = QtWidgets.QPushButton("Save") + spacer_widget = QtWidgets.QWidget() + footer_layout.addWidget(spacer_widget, 1) + footer_layout.addWidget(save_btn, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + self.setLayout(layout) + + layout.addWidget(scroll_widget, 1) + layout.addWidget(footer_widget, 0) + + save_btn.clicked.connect(self._save) + + self.reset() + + def reset(self): + if self.content_layout.count() != 0: + for widget in self.input_fields: + self.content_layout.removeWidget(widget) + widget.deleteLater() + self.input_fields.clear() + + values = {"studio": config.studio_presets()} + schema = config.gui_schema("studio_schema", "studio_gui_schema") + self.keys = schema.get("keys", []) + self.add_children_gui(schema, values) + self.schema = schema + + def _save(self): + all_values = {} + for item in self.input_fields: + all_values.update(item.config_value()) + + for key in reversed(self.keys): + _all_values = {key: all_values} + all_values = _all_values + + # Skip first key + all_values = all_values["studio"] + + # Load studio data with metadata + current_presets = config.studio_presets() + + keys_to_file = config.file_keys_from_schema(self.schema) + for key_sequence in keys_to_file: + # Skip first key + key_sequence = key_sequence[1:] + subpath = "/".join(key_sequence) + ".json" + origin_values = current_presets + for key in key_sequence: + if key not in origin_values: + origin_values = {} + break + origin_values = origin_values[key] + + new_values = all_values + for key in key_sequence: + new_values = new_values[key] + origin_values.update(new_values) + + output_path = os.path.join( + config.studio_presets_path, subpath + ) + dirpath = os.path.dirname(output_path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + with open(output_path, "w") as file_stream: + json.dump(origin_values, file_stream, indent=4) + + def add_children_gui(self, child_configuration, values): + item_type = child_configuration["type"] + klass = TypeToKlass.types.get(item_type) + item = klass( + child_configuration, values, self.keys, self + ) + self.input_fields.append(item) + self.content_layout.addWidget(item) + + +class ProjectListView(QtWidgets.QListView): + left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex) + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + index = self.indexAt(event.pos()) + self.left_mouse_released_at.emit(index) + super(ProjectListView, self).mouseReleaseEvent(event) + + +class ProjectListWidget(QtWidgets.QWidget): + default = "< Default >" + project_changed = QtCore.Signal() + + def __init__(self, parent): + self._parent = parent + + self.current_project = None + + super(ProjectListWidget, self).__init__(parent) + + label_widget = QtWidgets.QLabel("Projects") + project_list = ProjectListView(self) + project_list.setModel(QtGui.QStandardItemModel()) + + # Do not allow editing + project_list.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + # Do not automatically handle selection + project_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(3) + layout.addWidget(label_widget, 0) + layout.addWidget(project_list, 1) + + project_list.left_mouse_released_at.connect(self.on_item_clicked) + + self.project_list = project_list + + self.refresh() + + def on_item_clicked(self, new_index): + new_project_name = new_index.data(QtCore.Qt.DisplayRole) + if new_project_name is None: + return + + if self.current_project == new_project_name: + return + + save_changes = False + change_project = False + if self.validate_context_change(): + change_project = True + + else: + dialog = UnsavedChangesDialog(self) + result = dialog.exec_() + if result == 1: + save_changes = True + change_project = True + + elif result == 2: + change_project = True + + if save_changes: + self._parent._save() + + if change_project: + self.select_project(new_project_name) + self.current_project = new_project_name + self.project_changed.emit() + else: + self.select_project(self.current_project) + + def validate_context_change(self): + # TODO add check if project can be changed (is modified) + for item in self._parent.input_fields: + is_modified = item.child_modified + if is_modified: + return False + return True + + def project_name(self): + if self.current_project == self.default: + return None + return self.current_project + + def select_project(self, project_name): + model = self.project_list.model() + found_items = model.findItems(project_name) + if not found_items: + found_items = model.findItems(self.default) + + index = model.indexFromItem(found_items[0]) + self.project_list.selectionModel().clear() + self.project_list.selectionModel().setCurrentIndex( + index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent + ) + + def refresh(self): + selected_project = None + for index in self.project_list.selectedIndexes(): + selected_project = index.data(QtCore.Qt.DisplayRole) + break + + model = self.project_list.model() + model.clear() + items = [self.default] + io.install() + for project_doc in tuple(io.projects()): + items.append(project_doc["name"]) + + for item in items: + model.appendRow(QtGui.QStandardItem(item)) + + self.select_project(selected_project) + + self.current_project = self.project_list.currentIndex().data( + QtCore.Qt.DisplayRole + ) + + +class ProjectWidget(QtWidgets.QWidget, PypeConfigurationWidget): + is_overriden = False + is_group = False + any_parent_is_group = False + + def __init__(self, parent=None): + super(ProjectWidget, self).__init__(parent) + + self.is_overidable = False + self.ignore_value_changes = False + + self.input_fields = [] + + scroll_widget = QtWidgets.QScrollArea(self) + content_widget = QtWidgets.QWidget(scroll_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(3, 3, 3, 3) + content_layout.setSpacing(0) + content_layout.setAlignment(QtCore.Qt.AlignTop) + content_widget.setLayout(content_layout) + + scroll_widget.setWidgetResizable(True) + scroll_widget.setWidget(content_widget) + + project_list_widget = ProjectListWidget(self) + content_layout.addWidget(project_list_widget) + + footer_widget = QtWidgets.QWidget() + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + + save_btn = QtWidgets.QPushButton("Save") + spacer_widget = QtWidgets.QWidget() + footer_layout.addWidget(spacer_widget, 1) + footer_layout.addWidget(save_btn, 0) + + presets_widget = QtWidgets.QWidget() + presets_layout = QtWidgets.QVBoxLayout(presets_widget) + presets_layout.setContentsMargins(0, 0, 0, 0) + presets_layout.setSpacing(0) + + presets_layout.addWidget(scroll_widget, 1) + presets_layout.addWidget(footer_widget, 0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + self.setLayout(layout) + + layout.addWidget(project_list_widget, 0) + layout.addWidget(presets_widget, 1) + + save_btn.clicked.connect(self._save) + project_list_widget.project_changed.connect(self._on_project_change) + + self.project_list_widget = project_list_widget + self.scroll_widget = scroll_widget + self.content_layout = content_layout + self.content_widget = content_widget + + self.reset() + + def reset(self): + values = config.global_project_presets() + schema = config.gui_schema("projects_schema", "project_gui_schema") + self.keys = schema.get("keys", []) + self.add_children_gui(schema, values) + + def add_children_gui(self, child_configuration, values): + item_type = child_configuration["type"] + klass = TypeToKlass.types.get(item_type) + + item = klass( + child_configuration, values, self.keys, self + ) + self.input_fields.append(item) + self.content_layout.addWidget(item) + + def _on_project_change(self): + project_name = self.project_list_widget.project_name() + + if project_name is None: + overrides = None + self.is_overidable = False + else: + overrides = config.project_preset_overrides(project_name) + self.is_overidable = True + + self.ignore_value_changes = True + for item in self.input_fields: + item.apply_overrides(overrides) + self.ignore_value_changes = False + + def _save(self): + output = {} + for item in self.input_fields: + if hasattr(item, "override_value"): + print(item.override_value()) + else: + print("*** missing `override_value`", item) + + # for item in self.input_fields: + # output.update(item.config_value()) + # + # for key in reversed(self.keys): + # _output = {key: output} + # output = _output + + print(json.dumps(output, indent=4)) diff --git a/pype/tools/config_setting/widgets/inputs0.py b/pype/tools/config_setting/widgets/inputs0.py new file mode 100644 index 0000000000..7ab9c7fa01 --- /dev/null +++ b/pype/tools/config_setting/widgets/inputs0.py @@ -0,0 +1,2145 @@ +import json +from Qt import QtWidgets, QtCore, QtGui +from . import config +from .base import PypeConfigurationWidget, TypeToKlass +from .widgets import ( + ClickableWidget, + ExpandingWidget, + ModifiedIntSpinBox, + ModifiedFloatSpinBox +) +from .lib import NOT_SET, AS_WIDGET + + +class SchemeGroupHierarchyBug(Exception): + def __init__(self, msg=None): + if not msg: + # TODO better message + msg = "SCHEME BUG: Attribute `is_group` is mixed in the hierarchy" + super(SchemeGroupHierarchyBug, self).__init__(msg) + + +class BooleanWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + self._as_widget = as_widget + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + self.override_value = None + + super(BooleanWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.checkbox = QtWidgets.QCheckBox() + self.checkbox.setAttribute(QtCore.Qt.WA_StyledBackground) + if not self._as_widget and not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + label_widget.setAttribute(QtCore.Qt.WA_StyledBackground) + layout.addWidget(label_widget) + + if not self._as_widget: + self.label_widget = label_widget + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + layout.addWidget(self.checkbox) + + self.default_value = self.item_value() + + self.checkbox.stateChanged.connect(self._on_value_change) + + def set_default_values(self, default_values): + value = self.value_from_values(default_values) + if isinstance(value, bool): + self.set_value(value, default_value=True) + self.default_value = self.item_value() + else: + self.default_value = value + + def set_value(self, value, *, default_value=False): + # Ignore value change because if `self.isChecked()` has same + # value as `value` the `_on_value_change` is not triggered + self.checkbox.setChecked(value) + + if default_value: + self.default_value = self.item_value() + + self._on_value_change() + + def reset_value(self): + if self.is_overidable and self.override_value is not None: + self.set_value(self.override_value) + else: + self.set_value(self.default_value) + + def clear_value(self): + self.reset_value() + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + self._was_overriden = False + value = self.default_value + else: + self._is_overriden = True + self._was_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + _value = self.item_value() + is_modified = None + if self.is_overidable: + self._is_overriden = True + if self.override_value is not None: + is_modified = _value != self.override_value + + if is_modified is None: + is_modified = _value != self.default_value + + self._is_modified = is_modified + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + else: + property_name = "state" + + self.label_widget.setProperty(property_name, state) + self.label_widget.style().polish(self.label_widget) + self._state = state + + def item_value(self): + return self.checkbox.isChecked() + + def config_value(self): + return {self.key: self.item_value()} + + +class IntegerWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + self._parent = parent + self._as_widget = as_widget + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(IntegerWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.int_input = ModifiedIntSpinBox() + + if not self._as_widget and not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + layout.addWidget(self.int_input) + + if not self._as_widget: + self.label_widget = label_widget + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + self.default_value = self.item_value() + self.override_value = None + + self.int_input.valueChanged.connect(self._on_value_change) + + def set_default_values(self, default_values): + value = self.value_from_values(default_values) + if isinstance(value, int): + self.set_value(value, default_value=True) + self.default_value = self.item_value() + else: + self.default_value = value + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def set_value(self, value, *, default_value=False): + self.int_input.setValue(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def clear_value(self): + self.set_value(0) + + def reset_value(self): + self.set_value(self.default_value) + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + self._was_overriden = False + value = self.default_value + else: + self._is_overriden = True + self._was_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.int_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.int_input.value() + + def config_value(self): + return {self.key: self.item_value()} + + +class FloatWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + self._parent = parent + self._as_widget = as_widget + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(FloatWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.float_input = ModifiedFloatSpinBox() + + decimals = input_data.get("decimals", 5) + maximum = input_data.get("maximum") + minimum = input_data.get("minimum") + + self.float_input.setDecimals(decimals) + if maximum is not None: + self.float_input.setMaximum(float(maximum)) + if minimum is not None: + self.float_input.setMinimum(float(minimum)) + + if not self._as_widget and not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + layout.addWidget(self.float_input) + + if not self._as_widget: + self.label_widget = label_widget + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + self.default_value = self.item_value() + self.override_value = None + + self.float_input.valueChanged.connect(self._on_value_change) + + def set_default_values(self, default_values): + value = self.value_from_values(default_values) + if isinstance(value, float): + self.set_value(value, default_value=True) + self.default_value = self.item_value() + else: + self.default_value = value + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def set_value(self, value, *, default_value=False): + self.float_input.setValue(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + value = self.default_value + else: + self._is_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def clear_value(self): + self.set_value(0) + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.float_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.float_input.value() + + def config_value(self): + return {self.key: self.item_value()} + + +class TextSingleLineWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + self._parent = parent + self._as_widget = as_widget + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(TextSingleLineWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.text_input = QtWidgets.QLineEdit() + + if not self._as_widget and not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + layout.addWidget(self.text_input) + + if not self._as_widget: + self.label_widget = label_widget + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + self.default_value = self.item_value() + self.override_value = None + + self.text_input.textChanged.connect(self._on_value_change) + + def set_default_values(self, default_values): + value = self.value_from_values(default_values) + if isinstance(value, str): + self.set_value(value, default_value=True) + self.default_value = self.item_value() + else: + self.default_value = value + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def set_value(self, value, *, default_value=False): + self.text_input.setText(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + self._was_overriden = False + value = self.default_value + else: + self._is_overriden = True + self._was_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def clear_value(self): + self.set_value("") + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.text_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.text_input.text() + + def config_value(self): + return {self.key: self.item_value()} + + +class TextMultiLineWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + self._parent = parent + self._as_widget = as_widget + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(TextMultiLineWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.text_input = QtWidgets.QPlainTextEdit() + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + layout.addWidget(self.text_input) + + self.label_widget = label_widget + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + self.default_value = self.item_value() + self.override_value = None + + self.text_input.textChanged.connect(self._on_value_change) + + def set_default_values(self, default_values): + value = self.value_from_values(default_values) + if isinstance(value, str): + self.set_value(value, default_value=True) + self.default_value = self.item_value() + else: + self.default_value = value + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def set_value(self, value, *, default_value=False): + self.text_input.setPlainText(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + value = self.default_value + else: + self._is_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def clear_value(self): + self.set_value("") + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.text_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.text_input.toPlainText() + + def config_value(self): + return {self.key: self.item_value()} + + +class RawJsonInput(QtWidgets.QPlainTextEdit): + tab_length = 4 + + def __init__(self, *args, **kwargs): + super(RawJsonInput, self).__init__(*args, **kwargs) + self.setObjectName("RawJsonInput") + self.setTabStopDistance( + QtGui.QFontMetricsF( + self.font() + ).horizontalAdvance(" ") * self.tab_length + ) + + self.is_valid = None + + def set_value(self, value, *, default_value=False): + self.setPlainText(value) + + def setPlainText(self, *args, **kwargs): + super(RawJsonInput, self).setPlainText(*args, **kwargs) + self.validate() + + def focusOutEvent(self, event): + super(RawJsonInput, self).focusOutEvent(event) + self.validate() + + def validate_value(self, value): + if isinstance(value, str) and not value: + return True + + try: + json.loads(value) + return True + except Exception: + return False + + def update_style(self, is_valid=None): + if is_valid is None: + return self.validate() + + if is_valid != self.is_valid: + self.is_valid = is_valid + if is_valid: + state = "" + else: + state = "invalid" + self.setProperty("state", state) + self.style().polish(self) + + def value(self): + return self.toPlainText() + + def validate(self): + value = self.value() + is_valid = self.validate_value(value) + self.update_style(is_valid) + + +class RawJsonWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + self._parent = parent + self._as_widget = as_widget + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(RawJsonWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.text_input = RawJsonInput() + + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + layout.addWidget(self.text_input) + + self.label_widget = label_widget + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + self.default_value = self.item_value() + self.override_value = None + + self.text_input.textChanged.connect(self._on_value_change) + + def set_default_values(self, default_values): + value = self.value_from_values(default_values) + if isinstance(value, str): + self.set_value(value, default_value=True) + self.default_value = self.item_value() + else: + self.default_value = value + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def set_value(self, value, *, default_value=False): + self.text_input.setPlainText(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def clear_value(self): + self.set_value("") + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + value = self.default_value + else: + self._is_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.text_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.text_input.toPlainText() + + def config_value(self): + return {self.key: self.item_value()} + + +class TextListItem(QtWidgets.QWidget, PypeConfigurationWidget): + _btn_size = 20 + value_changed = QtCore.Signal(object) + + def __init__(self, parent): + super(TextListItem, self).__init__(parent) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(3) + + self.text_input = QtWidgets.QLineEdit() + self.add_btn = QtWidgets.QPushButton("+") + self.remove_btn = QtWidgets.QPushButton("-") + + self.add_btn.setProperty("btn-type", "text-list") + self.remove_btn.setProperty("btn-type", "text-list") + + layout.addWidget(self.text_input, 1) + layout.addWidget(self.add_btn, 0) + layout.addWidget(self.remove_btn, 0) + + self.add_btn.setFixedSize(self._btn_size, self._btn_size) + self.remove_btn.setFixedSize(self._btn_size, self._btn_size) + self.add_btn.clicked.connect(self.on_add_clicked) + self.remove_btn.clicked.connect(self.on_remove_clicked) + + self.text_input.textChanged.connect(self._on_value_change) + + self.is_single = False + + def _on_value_change(self, item=None): + self.value_changed.emit(self) + + def row(self): + return self.parent().input_fields.index(self) + + def on_add_clicked(self): + self.parent().add_row(row=self.row() + 1) + + def on_remove_clicked(self): + if self.is_single: + self.text_input.setText("") + else: + self.parent().remove_row(self) + + def config_value(self): + return self.text_input.text() + + +class TextListSubWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__(self, input_data, as_widget, parent_keys, parent): + super(TextListSubWidget, self).__init__(parent) + self.setObjectName("TextListSubWidget") + + self.as_widget = as_widget + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(5) + self.setLayout(layout) + + self.input_fields = [] + self.add_row() + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + self.default_value = self.item_value() + self.override_value = None + + def set_default_values(self, default_values): + value = self.value_from_values(default_values) + if isinstance(value, (list, tuple)): + self.set_value(value, default_value=True) + self.default_value = self.item_value() + else: + self.default_value = value + + def set_value(self, value, *, default_value=False): + for input_field in self.input_fields: + self.remove_row(input_field) + + for item_text in value: + self.add_row(text=item_text) + + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def clear_value(self): + self.set_value([]) + + def _on_value_change(self, item=None): + self.value_changed.emit(self) + + def count(self): + return len(self.input_fields) + + def add_row(self, row=None, text=None): + # Create new item + item_widget = TextListItem(self) + + # Set/unset if new item is single item + current_count = self.count() + if current_count == 0: + item_widget.is_single = True + elif current_count == 1: + for _input_field in self.input_fields: + _input_field.is_single = False + + item_widget.value_changed.connect(self._on_value_change) + + if row is None: + self.layout().addWidget(item_widget) + self.input_fields.append(item_widget) + else: + self.layout().insertWidget(row, item_widget) + self.input_fields.insert(row, item_widget) + + # Set text if entered text is not None + # else (when add button clicked) trigger `_on_value_change` + if text is not None: + item_widget.text_input.setText(text) + else: + self._on_value_change() + self.parent().updateGeometry() + + def remove_row(self, item_widget): + item_widget.value_changed.disconnect() + + self.layout().removeWidget(item_widget) + self.input_fields.remove(item_widget) + item_widget.setParent(None) + item_widget.deleteLater() + + current_count = self.count() + if current_count == 0: + self.add_row() + elif current_count == 1: + for _input_field in self.input_fields: + _input_field.is_single = True + + self._on_value_change() + self.parent().updateGeometry() + + def item_value(self): + output = [] + for item in self.input_fields: + text = item.config_value() + if text: + output.append(text) + + return output + + def config_value(self): + return {self.key: self.item_value()} + + +class TextListWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + self._parent = parent + self._as_widget = as_widget + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self._is_modified = False + self.is_group = is_group + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(TextListWidget, self).__init__(parent) + self.setObjectName("TextListWidget") + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + + self.label_widget = label_widget + # keys = list(parent_keys) + # keys.append(input_data["key"]) + # self.keys = keys + + self.value_widget = TextListSubWidget( + input_data, values, parent_keys, self + ) + self.value_widget.setAttribute(QtCore.Qt.WA_StyledBackground) + self.value_widget.value_changed.connect(self._on_value_change) + + # self.value_widget.se + self.key = input_data["key"] + layout.addWidget(self.value_widget) + self.setLayout(layout) + + self.default_value = self.item_value() + self.override_value = None + + def set_default_values(self, default_values): + value = self.value_from_values(default_values) + if isinstance(value, (list, tuple)): + self.set_value(value, default_value=True) + self.default_value = self.item_value() + else: + self.default_value = value + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def set_value(self, value, *, default_value=False): + self.value_widget.set_value(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def clear_value(self): + self.set_value([]) + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + value = self.default_value + else: + self._is_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + def item_value(self): + return self.value_widget.config_value() + + def config_value(self): + return {self.key: self.item_value()} + + +class DictExpandWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + if as_widget: + raise TypeError("Can't use \"{}\" as widget item.".format( + self.__class__.__name__ + )) + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + self.any_parent_is_group = any_parent_is_group + + self._is_modified = False + self._is_overriden = False + self.is_group = is_group + + self._state = None + self._child_state = None + + super(DictExpandWidget, self).__init__(parent) + self.setObjectName("DictExpandWidget") + top_part = ClickableWidget(parent=self) + + button_size = QtCore.QSize(5, 5) + button_toggle = QtWidgets.QToolButton(parent=top_part) + button_toggle.setProperty("btn-type", "expand-toggle") + button_toggle.setIconSize(button_size) + button_toggle.setArrowType(QtCore.Qt.RightArrow) + button_toggle.setCheckable(True) + button_toggle.setChecked(False) + + label = input_data["label"] + button_toggle_text = QtWidgets.QLabel(label, parent=top_part) + button_toggle_text.setObjectName("ExpandLabel") + + layout = QtWidgets.QHBoxLayout(top_part) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + layout.addWidget(button_toggle) + layout.addWidget(button_toggle_text) + top_part.setLayout(layout) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(9, 9, 9, 9) + + content_widget = QtWidgets.QWidget(self) + content_widget.setVisible(False) + + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(3, 3, 3, 3) + + main_layout.addWidget(top_part) + main_layout.addWidget(content_widget) + self.setLayout(main_layout) + + self.setAttribute(QtCore.Qt.WA_StyledBackground) + + self.top_part = top_part + self.button_toggle = button_toggle + self.button_toggle_text = button_toggle_text + + self.content_widget = content_widget + self.content_layout = content_layout + + self.top_part.clicked.connect(self._top_part_clicked) + self.button_toggle.clicked.connect(self.toggle_content) + + self.input_fields = [] + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + for child_data in input_data.get("children", []): + self.add_children_gui(child_data, values) + + def set_default_values(self, default_values): + for input_field in self.input_fields: + input_field.set_default_values(default_values) + + def _top_part_clicked(self): + self.toggle_content(not self.button_toggle.isChecked()) + + def toggle_content(self, *args): + if len(args) > 0: + checked = args[0] + else: + checked = self.button_toggle.isChecked() + arrow_type = QtCore.Qt.RightArrow + if checked: + arrow_type = QtCore.Qt.DownArrow + self.button_toggle.setChecked(checked) + self.button_toggle.setArrowType(arrow_type) + self.content_widget.setVisible(checked) + self.parent().updateGeometry() + + def resizeEvent(self, event): + super(DictExpandWidget, self).resizeEvent(event) + self.content_widget.updateGeometry() + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def apply_overrides(self, override_value): + # Make sure this is set to False + self._is_overriden = False + self._state = None + self._child_state = None + for item in self.input_fields: + if override_value is None: + child_value = None + else: + child_value = override_value.get(item.key) + + item.apply_overrides(child_value) + + self._is_overriden = ( + self.is_group + and self.is_overidable + and ( + override_value is not None + or self.child_overriden + ) + ) + self.update_style() + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if self.is_group: + if self.is_overidable: + self._is_overriden = True + + # TODO update items + if item is not None: + for _item in self.input_fields: + if _item is not item: + _item.update_style() + + self.value_changed.emit(self) + + self.update_style() + + def update_style(self, is_overriden=None): + child_modified = self.child_modified + child_state = self.style_state(self.child_overriden, child_modified) + if child_state: + child_state = "child-{}".format(child_state) + + if child_state != self._child_state: + self.setProperty("state", child_state) + self.style().polish(self) + self._child_state = child_state + + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + self.button_toggle_text.setProperty("state", state) + self.button_toggle_text.style().polish(self.button_toggle_text) + + self._state = state + + @property + def is_modified(self): + if self.is_group: + return self.child_modified + return False + + @property + def child_modified(self): + for input_field in self.input_fields: + if input_field.child_modified: + return True + return False + + @property + def child_overriden(self): + for input_field in self.input_fields: + if input_field.child_overriden: + return True + return False + + def item_value(self): + output = {} + for input_field in self.input_fields: + # TODO maybe merge instead of update should be used + # NOTE merge is custom function which merges 2 dicts + output.update(input_field.config_value()) + return output + + def config_value(self): + return {self.key: self.item_value()} + + @property + def is_overidable(self): + return self._parent.is_overidable + + def add_children_gui(self, child_configuration, values): + item_type = child_configuration["type"] + klass = TypeToKlass.types.get(item_type) + + item = klass( + child_configuration, values, self.keys, self + ) + item.value_changed.connect(self._on_value_change) + self.content_layout.addWidget(item) + + self.input_fields.append(item) + return item + + +class DictInvisible(QtWidgets.QWidget, PypeConfigurationWidget): + # TODO is not overridable by itself + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + self.any_parent_is_group = any_parent_is_group + + self._is_overriden = False + self.is_modified = False + self.is_group = is_group + + super(DictInvisible, self).__init__(parent) + self.setObjectName("DictInvisible") + + self.setAttribute(QtCore.Qt.WA_StyledBackground) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + self.input_fields = [] + + self.key = input_data["key"] + self.keys = list(parent_keys) + self.keys.append(self.key) + + for child_data in input_data.get("children", []): + self.add_children_gui(child_data, values) + + def set_default_values(self, default_values): + for input_field in self.input_fields: + input_field.set_default_values(default_values) + + def update_style(self, *args, **kwargs): + return + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def child_modified(self): + for input_field in self.input_fields: + if input_field.child_modified: + return True + return False + + @property + def child_overriden(self): + for input_field in self.input_fields: + if input_field.child_overriden: + return True + return False + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def item_value(self): + output = {} + for input_field in self.input_fields: + # TODO maybe merge instead of update should be used + # NOTE merge is custom function which merges 2 dicts + output.update(input_field.config_value()) + return output + + def config_value(self): + return {self.key: self.item_value()} + + def add_children_gui(self, child_configuration, values): + item_type = child_configuration["type"] + if item_type == "schema": + for _schema in child_configuration["children"]: + children = config.gui_schema(_schema) + self.add_children_gui(children, values) + return + + klass = TypeToKlass.types.get(item_type) + item = klass( + child_configuration, values, self.keys, self + ) + self.layout().addWidget(item) + + item.value_changed.connect(self._on_value_change) + + self.input_fields.append(item) + return item + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if self.is_group: + if self.is_overidable: + self._is_overriden = True + # TODO update items + if item is not None: + is_overriden = self.is_overriden + for _item in self.input_fields: + if _item is not item: + _item.update_style(is_overriden) + + self.value_changed.emit(self) + + def apply_overrides(self, override_value): + self._is_overriden = False + for item in self.input_fields: + if override_value is None: + child_value = None + else: + child_value = override_value.get(item.key) + item.apply_overrides(child_value) + + self._is_overriden = ( + self.is_group + and self.is_overidable + and ( + override_value is not None + or self.child_overriden + ) + ) + self.update_style() + + +class DictFormWidget(QtWidgets.QWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + self.any_parent_is_group = any_parent_is_group + + self.is_modified = False + self.is_overriden = False + self.is_group = False + + super(DictFormWidget, self).__init__(parent) + + self.input_fields = {} + self.content_layout = QtWidgets.QFormLayout(self) + + self.keys = list(parent_keys) + + for child_data in input_data.get("children", []): + self.add_children_gui(child_data, values) + + def set_default_values(self, default_values): + for key, input_field in self.input_fields.items(): + input_field.set_default_values(default_values) + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + self.value_changed.emit(self) + + def item_value(self): + output = {} + for input_field in self.input_fields.values(): + # TODO maybe merge instead of update should be used + # NOTE merge is custom function which merges 2 dicts + output.update(input_field.config_value()) + return output + + @property + def child_modified(self): + for input_field in self.input_fields.values(): + if input_field.child_modified: + return True + return False + + @property + def child_overriden(self): + for input_field in self.input_fields.values(): + if input_field.child_overriden: + return True + return False + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def config_value(self): + return self.item_value() + + def add_children_gui(self, child_configuration, values): + item_type = child_configuration["type"] + key = child_configuration["key"] + # Pop label to not be set in child + label = child_configuration["label"] + + klass = TypeToKlass.types.get(item_type) + + label_widget = QtWidgets.QLabel(label) + + item = klass( + child_configuration, values, self.keys, self, label_widget + ) + item.value_changed.connect(self._on_value_change) + self.content_layout.addRow(label_widget, item) + self.input_fields[key] = item + return item + + +class ModifiableDictItem(QtWidgets.QWidget, PypeConfigurationWidget): + _btn_size = 20 + value_changed = QtCore.Signal(object) + + def __init__(self, object_type, parent): + self._parent = parent + + super(ModifiableDictItem, self).__init__(parent) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(3) + + ItemKlass = TypeToKlass.types[object_type] + + self.key_input = QtWidgets.QLineEdit() + self.key_input.setObjectName("DictKey") + + self.value_input = ItemKlass( + {}, + AS_WIDGET, + [], + self, + None + ) + self.add_btn = QtWidgets.QPushButton("+") + self.remove_btn = QtWidgets.QPushButton("-") + + self.add_btn.setProperty("btn-type", "text-list") + self.remove_btn.setProperty("btn-type", "text-list") + + layout.addWidget(self.key_input, 0) + layout.addWidget(self.value_input, 1) + layout.addWidget(self.add_btn, 0) + layout.addWidget(self.remove_btn, 0) + + self.add_btn.setFixedSize(self._btn_size, self._btn_size) + self.remove_btn.setFixedSize(self._btn_size, self._btn_size) + self.add_btn.clicked.connect(self.on_add_clicked) + self.remove_btn.clicked.connect(self.on_remove_clicked) + + self.key_input.textChanged.connect(self._on_value_change) + self.value_input.value_changed.connect(self._on_value_change) + + self.default_key = self._key() + self.default_value = self.value_input.item_value() + + self.override_key = None + self.override_value = None + + self.is_single = False + + def _key(self): + return self.key_input.text() + + def _on_value_change(self, item=None): + self.update_style() + self.value_changed.emit(self) + + @property + def is_group(self): + return self._parent.is_group + + @property + def any_parent_is_group(self): + return self._parent.any_parent_is_group + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def is_key_modified(self): + return self._key() != self.default_key + + def is_value_modified(self): + return self.value_input.is_modified + + @property + def is_modified(self): + return self.is_value_modified() or self.is_key_modified() + + def update_style(self): + if self.is_key_modified(): + state = "modified" + else: + state = "" + + self.key_input.setProperty("state", state) + self.key_input.style().polish(self.key_input) + + def row(self): + return self.parent().input_fields.index(self) + + def on_add_clicked(self): + self.parent().add_row(row=self.row() + 1) + + def on_remove_clicked(self): + if self.is_single: + self.value_input.clear_value() + self.key_input.setText("") + else: + self.parent().remove_row(self) + + def config_value(self): + key = self.key_input.text() + value = self.value_input.item_value() + if not key: + return {} + return {key: value} + + +class ModifiableDictSubWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__(self, input_data, as_widget, parent_keys, parent): + self._parent = parent + + super(ModifiableDictSubWidget, self).__init__(parent) + self.setObjectName("ModifiableDictSubWidget") + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(5) + self.setLayout(layout) + + self.input_fields = [] + self.object_type = input_data["object_type"] + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + if self.count() == 0: + self.add_row() + + self.default_value = self.config_value() + self.override_value = None + + def set_default_values(self, default_values): + for input_field in self.input_fields: + self.remove_row(input_field) + + value = self.value_from_values(default_values) + if value is NOT_SET: + self.defaul_value = value + return + + for item_key, item_value in value.items(): + self.add_row(key=item_key, value=item_value) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._parent.is_overriden + + @property + def is_group(self): + return self._parent.is_group + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + @property + def any_parent_is_group(self): + return self._parent.any_parent_is_group + + def _on_value_change(self, item=None): + self.value_changed.emit(self) + + def count(self): + return len(self.input_fields) + + def add_row(self, row=None, key=None, value=None): + # Create new item + item_widget = ModifiableDictItem(self.object_type, self) + + # Set/unset if new item is single item + current_count = self.count() + if current_count == 0: + item_widget.is_single = True + elif current_count == 1: + for _input_field in self.input_fields: + _input_field.is_single = False + + item_widget.value_changed.connect(self._on_value_change) + + if row is None: + self.layout().addWidget(item_widget) + self.input_fields.append(item_widget) + else: + self.layout().insertWidget(row, item_widget) + self.input_fields.insert(row, item_widget) + + # Set value if entered value is not None + # else (when add button clicked) trigger `_on_value_change` + if value is not None and key is not None: + item_widget.default_key = key + item_widget.key_input.setText(key) + item_widget.value_input.set_value(value, default_value=True) + else: + self._on_value_change() + self.parent().updateGeometry() + + def remove_row(self, item_widget): + item_widget.value_changed.disconnect() + + self.layout().removeWidget(item_widget) + self.input_fields.remove(item_widget) + item_widget.setParent(None) + item_widget.deleteLater() + + current_count = self.count() + if current_count == 0: + self.add_row() + elif current_count == 1: + for _input_field in self.input_fields: + _input_field.is_single = True + + self._on_value_change() + self.parent().updateGeometry() + + def config_value(self): + output = {} + for item in self.input_fields: + item_value = item.config_value() + if item_value: + output.update(item_value) + return output + + +class ModifiableDict(ExpandingWidget, PypeConfigurationWidget): + # Should be used only for dictionary with one datatype as value + # TODO this is actually input field (do not care if is group or not) + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent_keys, parent, + as_widget=False, label_widget=None + ): + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.any_parent_is_group = any_parent_is_group + + self.is_group = is_group + self._is_modified = False + self._is_overriden = False + self._was_overriden = False + self._state = None + + super(ModifiableDict, self).__init__(input_data["label"], parent) + self.setObjectName("ModifiableDict") + + self.value_widget = ModifiableDictSubWidget( + input_data, as_widget, parent_keys, self + ) + self.value_widget.setAttribute(QtCore.Qt.WA_StyledBackground) + self.value_widget.value_changed.connect(self._on_value_change) + + self.set_content_widget(self.value_widget) + + self.key = input_data["key"] + + self.default_value = self.item_value() + self.override_value = None + + def set_default_values(self, default_values): + self.value_widget.set_default_values(default_values) + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if self.is_overidable: + self._is_overriden = True + + if self.is_overriden: + self._is_modified = self.item_value() != self.override_value + else: + self._is_modified = self.item_value() != self.default_value + + self.value_changed.emit(self) + + self.update_style() + + @property + def child_modified(self): + return self.is_modified + + @property + def is_modified(self): + return self._is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def is_modified(self): + return self._is_modified + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def apply_overrides(self, override_value): + self._state = None + self._is_modified = False + self.override_value = override_value + if override_value is None: + self._is_overriden = False + self._was_overriden = False + value = self.default_value + else: + self._is_overriden = True + self._was_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if state: + child_state = "child-{}".format(state) + else: + child_state = "" + + self.setProperty("state", child_state) + self.style().polish(self) + + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + self._state = state + + def item_value(self): + return self.value_widget.config_value() + + def config_value(self): + return {self.key: self.item_value()} + + +TypeToKlass.types["boolean"] = BooleanWidget +TypeToKlass.types["text-singleline"] = TextSingleLineWidget +TypeToKlass.types["text-multiline"] = TextMultiLineWidget +TypeToKlass.types["raw-json"] = RawJsonWidget +TypeToKlass.types["int"] = IntegerWidget +TypeToKlass.types["float"] = FloatWidget +TypeToKlass.types["dict-expanding"] = DictExpandWidget +TypeToKlass.types["dict-form"] = DictFormWidget +TypeToKlass.types["dict-invisible"] = DictInvisible +TypeToKlass.types["dict-modifiable"] = ModifiableDict +TypeToKlass.types["list-text"] = TextListWidget diff --git a/pype/tools/config_setting/widgets/inputs1.py b/pype/tools/config_setting/widgets/inputs1.py new file mode 100644 index 0000000000..f9eb60f31a --- /dev/null +++ b/pype/tools/config_setting/widgets/inputs1.py @@ -0,0 +1,2131 @@ +import json +from Qt import QtWidgets, QtCore, QtGui +from . import config +from .base import PypeConfigurationWidget, TypeToKlass +from .widgets import ( + ClickableWidget, + ExpandingWidget, + ModifiedIntSpinBox, + ModifiedFloatSpinBox +) +from .lib import NOT_SET, AS_WIDGET + + +class SchemeGroupHierarchyBug(Exception): + def __init__(self, msg=None): + if not msg: + # TODO better message + msg = "SCHEME BUG: Attribute `is_group` is mixed in the hierarchy" + super(SchemeGroupHierarchyBug, self).__init__(msg) + + +class BooleanWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, label_widget=None + ): + self._as_widget = values is AS_WIDGET + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(BooleanWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.checkbox = QtWidgets.QCheckBox() + self.checkbox.setAttribute(QtCore.Qt.WA_StyledBackground) + if not self._as_widget and not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + label_widget.setAttribute(QtCore.Qt.WA_StyledBackground) + layout.addWidget(label_widget) + + layout.addWidget(self.checkbox) + + if not self._as_widget: + self.label_widget = label_widget + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + value = self.value_from_values(values) + if value is not NOT_SET: + self.checkbox.setChecked(value) + + self.default_value = self.item_value() + self.override_value = None + + self.checkbox.stateChanged.connect(self._on_value_change) + + def set_value(self, value, *, default_value=False): + # Ignore value change because if `self.isChecked()` has same + # value as `value` the `_on_value_change` is not triggered + self.checkbox.setChecked(value) + + if default_value: + self.default_value = self.item_value() + + self._on_value_change() + + def reset_value(self): + if self.is_overidable and self.override_value is not None: + self.set_value(self.override_value) + else: + self.set_value(self.default_value) + + def clear_value(self): + self.reset_value() + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + self._was_overriden = False + value = self.default_value + else: + self._is_overriden = True + self._was_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + _value = self.item_value() + is_modified = None + if self.is_overidable: + self._is_overriden = True + if self.override_value is not None: + is_modified = _value != self.override_value + + if is_modified is None: + is_modified = _value != self.default_value + + self._is_modified = is_modified + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + else: + property_name = "state" + + self.label_widget.setProperty(property_name, state) + self.label_widget.style().polish(self.label_widget) + self._state = state + + def item_value(self): + return self.checkbox.isChecked() + + def config_value(self): + return {self.key: self.item_value()} + + def override_value(self): + if self.is_overriden: + output = { + "is_group": self.is_group, + "value": self.config_value() + } + return output + + +class IntegerWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, label_widget=None + ): + self._parent = parent + self._as_widget = values is AS_WIDGET + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(IntegerWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.int_input = ModifiedIntSpinBox() + + if not self._as_widget and not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + layout.addWidget(self.int_input) + + if not self._as_widget: + self.label_widget = label_widget + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + value = self.value_from_values(values) + if value is not NOT_SET: + self.int_input.setValue(value) + + self.default_value = self.item_value() + self.override_value = None + + self.int_input.valueChanged.connect(self._on_value_change) + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def set_value(self, value, *, default_value=False): + self.int_input.setValue(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def clear_value(self): + self.set_value(0) + + def reset_value(self): + self.set_value(self.default_value) + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + self._was_overriden = False + value = self.default_value + else: + self._is_overriden = True + self._was_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.int_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.int_input.value() + + def config_value(self): + return {self.key: self.item_value()} + + +class FloatWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, label_widget=None + ): + self._parent = parent + self._as_widget = values is AS_WIDGET + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(FloatWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.float_input = ModifiedFloatSpinBox() + + decimals = input_data.get("decimals", 5) + maximum = input_data.get("maximum") + minimum = input_data.get("minimum") + + self.float_input.setDecimals(decimals) + if maximum is not None: + self.float_input.setMaximum(float(maximum)) + if minimum is not None: + self.float_input.setMinimum(float(minimum)) + + if not self._as_widget and not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + layout.addWidget(self.float_input) + + if not self._as_widget: + self.label_widget = label_widget + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + value = self.value_from_values(values) + if value is not NOT_SET: + self.float_input.setValue(value) + + self.default_value = self.item_value() + self.override_value = None + + self.float_input.valueChanged.connect(self._on_value_change) + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def set_value(self, value, *, default_value=False): + self.float_input.setValue(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + value = self.default_value + else: + self._is_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def clear_value(self): + self.set_value(0) + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.float_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.float_input.value() + + def config_value(self): + return {self.key: self.item_value()} + + +class TextSingleLineWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, label_widget=None + ): + self._parent = parent + self._as_widget = values is AS_WIDGET + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(TextSingleLineWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.text_input = QtWidgets.QLineEdit() + + if not self._as_widget and not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + layout.addWidget(self.text_input) + + if not self._as_widget: + self.label_widget = label_widget + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + value = self.value_from_values(values) + if value is not NOT_SET: + self.text_input.setText(value) + + self.default_value = self.item_value() + self.override_value = None + + self.text_input.textChanged.connect(self._on_value_change) + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def set_value(self, value, *, default_value=False): + self.text_input.setText(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + self._was_overriden = False + value = self.default_value + else: + self._is_overriden = True + self._was_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def clear_value(self): + self.set_value("") + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.text_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.text_input.text() + + def config_value(self): + return {self.key: self.item_value()} + + +class TextMultiLineWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, label_widget=None + ): + self._parent = parent + self._as_widget = values is AS_WIDGET + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(TextMultiLineWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.text_input = QtWidgets.QPlainTextEdit() + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + layout.addWidget(self.text_input) + + self.label_widget = label_widget + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + value = self.value_from_values(values) + if value is not NOT_SET: + self.text_input.setPlainText(value) + + self.default_value = self.item_value() + self.override_value = None + + self.text_input.textChanged.connect(self._on_value_change) + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def set_value(self, value, *, default_value=False): + self.text_input.setPlainText(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + value = self.default_value + else: + self._is_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def clear_value(self): + self.set_value("") + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.text_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.text_input.toPlainText() + + def config_value(self): + return {self.key: self.item_value()} + + +class RawJsonInput(QtWidgets.QPlainTextEdit): + tab_length = 4 + + def __init__(self, *args, **kwargs): + super(RawJsonInput, self).__init__(*args, **kwargs) + self.setObjectName("RawJsonInput") + self.setTabStopDistance( + QtGui.QFontMetricsF( + self.font() + ).horizontalAdvance(" ") * self.tab_length + ) + + self.is_valid = None + + def set_value(self, value, *, default_value=False): + self.setPlainText(value) + + def setPlainText(self, *args, **kwargs): + super(RawJsonInput, self).setPlainText(*args, **kwargs) + self.validate() + + def focusOutEvent(self, event): + super(RawJsonInput, self).focusOutEvent(event) + self.validate() + + def validate_value(self, value): + if isinstance(value, str) and not value: + return True + + try: + json.loads(value) + return True + except Exception: + return False + + def update_style(self, is_valid=None): + if is_valid is None: + return self.validate() + + if is_valid != self.is_valid: + self.is_valid = is_valid + if is_valid: + state = "" + else: + state = "invalid" + self.setProperty("state", state) + self.style().polish(self) + + def value(self): + return self.toPlainText() + + def validate(self): + value = self.value() + is_valid = self.validate_value(value) + self.update_style(is_valid) + + +class RawJsonWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, label_widget=None + ): + self._parent = parent + self._as_widget = values is AS_WIDGET + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.is_group = is_group + self._is_modified = False + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(RawJsonWidget, self).__init__(parent) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.text_input = RawJsonInput() + + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + layout.addWidget(self.text_input) + + self.label_widget = label_widget + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + value = self.value_from_values(values) + if value is not NOT_SET: + self.text_input.setPlainText(value) + + self.default_value = self.item_value() + self.override_value = None + + self.text_input.textChanged.connect(self._on_value_change) + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def set_value(self, value, *, default_value=False): + self.text_input.setPlainText(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def clear_value(self): + self.set_value("") + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + value = self.default_value + else: + self._is_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + widget = self.text_input + else: + property_name = "state" + widget = self.label_widget + + widget.setProperty(property_name, state) + widget.style().polish(widget) + + def item_value(self): + return self.text_input.toPlainText() + + def config_value(self): + return {self.key: self.item_value()} + + +class TextListItem(QtWidgets.QWidget, PypeConfigurationWidget): + _btn_size = 20 + value_changed = QtCore.Signal(object) + + def __init__(self, parent): + super(TextListItem, self).__init__(parent) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(3) + + self.text_input = QtWidgets.QLineEdit() + self.add_btn = QtWidgets.QPushButton("+") + self.remove_btn = QtWidgets.QPushButton("-") + + self.add_btn.setProperty("btn-type", "text-list") + self.remove_btn.setProperty("btn-type", "text-list") + + layout.addWidget(self.text_input, 1) + layout.addWidget(self.add_btn, 0) + layout.addWidget(self.remove_btn, 0) + + self.add_btn.setFixedSize(self._btn_size, self._btn_size) + self.remove_btn.setFixedSize(self._btn_size, self._btn_size) + self.add_btn.clicked.connect(self.on_add_clicked) + self.remove_btn.clicked.connect(self.on_remove_clicked) + + self.text_input.textChanged.connect(self._on_value_change) + + self.is_single = False + + def _on_value_change(self, item=None): + self.value_changed.emit(self) + + def row(self): + return self.parent().input_fields.index(self) + + def on_add_clicked(self): + self.parent().add_row(row=self.row() + 1) + + def on_remove_clicked(self): + if self.is_single: + self.text_input.setText("") + else: + self.parent().remove_row(self) + + def config_value(self): + return self.text_input.text() + + +class TextListSubWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__(self, input_data, values, parent_keys, parent): + super(TextListSubWidget, self).__init__(parent) + self.setObjectName("TextListSubWidget") + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(5) + self.setLayout(layout) + + self.input_fields = [] + self.add_row() + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + value = self.value_from_values(values) + if value is not NOT_SET: + self.set_value(value) + + self.default_value = self.item_value() + self.override_value = None + + def set_value(self, value, *, default_value=False): + for input_field in self.input_fields: + self.remove_row(input_field) + + for item_text in value: + self.add_row(text=item_text) + + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def clear_value(self): + self.set_value([]) + + def _on_value_change(self, item=None): + self.value_changed.emit(self) + + def count(self): + return len(self.input_fields) + + def add_row(self, row=None, text=None): + # Create new item + item_widget = TextListItem(self) + + # Set/unset if new item is single item + current_count = self.count() + if current_count == 0: + item_widget.is_single = True + elif current_count == 1: + for _input_field in self.input_fields: + _input_field.is_single = False + + item_widget.value_changed.connect(self._on_value_change) + + if row is None: + self.layout().addWidget(item_widget) + self.input_fields.append(item_widget) + else: + self.layout().insertWidget(row, item_widget) + self.input_fields.insert(row, item_widget) + + # Set text if entered text is not None + # else (when add button clicked) trigger `_on_value_change` + if text is not None: + item_widget.text_input.setText(text) + else: + self._on_value_change() + self.parent().updateGeometry() + + def remove_row(self, item_widget): + item_widget.value_changed.disconnect() + + self.layout().removeWidget(item_widget) + self.input_fields.remove(item_widget) + item_widget.setParent(None) + item_widget.deleteLater() + + current_count = self.count() + if current_count == 0: + self.add_row() + elif current_count == 1: + for _input_field in self.input_fields: + _input_field.is_single = True + + self._on_value_change() + self.parent().updateGeometry() + + def item_value(self): + output = [] + for item in self.input_fields: + text = item.config_value() + if text: + output.append(text) + + return output + + def config_value(self): + return {self.key: self.item_value()} + + +class TextListWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, label_widget=None + ): + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self._is_modified = False + self.is_group = is_group + self._was_overriden = False + self._is_overriden = False + + self._state = None + + super(TextListWidget, self).__init__(parent) + self.setObjectName("TextListWidget") + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + if not label_widget: + label = input_data["label"] + label_widget = QtWidgets.QLabel(label) + layout.addWidget(label_widget) + + self.label_widget = label_widget + # keys = list(parent_keys) + # keys.append(input_data["key"]) + # self.keys = keys + + self.value_widget = TextListSubWidget( + input_data, values, parent_keys, self + ) + self.value_widget.setAttribute(QtCore.Qt.WA_StyledBackground) + self.value_widget.value_changed.connect(self._on_value_change) + + # self.value_widget.se + self.key = input_data["key"] + layout.addWidget(self.value_widget) + self.setLayout(layout) + + self.default_value = self.item_value() + self.override_value = None + + @property + def child_modified(self): + return self.is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_modified(self): + return self._is_modified or (self._was_overriden != self.is_overriden) + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + self._is_modified = self.item_value() != self.default_value + if self.is_overidable: + self._is_overriden = True + + self.update_style() + + self.value_changed.emit(self) + + def set_value(self, value, *, default_value=False): + self.value_widget.set_value(value) + if default_value: + self.default_value = self.item_value() + self._on_value_change() + + def reset_value(self): + self.set_value(self.default_value) + + def clear_value(self): + self.set_value([]) + + def apply_overrides(self, override_value): + self._is_modified = False + self._state = None + self.override_value = override_value + if override_value is None: + self._is_overriden = False + value = self.default_value + else: + self._is_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + def item_value(self): + return self.value_widget.config_value() + + def config_value(self): + return {self.key: self.item_value()} + + +class DictExpandWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, label_widget=None + ): + if values is AS_WIDGET: + raise TypeError("Can't use \"{}\" as widget item.".format( + self.__class__.__name__ + )) + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + self.any_parent_is_group = any_parent_is_group + + self._is_modified = False + self._is_overriden = False + self.is_group = is_group + + self._state = None + self._child_state = None + + super(DictExpandWidget, self).__init__(parent) + self.setObjectName("DictExpandWidget") + top_part = ClickableWidget(parent=self) + + button_size = QtCore.QSize(5, 5) + button_toggle = QtWidgets.QToolButton(parent=top_part) + button_toggle.setProperty("btn-type", "expand-toggle") + button_toggle.setIconSize(button_size) + button_toggle.setArrowType(QtCore.Qt.RightArrow) + button_toggle.setCheckable(True) + button_toggle.setChecked(False) + + label = input_data["label"] + button_toggle_text = QtWidgets.QLabel(label, parent=top_part) + button_toggle_text.setObjectName("ExpandLabel") + + layout = QtWidgets.QHBoxLayout(top_part) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + layout.addWidget(button_toggle) + layout.addWidget(button_toggle_text) + top_part.setLayout(layout) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(9, 9, 9, 9) + + content_widget = QtWidgets.QWidget(self) + content_widget.setVisible(False) + + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(3, 3, 3, 3) + + main_layout.addWidget(top_part) + main_layout.addWidget(content_widget) + self.setLayout(main_layout) + + self.setAttribute(QtCore.Qt.WA_StyledBackground) + + self.top_part = top_part + self.button_toggle = button_toggle + self.button_toggle_text = button_toggle_text + + self.content_widget = content_widget + self.content_layout = content_layout + + self.top_part.clicked.connect(self._top_part_clicked) + self.button_toggle.clicked.connect(self.toggle_content) + + self.input_fields = [] + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + for child_data in input_data.get("children", []): + self.add_children_gui(child_data, values) + + def _top_part_clicked(self): + self.toggle_content(not self.button_toggle.isChecked()) + + def toggle_content(self, *args): + if len(args) > 0: + checked = args[0] + else: + checked = self.button_toggle.isChecked() + arrow_type = QtCore.Qt.RightArrow + if checked: + arrow_type = QtCore.Qt.DownArrow + self.button_toggle.setChecked(checked) + self.button_toggle.setArrowType(arrow_type) + self.content_widget.setVisible(checked) + self.parent().updateGeometry() + + def resizeEvent(self, event): + super(DictExpandWidget, self).resizeEvent(event) + self.content_widget.updateGeometry() + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def apply_overrides(self, override_value): + # Make sure this is set to False + self._is_overriden = False + self._state = None + self._child_state = None + for item in self.input_fields: + if override_value is None: + child_value = None + else: + child_value = override_value.get(item.key) + + item.apply_overrides(child_value) + + self._is_overriden = ( + self.is_group + and self.is_overidable + and ( + override_value is not None + or self.child_overriden + ) + ) + self.update_style() + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if self.is_group: + if self.is_overidable: + self._is_overriden = True + + # TODO update items + if item is not None: + for _item in self.input_fields: + if _item is not item: + _item.update_style() + + self.value_changed.emit(self) + + self.update_style() + + def update_style(self, is_overriden=None): + child_modified = self.child_modified + child_state = self.style_state(self.child_overriden, child_modified) + if child_state: + child_state = "child-{}".format(child_state) + + if child_state != self._child_state: + self.setProperty("state", child_state) + self.style().polish(self) + self._child_state = child_state + + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + self.button_toggle_text.setProperty("state", state) + self.button_toggle_text.style().polish(self.button_toggle_text) + + self._state = state + + @property + def is_modified(self): + if self.is_group: + return self.child_modified + return False + + @property + def child_modified(self): + for input_field in self.input_fields: + if input_field.child_modified: + return True + return False + + @property + def child_overriden(self): + for input_field in self.input_fields: + if input_field.child_overriden: + return True + return False + + def item_value(self): + output = {} + for input_field in self.input_fields: + # TODO maybe merge instead of update should be used + # NOTE merge is custom function which merges 2 dicts + output.update(input_field.config_value()) + return output + + def config_value(self): + return {self.key: self.item_value()} + + @property + def is_overidable(self): + return self._parent.is_overidable + + def add_children_gui(self, child_configuration, values): + item_type = child_configuration["type"] + klass = TypeToKlass.types.get(item_type) + + item = klass( + child_configuration, values, self.keys, self + ) + item.value_changed.connect(self._on_value_change) + self.content_layout.addWidget(item) + + self.input_fields.append(item) + return item + + def override_values(self): + if not self.is_overriden and not self.child_overriden: + return + + value = {} + for item in self.input_fields: + if hasattr(item, "override_values"): + print("*** HAVE `override_values`", item) + print(item.override_values()) + else: + print("*** missing `override_values`", item) + + if not value: + return + + output = { + "is_group": self.is_group, + "value": value + } + return output + + +class DictInvisible(QtWidgets.QWidget, PypeConfigurationWidget): + # TODO is not overridable by itself + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, label_widget=None + ): + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + self.any_parent_is_group = any_parent_is_group + + self._is_overriden = False + self.is_modified = False + self.is_group = is_group + + super(DictInvisible, self).__init__(parent) + self.setObjectName("DictInvisible") + + self.setAttribute(QtCore.Qt.WA_StyledBackground) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + self.input_fields = [] + + if "key" not in input_data: + print(json.dumps(input_data, indent=4)) + + self.key = input_data["key"] + self.keys = list(parent_keys) + self.keys.append(self.key) + + for child_data in input_data.get("children", []): + self.add_children_gui(child_data, values) + + def update_style(self, *args, **kwargs): + return + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def child_modified(self): + for input_field in self.input_fields: + if input_field.child_modified: + return True + return False + + @property + def child_overriden(self): + for input_field in self.input_fields: + if input_field.child_overriden: + return True + return False + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def item_value(self): + output = {} + for input_field in self.input_fields: + # TODO maybe merge instead of update should be used + # NOTE merge is custom function which merges 2 dicts + output.update(input_field.config_value()) + return output + + def config_value(self): + return {self.key: self.item_value()} + + def add_children_gui(self, child_configuration, values): + item_type = child_configuration["type"] + if item_type == "schema": + for _schema in child_configuration["children"]: + children = config.gui_schema(_schema) + self.add_children_gui(children, values) + return + + klass = TypeToKlass.types.get(item_type) + item = klass( + child_configuration, values, self.keys, self + ) + self.layout().addWidget(item) + + item.value_changed.connect(self._on_value_change) + + self.input_fields.append(item) + return item + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if self.is_group: + if self.is_overidable: + self._is_overriden = True + # TODO update items + if item is not None: + is_overriden = self.is_overriden + for _item in self.input_fields: + if _item is not item: + _item.update_style(is_overriden) + + self.value_changed.emit(self) + + def apply_overrides(self, override_value): + self._is_overriden = False + for item in self.input_fields: + if override_value is None: + child_value = None + else: + child_value = override_value.get(item.key) + item.apply_overrides(child_value) + + self._is_overriden = ( + self.is_group + and self.is_overidable + and ( + override_value is not None + or self.child_overriden + ) + ) + self.update_style() + + def override_values(self): + if not self.is_overriden and not self.child_overriden: + return + + value = {} + for item in self.input_fields: + if hasattr(item, "override_values"): + print("*** HAVE `override_values`", item) + print(item.override_values()) + else: + print("*** missing `override_values`", item) + + if not value: + return + + output = { + "is_group": self.is_group, + "value": value + } + return output + + +class DictFormWidget(QtWidgets.QWidget): + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, label_widget=None + ): + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + self.any_parent_is_group = any_parent_is_group + + self.is_modified = False + self.is_overriden = False + self.is_group = False + + super(DictFormWidget, self).__init__(parent) + + self.input_fields = {} + self.content_layout = QtWidgets.QFormLayout(self) + + self.keys = list(parent_keys) + + for child_data in input_data.get("children", []): + self.add_children_gui(child_data, values) + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + self.value_changed.emit(self) + + def item_value(self): + output = {} + for input_field in self.input_fields.values(): + # TODO maybe merge instead of update should be used + # NOTE merge is custom function which merges 2 dicts + output.update(input_field.config_value()) + return output + + @property + def child_modified(self): + for input_field in self.input_fields.values(): + if input_field.child_modified: + return True + return False + + @property + def child_overriden(self): + for input_field in self.input_fields.values(): + if input_field.child_overriden: + return True + return False + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def config_value(self): + return self.item_value() + + def add_children_gui(self, child_configuration, values): + item_type = child_configuration["type"] + key = child_configuration["key"] + # Pop label to not be set in child + label = child_configuration["label"] + + klass = TypeToKlass.types.get(item_type) + + label_widget = QtWidgets.QLabel(label) + + item = klass( + child_configuration, values, self.keys, self, label_widget + ) + item.value_changed.connect(self._on_value_change) + self.content_layout.addRow(label_widget, item) + self.input_fields[key] = item + return item + + +class ModifiableDictItem(QtWidgets.QWidget, PypeConfigurationWidget): + _btn_size = 20 + value_changed = QtCore.Signal(object) + + def __init__(self, object_type, parent): + self._parent = parent + + super(ModifiableDictItem, self).__init__(parent) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(3) + + ItemKlass = TypeToKlass.types[object_type] + + self.key_input = QtWidgets.QLineEdit() + self.key_input.setObjectName("DictKey") + + self.value_input = ItemKlass( + {}, + AS_WIDGET, + [], + self, + None + ) + self.add_btn = QtWidgets.QPushButton("+") + self.remove_btn = QtWidgets.QPushButton("-") + + self.add_btn.setProperty("btn-type", "text-list") + self.remove_btn.setProperty("btn-type", "text-list") + + layout.addWidget(self.key_input, 0) + layout.addWidget(self.value_input, 1) + layout.addWidget(self.add_btn, 0) + layout.addWidget(self.remove_btn, 0) + + self.add_btn.setFixedSize(self._btn_size, self._btn_size) + self.remove_btn.setFixedSize(self._btn_size, self._btn_size) + self.add_btn.clicked.connect(self.on_add_clicked) + self.remove_btn.clicked.connect(self.on_remove_clicked) + + self.key_input.textChanged.connect(self._on_value_change) + self.value_input.value_changed.connect(self._on_value_change) + + self.default_key = self._key() + self.default_value = self.value_input.item_value() + + self.override_key = None + self.override_value = None + + self.is_single = False + + def _key(self): + return self.key_input.text() + + def _on_value_change(self, item=None): + self.update_style() + self.value_changed.emit(self) + + @property + def is_group(self): + return self._parent.is_group + + @property + def any_parent_is_group(self): + return self._parent.any_parent_is_group + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._parent.is_overriden + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def is_key_modified(self): + return self._key() != self.default_key + + def is_value_modified(self): + return self.value_input.is_modified + + @property + def is_modified(self): + return self.is_value_modified() or self.is_key_modified() + + def update_style(self): + if self.is_key_modified(): + state = "modified" + else: + state = "" + + self.key_input.setProperty("state", state) + self.key_input.style().polish(self.key_input) + + def row(self): + return self.parent().input_fields.index(self) + + def on_add_clicked(self): + self.parent().add_row(row=self.row() + 1) + + def on_remove_clicked(self): + if self.is_single: + self.value_input.clear_value() + self.key_input.setText("") + else: + self.parent().remove_row(self) + + def config_value(self): + key = self.key_input.text() + value = self.value_input.item_value() + if not key: + return {} + return {key: value} + + +class ModifiableDictSubWidget(QtWidgets.QWidget, PypeConfigurationWidget): + value_changed = QtCore.Signal(object) + + def __init__(self, input_data, values, parent_keys, parent): + self._parent = parent + + super(ModifiableDictSubWidget, self).__init__(parent) + self.setObjectName("ModifiableDictSubWidget") + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(5) + self.setLayout(layout) + + self.input_fields = [] + self.object_type = input_data["object_type"] + + self.key = input_data["key"] + keys = list(parent_keys) + keys.append(self.key) + self.keys = keys + + value = self.value_from_values(values) + if value is not NOT_SET: + for item_key, item_value in value.items(): + self.add_row(key=item_key, value=item_value) + + if self.count() == 0: + self.add_row() + + self.default_value = self.config_value() + self.override_value = None + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._parent.is_overriden + + @property + def is_group(self): + return self._parent.is_group + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + @property + def any_parent_is_group(self): + return self._parent.any_parent_is_group + + def _on_value_change(self, item=None): + self.value_changed.emit(self) + + def count(self): + return len(self.input_fields) + + def add_row(self, row=None, key=None, value=None): + # Create new item + item_widget = ModifiableDictItem(self.object_type, self) + + # Set/unset if new item is single item + current_count = self.count() + if current_count == 0: + item_widget.is_single = True + elif current_count == 1: + for _input_field in self.input_fields: + _input_field.is_single = False + + item_widget.value_changed.connect(self._on_value_change) + + if row is None: + self.layout().addWidget(item_widget) + self.input_fields.append(item_widget) + else: + self.layout().insertWidget(row, item_widget) + self.input_fields.insert(row, item_widget) + + # Set value if entered value is not None + # else (when add button clicked) trigger `_on_value_change` + if value is not None and key is not None: + item_widget.default_key = key + item_widget.key_input.setText(key) + item_widget.value_input.set_value(value, default_value=True) + else: + self._on_value_change() + self.parent().updateGeometry() + + def remove_row(self, item_widget): + item_widget.value_changed.disconnect() + + self.layout().removeWidget(item_widget) + self.input_fields.remove(item_widget) + item_widget.setParent(None) + item_widget.deleteLater() + + current_count = self.count() + if current_count == 0: + self.add_row() + elif current_count == 1: + for _input_field in self.input_fields: + _input_field.is_single = True + + self._on_value_change() + self.parent().updateGeometry() + + def config_value(self): + output = {} + for item in self.input_fields: + item_value = item.config_value() + if item_value: + output.update(item_value) + return output + + +class ModifiableDict(ExpandingWidget, PypeConfigurationWidget): + # Should be used only for dictionary with one datatype as value + # TODO this is actually input field (do not care if is group or not) + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, values, parent_keys, parent, + label_widget=None + ): + self._parent = parent + + any_parent_is_group = parent.is_group + if not any_parent_is_group: + any_parent_is_group = parent.any_parent_is_group + + is_group = input_data.get("is_group", False) + if is_group and any_parent_is_group: + raise SchemeGroupHierarchyBug() + + if not any_parent_is_group and not is_group: + is_group = True + + self.any_parent_is_group = any_parent_is_group + + self.is_group = is_group + self._is_modified = False + self._is_overriden = False + self._was_overriden = False + self._state = None + + super(ModifiableDict, self).__init__(input_data["label"], parent) + self.setObjectName("ModifiableDict") + + self.value_widget = ModifiableDictSubWidget( + input_data, values, parent_keys, self + ) + self.value_widget.setAttribute(QtCore.Qt.WA_StyledBackground) + self.value_widget.value_changed.connect(self._on_value_change) + + self.set_content_widget(self.value_widget) + + self.key = input_data["key"] + + self.default_value = self.item_value() + self.override_value = None + + def _on_value_change(self, item=None): + if self.ignore_value_changes: + return + + if self.is_overidable: + self._is_overriden = True + + if self.is_overriden: + self._is_modified = self.item_value() != self.override_value + else: + self._is_modified = self.item_value() != self.default_value + + self.value_changed.emit(self) + + self.update_style() + + @property + def child_modified(self): + return self.is_modified + + @property + def is_modified(self): + return self._is_modified + + @property + def child_overriden(self): + return self._is_overriden + + @property + def is_overidable(self): + return self._parent.is_overidable + + @property + def is_overriden(self): + return self._is_overriden or self._parent.is_overriden + + @property + def is_modified(self): + return self._is_modified + + @property + def ignore_value_changes(self): + return self._parent.ignore_value_changes + + def apply_overrides(self, override_value): + self._state = None + self._is_modified = False + self.override_value = override_value + if override_value is None: + self._is_overriden = False + self._was_overriden = False + value = self.default_value + else: + self._is_overriden = True + self._was_overriden = True + value = override_value + + self.set_value(value) + self.update_style() + + def update_style(self): + state = self.style_state(self.is_overriden, self.is_modified) + if self._state == state: + return + + if state: + child_state = "child-{}".format(state) + else: + child_state = "" + + self.setProperty("state", child_state) + self.style().polish(self) + + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + self._state = state + + def item_value(self): + return self.value_widget.config_value() + + def config_value(self): + return {self.key: self.item_value()} + + def override_value(self): + return + + +TypeToKlass.types["boolean"] = BooleanWidget +TypeToKlass.types["text-singleline"] = TextSingleLineWidget +TypeToKlass.types["text-multiline"] = TextMultiLineWidget +TypeToKlass.types["raw-json"] = RawJsonWidget +TypeToKlass.types["int"] = IntegerWidget +TypeToKlass.types["float"] = FloatWidget +TypeToKlass.types["dict-expanding"] = DictExpandWidget +TypeToKlass.types["dict-form"] = DictFormWidget +TypeToKlass.types["dict-invisible"] = DictInvisible +TypeToKlass.types["dict-modifiable"] = ModifiableDict +TypeToKlass.types["list-text"] = TextListWidget diff --git a/pype/tools/standalonepublish/__init__.py b/pype/tools/standalonepublish/__init__.py new file mode 100644 index 0000000000..29a4e52904 --- /dev/null +++ b/pype/tools/standalonepublish/__init__.py @@ -0,0 +1,8 @@ +from .app import ( + show, + cli +) +__all__ = [ + "show", + "cli" +] diff --git a/pype/modules/standalonepublish/__main__.py b/pype/tools/standalonepublish/__main__.py similarity index 100% rename from pype/modules/standalonepublish/__main__.py rename to pype/tools/standalonepublish/__main__.py diff --git a/pype/modules/standalonepublish/app.py b/pype/tools/standalonepublish/app.py similarity index 100% rename from pype/modules/standalonepublish/app.py rename to pype/tools/standalonepublish/app.py diff --git a/pype/modules/standalonepublish/publish.py b/pype/tools/standalonepublish/publish.py similarity index 100% rename from pype/modules/standalonepublish/publish.py rename to pype/tools/standalonepublish/publish.py diff --git a/pype/modules/standalonepublish/resources/__init__.py b/pype/tools/standalonepublish/resources/__init__.py similarity index 100% rename from pype/modules/standalonepublish/resources/__init__.py rename to pype/tools/standalonepublish/resources/__init__.py diff --git a/pype/modules/standalonepublish/resources/edit.svg b/pype/tools/standalonepublish/resources/edit.svg similarity index 100% rename from pype/modules/standalonepublish/resources/edit.svg rename to pype/tools/standalonepublish/resources/edit.svg diff --git a/pype/modules/standalonepublish/resources/file.png b/pype/tools/standalonepublish/resources/file.png similarity index 100% rename from pype/modules/standalonepublish/resources/file.png rename to pype/tools/standalonepublish/resources/file.png diff --git a/pype/modules/standalonepublish/resources/files.png b/pype/tools/standalonepublish/resources/files.png similarity index 100% rename from pype/modules/standalonepublish/resources/files.png rename to pype/tools/standalonepublish/resources/files.png diff --git a/pype/modules/standalonepublish/resources/houdini.png b/pype/tools/standalonepublish/resources/houdini.png similarity index 100% rename from pype/modules/standalonepublish/resources/houdini.png rename to pype/tools/standalonepublish/resources/houdini.png diff --git a/pype/modules/standalonepublish/resources/image_file.png b/pype/tools/standalonepublish/resources/image_file.png similarity index 100% rename from pype/modules/standalonepublish/resources/image_file.png rename to pype/tools/standalonepublish/resources/image_file.png diff --git a/pype/modules/standalonepublish/resources/image_files.png b/pype/tools/standalonepublish/resources/image_files.png similarity index 100% rename from pype/modules/standalonepublish/resources/image_files.png rename to pype/tools/standalonepublish/resources/image_files.png diff --git a/pype/modules/standalonepublish/resources/information.svg b/pype/tools/standalonepublish/resources/information.svg similarity index 100% rename from pype/modules/standalonepublish/resources/information.svg rename to pype/tools/standalonepublish/resources/information.svg diff --git a/pype/modules/standalonepublish/resources/maya.png b/pype/tools/standalonepublish/resources/maya.png similarity index 100% rename from pype/modules/standalonepublish/resources/maya.png rename to pype/tools/standalonepublish/resources/maya.png diff --git a/pype/modules/standalonepublish/resources/menu.png b/pype/tools/standalonepublish/resources/menu.png similarity index 100% rename from pype/modules/standalonepublish/resources/menu.png rename to pype/tools/standalonepublish/resources/menu.png diff --git a/pype/modules/standalonepublish/resources/menu_disabled.png b/pype/tools/standalonepublish/resources/menu_disabled.png similarity index 100% rename from pype/modules/standalonepublish/resources/menu_disabled.png rename to pype/tools/standalonepublish/resources/menu_disabled.png diff --git a/pype/modules/standalonepublish/resources/menu_hover.png b/pype/tools/standalonepublish/resources/menu_hover.png similarity index 100% rename from pype/modules/standalonepublish/resources/menu_hover.png rename to pype/tools/standalonepublish/resources/menu_hover.png diff --git a/pype/modules/standalonepublish/resources/menu_pressed.png b/pype/tools/standalonepublish/resources/menu_pressed.png similarity index 100% rename from pype/modules/standalonepublish/resources/menu_pressed.png rename to pype/tools/standalonepublish/resources/menu_pressed.png diff --git a/pype/modules/standalonepublish/resources/menu_pressed_hover.png b/pype/tools/standalonepublish/resources/menu_pressed_hover.png similarity index 100% rename from pype/modules/standalonepublish/resources/menu_pressed_hover.png rename to pype/tools/standalonepublish/resources/menu_pressed_hover.png diff --git a/pype/modules/standalonepublish/resources/nuke.png b/pype/tools/standalonepublish/resources/nuke.png similarity index 100% rename from pype/modules/standalonepublish/resources/nuke.png rename to pype/tools/standalonepublish/resources/nuke.png diff --git a/pype/modules/standalonepublish/resources/premiere.png b/pype/tools/standalonepublish/resources/premiere.png similarity index 100% rename from pype/modules/standalonepublish/resources/premiere.png rename to pype/tools/standalonepublish/resources/premiere.png diff --git a/pype/modules/standalonepublish/resources/trash.png b/pype/tools/standalonepublish/resources/trash.png similarity index 100% rename from pype/modules/standalonepublish/resources/trash.png rename to pype/tools/standalonepublish/resources/trash.png diff --git a/pype/modules/standalonepublish/resources/trash_disabled.png b/pype/tools/standalonepublish/resources/trash_disabled.png similarity index 100% rename from pype/modules/standalonepublish/resources/trash_disabled.png rename to pype/tools/standalonepublish/resources/trash_disabled.png diff --git a/pype/modules/standalonepublish/resources/trash_hover.png b/pype/tools/standalonepublish/resources/trash_hover.png similarity index 100% rename from pype/modules/standalonepublish/resources/trash_hover.png rename to pype/tools/standalonepublish/resources/trash_hover.png diff --git a/pype/modules/standalonepublish/resources/trash_pressed.png b/pype/tools/standalonepublish/resources/trash_pressed.png similarity index 100% rename from pype/modules/standalonepublish/resources/trash_pressed.png rename to pype/tools/standalonepublish/resources/trash_pressed.png diff --git a/pype/modules/standalonepublish/resources/trash_pressed_hover.png b/pype/tools/standalonepublish/resources/trash_pressed_hover.png similarity index 100% rename from pype/modules/standalonepublish/resources/trash_pressed_hover.png rename to pype/tools/standalonepublish/resources/trash_pressed_hover.png diff --git a/pype/modules/standalonepublish/resources/video_file.png b/pype/tools/standalonepublish/resources/video_file.png similarity index 100% rename from pype/modules/standalonepublish/resources/video_file.png rename to pype/tools/standalonepublish/resources/video_file.png diff --git a/pype/modules/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py similarity index 100% rename from pype/modules/standalonepublish/widgets/__init__.py rename to pype/tools/standalonepublish/widgets/__init__.py diff --git a/pype/modules/standalonepublish/widgets/button_from_svgs.py b/pype/tools/standalonepublish/widgets/button_from_svgs.py similarity index 100% rename from pype/modules/standalonepublish/widgets/button_from_svgs.py rename to pype/tools/standalonepublish/widgets/button_from_svgs.py diff --git a/pype/modules/standalonepublish/widgets/model_asset.py b/pype/tools/standalonepublish/widgets/model_asset.py similarity index 100% rename from pype/modules/standalonepublish/widgets/model_asset.py rename to pype/tools/standalonepublish/widgets/model_asset.py diff --git a/pype/modules/standalonepublish/widgets/model_filter_proxy_exact_match.py b/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py similarity index 100% rename from pype/modules/standalonepublish/widgets/model_filter_proxy_exact_match.py rename to pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py diff --git a/pype/modules/standalonepublish/widgets/model_filter_proxy_recursive_sort.py b/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py similarity index 100% rename from pype/modules/standalonepublish/widgets/model_filter_proxy_recursive_sort.py rename to pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py diff --git a/pype/modules/standalonepublish/widgets/model_node.py b/pype/tools/standalonepublish/widgets/model_node.py similarity index 100% rename from pype/modules/standalonepublish/widgets/model_node.py rename to pype/tools/standalonepublish/widgets/model_node.py diff --git a/pype/modules/standalonepublish/widgets/model_tasks_template.py b/pype/tools/standalonepublish/widgets/model_tasks_template.py similarity index 100% rename from pype/modules/standalonepublish/widgets/model_tasks_template.py rename to pype/tools/standalonepublish/widgets/model_tasks_template.py diff --git a/pype/modules/standalonepublish/widgets/model_tree.py b/pype/tools/standalonepublish/widgets/model_tree.py similarity index 100% rename from pype/modules/standalonepublish/widgets/model_tree.py rename to pype/tools/standalonepublish/widgets/model_tree.py diff --git a/pype/modules/standalonepublish/widgets/model_tree_view_deselectable.py b/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py similarity index 100% rename from pype/modules/standalonepublish/widgets/model_tree_view_deselectable.py rename to pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py diff --git a/pype/modules/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py similarity index 100% rename from pype/modules/standalonepublish/widgets/widget_asset.py rename to pype/tools/standalonepublish/widgets/widget_asset.py diff --git a/pype/modules/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py similarity index 100% rename from pype/modules/standalonepublish/widgets/widget_component_item.py rename to pype/tools/standalonepublish/widgets/widget_component_item.py diff --git a/pype/modules/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py similarity index 100% rename from pype/modules/standalonepublish/widgets/widget_components.py rename to pype/tools/standalonepublish/widgets/widget_components.py diff --git a/pype/modules/standalonepublish/widgets/widget_components_list.py b/pype/tools/standalonepublish/widgets/widget_components_list.py similarity index 100% rename from pype/modules/standalonepublish/widgets/widget_components_list.py rename to pype/tools/standalonepublish/widgets/widget_components_list.py diff --git a/pype/modules/standalonepublish/widgets/widget_drop_empty.py b/pype/tools/standalonepublish/widgets/widget_drop_empty.py similarity index 100% rename from pype/modules/standalonepublish/widgets/widget_drop_empty.py rename to pype/tools/standalonepublish/widgets/widget_drop_empty.py diff --git a/pype/modules/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py similarity index 100% rename from pype/modules/standalonepublish/widgets/widget_drop_frame.py rename to pype/tools/standalonepublish/widgets/widget_drop_frame.py diff --git a/pype/modules/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py similarity index 100% rename from pype/modules/standalonepublish/widgets/widget_family.py rename to pype/tools/standalonepublish/widgets/widget_family.py diff --git a/pype/modules/standalonepublish/widgets/widget_family_desc.py b/pype/tools/standalonepublish/widgets/widget_family_desc.py similarity index 100% rename from pype/modules/standalonepublish/widgets/widget_family_desc.py rename to pype/tools/standalonepublish/widgets/widget_family_desc.py diff --git a/pype/modules/standalonepublish/widgets/widget_shadow.py b/pype/tools/standalonepublish/widgets/widget_shadow.py similarity index 100% rename from pype/modules/standalonepublish/widgets/widget_shadow.py rename to pype/tools/standalonepublish/widgets/widget_shadow.py From ce70f9ed4973fb077771c8201979b2a02d24f871 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Aug 2020 18:02:17 +0200 Subject: [PATCH 066/158] cleaned (a little bit) app.py --- pype/tools/standalonepublish/app.py | 56 ++++++++--------------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index 60274f6b0a..ca6d26c025 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -1,18 +1,9 @@ -import os -import sys -import json -from subprocess import Popen from bson.objectid import ObjectId -from pype import lib as pypelib -from avalon.vendor.Qt import QtWidgets, QtCore -from avalon import api, style, schema -from avalon.tools import lib as parentlib -from .widgets import * -# Move this to pype lib? +from Qt import QtWidgets, QtCore +from avalon import style +from .widgets import AssetWidget, FamilyWidget, ComponentsWidget, ShadowWidget from avalon.tools.libraryloader.io_nonsingleton import DbConnector -module = sys.modules[__name__] -module.window = None class Window(QtWidgets.QDialog): """Main window of Standalone publisher. @@ -99,8 +90,14 @@ class Window(QtWidgets.QDialog): def resizeEvent(self, event=None): ''' Helps resize shadow widget ''' - position_x = (self.frameGeometry().width()-self.shadow_widget.frameGeometry().width())/2 - position_y = (self.frameGeometry().height()-self.shadow_widget.frameGeometry().height())/2 + position_x = ( + self.frameGeometry().width() + - self.shadow_widget.frameGeometry().width() + ) / 2 + position_y = ( + self.frameGeometry().height() + - self.shadow_widget.frameGeometry().height() + ) / 2 self.shadow_widget.move(position_x, position_y) w = self.frameGeometry().width() h = self.frameGeometry().height() @@ -144,7 +141,10 @@ class Window(QtWidgets.QDialog): - files/folders in clipboard (tested only on Windows OS) - copied path of file/folder in clipboard ('c:/path/to/folder') ''' - if event.key() == QtCore.Qt.Key_V and event.modifiers() == QtCore.Qt.ControlModifier: + if ( + event.key() == QtCore.Qt.Key_V + and event.modifiers() == QtCore.Qt.ControlModifier + ): clip = QtWidgets.QApplication.clipboard() self.widget_components.process_mime_data(clip) super().keyPressEvent(event) @@ -190,29 +190,3 @@ class Window(QtWidgets.QDialog): data.update(self.widget_components.collect_data()) return data - -def show(parent=None, debug=False): - try: - module.window.close() - del module.window - except (RuntimeError, AttributeError): - pass - - with parentlib.application(): - window = Window(parent) - window.show() - - module.window = window - - -def cli(args): - import argparse - parser = argparse.ArgumentParser() - parser.add_argument("project") - parser.add_argument("asset") - - args = parser.parse_args(args) - # project = args.project - # asset = args.asset - - show() From cd7b5983615939db4e551e1185113635ad99ab7e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Aug 2020 18:21:54 +0200 Subject: [PATCH 067/158] fixed minor issues --- .../standalonepublish_module.py | 9 ++++++- pype/tools/standalonepublish/__main__.py | 26 ++++++++++++++++--- pype/tools/standalonepublish/app.py | 4 +-- .../standalonepublish/widgets/__init__.py | 2 +- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/pype/modules/standalonepublish/standalonepublish_module.py b/pype/modules/standalonepublish/standalonepublish_module.py index b528642e8d..3ceb93578d 100644 --- a/pype/modules/standalonepublish/standalonepublish_module.py +++ b/pype/modules/standalonepublish/standalonepublish_module.py @@ -1,5 +1,8 @@ import os +import sys +import subprocess import pype +from pype import tools class StandAlonePublishModule: @@ -27,4 +30,8 @@ class StandAlonePublishModule: )) def show(self): - print("Running") + standalone_publisher_tool_path = os.path.join( + os.path.dirname(tools.__file__), + "standalonepublish" + ) + subprocess.Popen([sys.executable, standalone_publisher_tool_path]) diff --git a/pype/tools/standalonepublish/__main__.py b/pype/tools/standalonepublish/__main__.py index d77bc585c5..ea6291ec18 100644 --- a/pype/tools/standalonepublish/__main__.py +++ b/pype/tools/standalonepublish/__main__.py @@ -1,5 +1,23 @@ -from . import cli +import sys +import app +import signal +from Qt import QtWidgets +from avalon import style -if __name__ == '__main__': - import sys - sys.exit(cli(sys.argv[1:])) + +if __name__ == "__main__": + qt_app = QtWidgets.QApplication(sys.argv[1:]) + # app.setQuitOnLastWindowClosed(False) + qt_app.setStyleSheet(style.load_stylesheet()) + + def signal_handler(sig, frame): + print("You pressed Ctrl+C. Process ended.") + qt_app.quit() + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + window = app.Window() + window.show() + + sys.exit(qt_app.exec_()) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index ca6d26c025..8c854d9406 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -1,7 +1,6 @@ from bson.objectid import ObjectId from Qt import QtWidgets, QtCore -from avalon import style -from .widgets import AssetWidget, FamilyWidget, ComponentsWidget, ShadowWidget +from widgets import AssetWidget, FamilyWidget, ComponentsWidget, ShadowWidget from avalon.tools.libraryloader.io_nonsingleton import DbConnector @@ -26,7 +25,6 @@ class Window(QtWidgets.QDialog): self.setWindowTitle("Standalone Publish") self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.setStyleSheet(style.load_stylesheet()) # Validators self.valid_parent = False diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 9a71e0dee6..0db3643cf3 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -8,7 +8,7 @@ ExistsRole = QtCore.Qt.UserRole + 4 PluginRole = QtCore.Qt.UserRole + 5 PluginKeyRole = QtCore.Qt.UserRole + 6 -from ..resources import get_resource +from pype.resources import get_resource from .button_from_svgs import SvgResizable, SvgButton from .model_node import Node From e1ef2aad244444c65578f8ee038925adc177738d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Aug 2020 18:41:36 +0200 Subject: [PATCH 068/158] post standalone paths to process --- .../modules/standalonepublish/standalonepublish_module.py | 6 +++++- pype/tools/standalonepublish/__main__.py | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pype/modules/standalonepublish/standalonepublish_module.py b/pype/modules/standalonepublish/standalonepublish_module.py index 3ceb93578d..ed997bfd9f 100644 --- a/pype/modules/standalonepublish/standalonepublish_module.py +++ b/pype/modules/standalonepublish/standalonepublish_module.py @@ -34,4 +34,8 @@ class StandAlonePublishModule: os.path.dirname(tools.__file__), "standalonepublish" ) - subprocess.Popen([sys.executable, standalone_publisher_tool_path]) + subprocess.Popen([ + sys.executable, + standalone_publisher_tool_path, + os.pathsep.join(self.publish_paths).replace("\\", "/") + ]) diff --git a/pype/tools/standalonepublish/__main__.py b/pype/tools/standalonepublish/__main__.py index ea6291ec18..21c0635cf6 100644 --- a/pype/tools/standalonepublish/__main__.py +++ b/pype/tools/standalonepublish/__main__.py @@ -1,12 +1,16 @@ +import os import sys import app import signal from Qt import QtWidgets from avalon import style +import pype +import pyblish.api if __name__ == "__main__": - qt_app = QtWidgets.QApplication(sys.argv[1:]) + pype.install() + qt_app = QtWidgets.QApplication([]) # app.setQuitOnLastWindowClosed(False) qt_app.setStyleSheet(style.load_stylesheet()) @@ -17,6 +21,8 @@ if __name__ == "__main__": signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) + for path in sys.argv[-1].split(os.pathsep): + pyblish.api.register_plugin_path(path) window = app.Window() window.show() From 5ef59976341d8c33b31a1c9a3af806fda52080ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Aug 2020 18:57:00 +0200 Subject: [PATCH 069/158] now its working --- pype/tools/standalonepublish/__main__.py | 5 +- pype/tools/standalonepublish/app.py | 4 +- pype/tools/standalonepublish/publish.py | 98 ---------------- .../widgets/widget_components.py | 109 +++++++++++++++++- 4 files changed, 107 insertions(+), 109 deletions(-) diff --git a/pype/tools/standalonepublish/__main__.py b/pype/tools/standalonepublish/__main__.py index 21c0635cf6..5bcf514994 100644 --- a/pype/tools/standalonepublish/__main__.py +++ b/pype/tools/standalonepublish/__main__.py @@ -9,7 +9,6 @@ import pyblish.api if __name__ == "__main__": - pype.install() qt_app = QtWidgets.QApplication([]) # app.setQuitOnLastWindowClosed(False) qt_app.setStyleSheet(style.load_stylesheet()) @@ -21,9 +20,7 @@ if __name__ == "__main__": signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) - for path in sys.argv[-1].split(os.pathsep): - pyblish.api.register_plugin_path(path) - window = app.Window() + window = app.Window(sys.argv[-1].split(os.pathsep)) window.show() sys.exit(qt_app.exec_()) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index 8c854d9406..d139366a1c 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -18,10 +18,12 @@ class Window(QtWidgets.QDialog): WIDTH = 1100 HEIGHT = 500 - def __init__(self, parent=None): + def __init__(self, pyblish_paths, parent=None): super(Window, self).__init__(parent=parent) self._db.install() + self.pyblish_paths = pyblish_paths + self.setWindowTitle("Standalone Publish") self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) diff --git a/pype/tools/standalonepublish/publish.py b/pype/tools/standalonepublish/publish.py index 27062e8457..a4bb81ad3c 100644 --- a/pype/tools/standalonepublish/publish.py +++ b/pype/tools/standalonepublish/publish.py @@ -1,108 +1,10 @@ import os import sys -import json -import tempfile -import random -import string -from avalon import io import pype -from pype.api import execute, Logger - import pyblish.api -log = Logger().get_logger("standalonepublisher") - - -def set_context(project, asset, task, app): - ''' Sets context for pyblish (must be done before pyblish is launched) - :param project: Name of `Project` where instance should be published - :type project: str - :param asset: Name of `Asset` where instance should be published - :type asset: str - ''' - os.environ["AVALON_PROJECT"] = project - io.Session["AVALON_PROJECT"] = project - os.environ["AVALON_ASSET"] = asset - io.Session["AVALON_ASSET"] = asset - if not task: - task = '' - os.environ["AVALON_TASK"] = task - io.Session["AVALON_TASK"] = task - - io.install() - - av_project = io.find_one({'type': 'project'}) - av_asset = io.find_one({ - "type": 'asset', - "name": asset - }) - - parents = av_asset['data']['parents'] - hierarchy = '' - if parents and len(parents) > 0: - hierarchy = os.path.sep.join(parents) - - os.environ["AVALON_HIERARCHY"] = hierarchy - io.Session["AVALON_HIERARCHY"] = hierarchy - - os.environ["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') - io.Session["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') - - io.Session["current_dir"] = os.path.normpath(os.getcwd()) - - os.environ["AVALON_APP"] = app - io.Session["AVALON_APP"] = app - - io.uninstall() - - -def publish(data, gui=True): - # cli pyblish seems like better solution - return cli_publish(data, gui) - - -def cli_publish(data, gui=True): - from . import PUBLISH_PATHS - - PUBLISH_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "publish.py") - io.install() - - # Create hash name folder in temp - chars = "".join([random.choice(string.ascii_letters) for i in range(15)]) - staging_dir = tempfile.mkdtemp(chars) - - # create also json and fill with data - json_data_path = staging_dir + os.path.basename(staging_dir) + '.json' - with open(json_data_path, 'w') as outfile: - json.dump(data, outfile) - - envcopy = os.environ.copy() - envcopy["PYBLISH_HOSTS"] = "standalonepublisher" - envcopy["SAPUBLISH_INPATH"] = json_data_path - envcopy["PYBLISHGUI"] = "pyblish_pype" - envcopy["PUBLISH_PATHS"] = os.pathsep.join(PUBLISH_PATHS) - if data.get("family", "").lower() == "editorial": - envcopy["PYBLISH_SUSPEND_LOGS"] = "1" - - result = execute( - [sys.executable, PUBLISH_SCRIPT_PATH], - env=envcopy - ) - - result = {} - if os.path.exists(json_data_path): - with open(json_data_path, "r") as f: - result = json.load(f) - - log.info(f"Publish result: {result}") - - io.uninstall() - - return False - - def main(env): from avalon.tools import publish # Registers pype's Global pyblish plugins diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 90167f2fa6..3b6c326af0 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -1,7 +1,17 @@ -from . import QtWidgets, QtCore, QtGui -from . import DropDataFrame +import os +import sys +import json +import tempfile +import random +import string -from .. import publish +from Qt import QtWidgets, QtCore +from . import DropDataFrame +from avalon.tools import publish +from avalon import io +from pype.api import execute, Logger + +log = Logger().get_logger("standalonepublisher") class ComponentsWidget(QtWidgets.QWidget): @@ -113,16 +123,103 @@ class ComponentsWidget(QtWidgets.QWidget): self.parent_widget.working_stop() def _publish(self): + log.info(self.parent_widget.pyblish_paths) self.working_start('Pyblish is running') try: data = self.parent_widget.collect_data() - publish.set_context( - data['project'], data['asset'], data['task'], 'standalonepublish' + set_context( + data['project'], + data['asset'], + data['task'] ) - result = publish.publish(data) + result = cli_publish(data, self.parent_widget.pyblish_paths) # Clear widgets from components list if publishing was successful if result: self.drop_frame.components_list.clear_widgets() self.drop_frame._refresh_view() finally: self.working_stop() + + +def set_context(project, asset, task): + ''' Sets context for pyblish (must be done before pyblish is launched) + :param project: Name of `Project` where instance should be published + :type project: str + :param asset: Name of `Asset` where instance should be published + :type asset: str + ''' + os.environ["AVALON_PROJECT"] = project + io.Session["AVALON_PROJECT"] = project + os.environ["AVALON_ASSET"] = asset + io.Session["AVALON_ASSET"] = asset + if not task: + task = '' + os.environ["AVALON_TASK"] = task + io.Session["AVALON_TASK"] = task + + io.install() + + av_project = io.find_one({'type': 'project'}) + av_asset = io.find_one({ + "type": 'asset', + "name": asset + }) + + parents = av_asset['data']['parents'] + hierarchy = '' + if parents and len(parents) > 0: + hierarchy = os.path.sep.join(parents) + + os.environ["AVALON_HIERARCHY"] = hierarchy + io.Session["AVALON_HIERARCHY"] = hierarchy + + os.environ["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') + io.Session["AVALON_PROJECTCODE"] = av_project['data'].get('code', '') + + io.Session["current_dir"] = os.path.normpath(os.getcwd()) + + os.environ["AVALON_APP"] = "standalonepublish" + io.Session["AVALON_APP"] = "standalonepublish" + + io.uninstall() + + +def cli_publish(data, publish_paths, gui=True): + PUBLISH_SCRIPT_PATH = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + "publish.py" + ) + io.install() + + # Create hash name folder in temp + chars = "".join([random.choice(string.ascii_letters) for i in range(15)]) + staging_dir = tempfile.mkdtemp(chars) + + # create also json and fill with data + json_data_path = staging_dir + os.path.basename(staging_dir) + '.json' + with open(json_data_path, 'w') as outfile: + json.dump(data, outfile) + + envcopy = os.environ.copy() + envcopy["PYBLISH_HOSTS"] = "standalonepublisher" + envcopy["SAPUBLISH_INPATH"] = json_data_path + envcopy["PYBLISHGUI"] = "pyblish_pype" + envcopy["PUBLISH_PATHS"] = os.pathsep.join(publish_paths) + if data.get("family", "").lower() == "editorial": + envcopy["PYBLISH_SUSPEND_LOGS"] = "1" + + result = execute( + [sys.executable, PUBLISH_SCRIPT_PATH], + env=envcopy + ) + + result = {} + if os.path.exists(json_data_path): + with open(json_data_path, "r") as f: + result = json.load(f) + + log.info(f"Publish result: {result}") + + io.uninstall() + + return False From 507be4926965271d5ab0db6f078ba1918ad7b656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Sat, 15 Aug 2020 00:09:04 +0200 Subject: [PATCH 070/158] tile rendering support in pype --- .../global/publish/submit_publish_job.py | 11 +- pype/plugins/maya/create/create_render.py | 2 + pype/plugins/maya/publish/collect_render.py | 2 + .../maya/publish/submit_maya_deadline.py | 284 +++++++++++++++++- 4 files changed, 280 insertions(+), 19 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 3053c80b11..d106175eb6 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -718,7 +718,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "pixelAspect": data.get("pixelAspect", 1), "resolutionWidth": data.get("resolutionWidth", 1920), "resolutionHeight": data.get("resolutionHeight", 1080), - "multipartExr": data.get("multipartExr", False) + "multipartExr": data.get("multipartExr", False), + "jobBatchName": data.get("jobBatchName", "") } if "prerender" in instance.data["families"]: @@ -895,8 +896,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # We still use data from it so lets fake it. # # Batch name reflect original scene name - render_job["Props"]["Batch"] = os.path.splitext(os.path.basename( - context.data.get("currentFile")))[0] + + if instance.data.get("assemblySubmissionJob"): + render_job["Props"]["Batch"] = instance.data.get("jobBatchName") + else: + render_job["Props"]["Batch"] = os.path.splitext( + os.path.basename(context.data.get("currentFile")))[0] # User is deadline user render_job["Props"]["User"] = context.data.get( "deadlineUser", getpass.getuser()) diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index 9e5f9310ae..5d68c5301a 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -185,6 +185,8 @@ class CreateRender(avalon.maya.Creator): self.data["useMayaBatch"] = False self.data["vrayScene"] = False self.data["tileRendering"] = False + self.data["tilesX"] = 2 + self.data["tilesY"] = 2 # Disable for now as this feature is not working yet # self.data["assScene"] = False diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 5ca9392080..1db7b31c8c 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -243,6 +243,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "resolutionHeight": cmds.getAttr("defaultResolution.height"), "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), "tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501 + "tilesX": render_instance.data.get("tilesX") or 2, + "tilesY": render_instance.data.get("tilesY") or 2, "priority": render_instance.data.get("priority") } diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 7fe20c779d..d6d4bd2910 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -16,11 +16,14 @@ Attributes: """ +from __future__ import print_function import os import json import getpass import copy import re +import hashlib +from datetime import datetime import clique import requests @@ -61,6 +64,91 @@ payload_skeleton = { } +def _format_tiles(filename, index, tiles_x, tiles_y, width, height, prefix): + """Generate tile entries for Deadline tile job. + + Returns two dictionaries - one that can be directly used in Deadline + job, second that can be used for Deadline Assembly job configuration + file. + + This will format tile names: + + Example:: + { + "OutputFilename0Tile0": "_tile_1x1_4x4_Main_beauty.1001.exr", + "OutputFilename0Tile1": "_tile_2x1_4x4_Main_beauty.1001.exr" + } + + And add tile prefixes like: + + Example:: + Image prefix is: + `maya///_` + + Result for tile 0 for 4x4 will be: + `maya///_tile_1x1_4x4__` + + Calculating coordinates is tricky as in Job they are defined as top, + left, bottom, right with zero being in top-left corner. But Assembler + configuration file takes tile coordinates as X, Y, Width and Height and + zero is bottom left corner. + + Args: + filename (str): Filename to process as tiles. + index (int): Index of that file if it is sequence. + tiles_x (int): Number of tiles in X. + tiles_y (int): Number if tikes in Y. + width (int): Width resolution of final image. + height (int): Height resolution of final image. + prefix (str): Image prefix. + + Returns: + (dict, dict): Tuple of two dictionaires - first can be used to + extend JobInfo, second has tiles x, y, width and height + used for assembler configuration. + + """ + tile = 0 + out = {"JobInfo": {}, "PluginInfo": {}} + cfg = {} + w_space = width / tiles_x + h_space = height / tiles_y + + for tile_x in range(1, tiles_x + 1): + for tile_y in range(1, tiles_y + 1): + tile_prefix = "_tile_{}x{}_{}x{}_".format( + tile_x, tile_y, + tiles_x, + tiles_y + ) + out_tile_index = "OutputFilename{}Tile{}".format( + str(index), tile + ) + new_filename = "{}/{}{}".format( + os.path.dirname(filename), + tile_prefix, + os.path.basename(filename) + ) + out["JobInfo"][out_tile_index] = new_filename + out["PluginInfo"]["RegionPrefix{}".format(str(tile))] = tile_prefix.join( # noqa: E501 + prefix.rsplit("/", 1)) + + out["PluginInfo"]["RegionTop{}".format(tile)] = int(height) - (tile_y * h_space) # noqa: E501 + out["PluginInfo"]["RegionBottom{}".format(tile)] = int(height) - ((tile_y - 1) * h_space) - 1 # noqa: E501 + out["PluginInfo"]["RegionLeft{}".format(tile)] = (tile_x - 1) * w_space # noqa: E501 + out["PluginInfo"]["RegionRight{}".format(tile)] = (tile_x * w_space) - 1 # noqa: E501 + + cfg["Tile{}".format(tile)] = new_filename + cfg["Tile{}Tile".format(tile)] = new_filename + cfg["Tile{}X".format(tile)] = (tile_x - 1) * w_space + cfg["Tile{}Y".format(tile)] = (tile_y - 1) * h_space + cfg["Tile{}Width".format(tile)] = tile_x * w_space + cfg["Tile{}Height".format(tile)] = tile_y * h_space + + tile += 1 + return out, cfg + + def get_renderer_variables(renderlayer, root): """Retrieve the extension which has been set in the VRay settings. @@ -164,6 +252,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): optional = True use_published = True + tile_assembler_plugin = "DraftTileAssembler" def process(self, instance): """Plugin entry point.""" @@ -309,7 +398,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # Optional, enable double-click to preview rendered # frames from Deadline Monitor payload_skeleton["JobInfo"]["OutputDirectory0"] = \ - os.path.dirname(output_filename_0) + os.path.dirname(output_filename_0).replace("\\", "/") payload_skeleton["JobInfo"]["OutputFilename0"] = \ output_filename_0.replace("\\", "/") @@ -376,9 +465,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # Add list of expected files to job --------------------------------- exp = instance.data.get("expectedFiles") - - output_filenames = {} exp_index = 0 + output_filenames = {} if isinstance(exp[0], dict): # we have aovs and we need to iterate over them @@ -390,33 +478,202 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): assert len(rem) == 1, ("Found multiple non related files " "to render, don't know what to do " "with them.") - payload['JobInfo']['OutputFilename' + str(exp_index)] = rem[0] # noqa: E501 output_file = rem[0] + if not instance.data.get("tileRendering"): + payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 else: output_file = col[0].format('{head}{padding}{tail}') - payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 - output_filenames[exp_index] = output_file + if not instance.data.get("tileRendering"): + payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 + + output_filenames['OutputFilename' + str(exp_index)] = output_file # noqa: E501 exp_index += 1 else: - col, rem = clique.assemble(files) + col, rem = clique.assemble(exp) if not col and rem: # we couldn't find any collections but have # individual files. assert len(rem) == 1, ("Found multiple non related files " "to render, don't know what to do " "with them.") - payload['JobInfo']['OutputFilename' + str(exp_index)] = rem[0] # noqa: E501 + + output_file = rem[0] + if not instance.data.get("tileRendering"): + payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 else: output_file = col[0].format('{head}{padding}{tail}') - payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 + if not instance.data.get("tileRendering"): + payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 + + output_filenames['OutputFilename' + str(exp_index)] = output_file plugin = payload["JobInfo"]["Plugin"] self.log.info("using render plugin : {}".format(plugin)) + # Store output dir for unified publisher (filesequence) + instance.data["outputDir"] = os.path.dirname(output_filename_0) + self.preflight_check(instance) - # Submit job to farm ------------------------------------------------ - if not instance.data.get("tileRendering"): + # Prepare tiles data ------------------------------------------------ + if instance.data.get("tileRendering"): + # if we have sequence of files, we need to create tile job for + # every frame + + payload["JobInfo"]["TileJob"] = True + payload["JobInfo"]["TileJobTilesInX"] = instance.data.get("tilesX") + payload["JobInfo"]["TileJobTilesInY"] = instance.data.get("tilesY") + payload["PluginInfo"]["ImageHeight"] = instance.data.get("resolutionHeight") # noqa: E501 + payload["PluginInfo"]["ImageWidth"] = instance.data.get("resolutionWidth") # noqa: E501 + payload["PluginInfo"]["RegionRendering"] = True + + assembly_payload = { + "AuxFiles": [], + "JobInfo": { + "BatchName": payload["JobInfo"]["BatchName"], + "Frames": 0, + "Name": "{} - Tile Assembly Job".format( + payload["JobInfo"]["Name"]), + "OutputDirectory0": + payload["JobInfo"]["OutputDirectory0"].replace( + "\\", "/"), + "Plugin": self.tile_assembler_plugin, + "MachineLimit": 1 + }, + "PluginInfo": { + "CleanupTiles": 1, + "ErrorOnMissing": True + } + } + assembly_payload["JobInfo"].update(output_filenames) + + frame_payloads = [] + assembly_payloads = {} + + R_FRAME_NUMBER = re.compile(r".+\.(?P[0-9]+)\..+") # noqa: N806, E501 + REPL_FRAME_NUMBER = re.compile(r"(.+\.)([0-9]+)(\..+)") # noqa: N806, E501 + + if isinstance(exp[0], dict): + # we have aovs and we need to iterate over them + # get files from `beauty` + files = exp[0].get("beauty") + if not files: + # if beauty doesn't exists, use first aov we found + files = exp[0].get(list(exp[0].keys())[0]) + else: + files = exp + + file_index = 1 + for file in files: + frame = re.search(R_FRAME_NUMBER, file).group("frame") + new_payload = copy.copy(payload) + new_payload["JobInfo"]["Name"] = \ + "{} (Frame {} - {} tiles)".format( + new_payload["JobInfo"]["Name"], + frame, + instance.data.get("tilesX") * instance.data.get("tilesY") # noqa: E501 + ) + new_payload["JobInfo"]["TileJobFrame"] = frame + + tiles_data = _format_tiles( + file, 0, + instance.data.get("tilesX"), + instance.data.get("tilesY"), + instance.data.get("resolutionWidth"), + instance.data.get("resolutionHeight"), + payload["PluginInfo"]["OutputFilePrefix"] + )[0] + new_payload["JobInfo"].update(tiles_data["JobInfo"]) + new_payload["PluginInfo"].update(tiles_data["PluginInfo"]) + + job_hash = hashlib.sha256("{}_{}".format(file_index, file)) + new_payload["JobInfo"]["ExtraInfo0"] = job_hash.hexdigest() + new_payload["JobInfo"]["ExtraInfo1"] = file + + frame_payloads.append(new_payload) + + new_assembly_payload = copy.copy(assembly_payload) + new_assembly_payload["JobInfo"]["OutputFilename0"] = re.sub( + REPL_FRAME_NUMBER, + "\\1{}\\3".format("#" * len(frame)), file) + + new_assembly_payload["JobInfo"]["ExtraInfo0"] = job_hash.hexdigest() # noqa: E501 + new_assembly_payload["JobInfo"]["ExtraInfo1"] = file + assembly_payloads[job_hash.hexdigest()] = new_assembly_payload + file_index += 1 + + self.log.info( + "Submitting tile job(s) [{}] ...".format(len(frame_payloads))) + + url = "{}/api/jobs".format(self._deadline_url) + tiles_count = instance.data.get("tilesX") * instance.data.get("tilesY") # noqa: E501 + + for tile_job in frame_payloads: + response = self._requests_post(url, json=tile_job) + if not response.ok: + raise Exception(response.text) + + job_id = response.json()["_id"] + hash = response.json()["Props"]["Ex0"] + file = response.json()["Props"]["Ex1"] + assembly_payloads[hash]["JobInfo"]["JobDependency0"] = job_id + + # write assembly job config files + now = datetime.now() + + config_file = os.path.join( + os.path.dirname(output_filename_0), + "{}_config_{}.txt".format( + os.path.splitext(file)[0], + now.strftime("%Y_%m_%d_%H_%M_%S") + ) + ) + + try: + if not os.path.isdir(os.path.dirname(config_file)): + os.makedirs(os.path.dirname(config_file)) + except OSError: + # directory is not available + self.log.warning( + "Path is unreachable: `{}`".format( + os.path.dirname(config_file))) + + with open(config_file, "w") as cf: + print("TileCount={}".format(tiles_count), file=cf) + print("ImageFileName={}".format(file), file=cf) + print("ImageWidth={}".format( + instance.data.get("resolutionWidth")), file=cf) + print("ImageHeight={}".format( + instance.data.get("resolutionHeight")), file=cf) + + tiles = _format_tiles( + file, 0, + instance.data.get("tilesX"), + instance.data.get("tilesY"), + instance.data.get("resolutionWidth"), + instance.data.get("resolutionHeight"), + payload["PluginInfo"]["OutputFilePrefix"] + )[1] + sorted(tiles) + for k, v in tiles.items(): + print("{}={}".format(k, v), file=cf) + + self.log.debug(json.dumps(assembly_payloads, + indent=4, sort_keys=True)) + self.log.info( + "Submitting assembly job(s) [{}] ...".format(len(assembly_payloads))) # noqa: E501 + url = "{}/api/jobs".format(self._deadline_url) + response = self._requests_post(url, json={ + "Jobs": list(assembly_payloads.values()), + "AuxFiles": [] + }) + if not response.ok: + raise Exception(response) + + instance.data["assemblySubmissionJob"] = assembly_payloads + instance.data["jobBatchName"] = payload["JobInfo"]["BatchName"] + else: + # Submit job to farm -------------------------------------------- self.log.info("Submitting ...") self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) @@ -426,11 +683,6 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): if not response.ok: raise Exception(response.text) instance.data["deadlineSubmissionJob"] = response.json() - else: - self.log.info("Skipping submission, tile rendering enabled.") - - # Store output dir for unified publisher (filesequence) - instance.data["outputDir"] = os.path.dirname(output_filename_0) def _get_maya_payload(self, data): payload = copy.deepcopy(payload_skeleton) From 7ef0c58019326c7d9b0524cac82727751c746d5d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 00:09:14 +0200 Subject: [PATCH 071/158] removed files that should not be in this PR --- pype/tools/config_setting/widgets/base0.py | 404 ---- pype/tools/config_setting/widgets/inputs0.py | 2145 ------------------ pype/tools/config_setting/widgets/inputs1.py | 2131 ----------------- 3 files changed, 4680 deletions(-) delete mode 100644 pype/tools/config_setting/widgets/base0.py delete mode 100644 pype/tools/config_setting/widgets/inputs0.py delete mode 100644 pype/tools/config_setting/widgets/inputs1.py diff --git a/pype/tools/config_setting/widgets/base0.py b/pype/tools/config_setting/widgets/base0.py deleted file mode 100644 index 7f01f27ca8..0000000000 --- a/pype/tools/config_setting/widgets/base0.py +++ /dev/null @@ -1,404 +0,0 @@ -import os -import json -import copy -from Qt import QtWidgets, QtCore, QtGui -from . import config -from .widgets import UnsavedChangesDialog -from .lib import NOT_SET -from avalon import io -from queue import Queue - - -class TypeToKlass: - types = {} - - -class PypeConfigurationWidget: - default_state = "" - - def config_value(self): - raise NotImplementedError( - "Method `config_value` is not implemented for `{}`.".format( - self.__class__.__name__ - ) - ) - - def value_from_values(self, values, keys=None): - if not values: - return NOT_SET - - if keys is None: - keys = self.keys - - value = values - for key in keys: - if not isinstance(value, dict): - raise TypeError( - "Expected dictionary got {}.".format(str(type(value))) - ) - - if key not in value: - return NOT_SET - value = value[key] - return value - - def style_state(self, is_overriden, is_modified): - items = [] - if is_overriden: - items.append("overriden") - if is_modified: - items.append("modified") - return "-".join(items) or self.default_state - - def add_children_gui(self, child_configuration, values): - raise NotImplementedError(( - "Method `add_children_gui` is not implemented for `{}`." - ).format(self.__class__.__name__)) - - -class StudioWidget(QtWidgets.QWidget, PypeConfigurationWidget): - is_overidable = False - is_overriden = False - is_group = False - any_parent_is_group = False - ignore_value_changes = False - - def __init__(self, parent=None): - super(StudioWidget, self).__init__(parent) - - self.input_fields = [] - - scroll_widget = QtWidgets.QScrollArea(self) - content_widget = QtWidgets.QWidget(scroll_widget) - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(3, 3, 3, 3) - content_layout.setSpacing(0) - content_layout.setAlignment(QtCore.Qt.AlignTop) - content_widget.setLayout(content_layout) - - # scroll_widget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - # scroll_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) - scroll_widget.setWidgetResizable(True) - scroll_widget.setWidget(content_widget) - - self.scroll_widget = scroll_widget - self.content_layout = content_layout - self.content_widget = content_widget - - footer_widget = QtWidgets.QWidget() - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - - save_btn = QtWidgets.QPushButton("Save") - spacer_widget = QtWidgets.QWidget() - footer_layout.addWidget(spacer_widget, 1) - footer_layout.addWidget(save_btn, 0) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - self.setLayout(layout) - - layout.addWidget(scroll_widget, 1) - layout.addWidget(footer_widget, 0) - - save_btn.clicked.connect(self._save) - - self.reset() - - def reset(self): - if self.content_layout.count() != 0: - for widget in self.input_fields: - self.content_layout.removeWidget(widget) - widget.deleteLater() - self.input_fields.clear() - - values = {"studio": config.studio_presets()} - schema = config.gui_schema("studio_schema", "studio_gui_schema") - self.keys = schema.get("keys", []) - self.add_children_gui(schema, values) - self.schema = schema - - def _save(self): - all_values = {} - for item in self.input_fields: - all_values.update(item.config_value()) - - for key in reversed(self.keys): - _all_values = {key: all_values} - all_values = _all_values - - # Skip first key - all_values = all_values["studio"] - - # Load studio data with metadata - current_presets = config.studio_presets() - - keys_to_file = config.file_keys_from_schema(self.schema) - for key_sequence in keys_to_file: - # Skip first key - key_sequence = key_sequence[1:] - subpath = "/".join(key_sequence) + ".json" - origin_values = current_presets - for key in key_sequence: - if key not in origin_values: - origin_values = {} - break - origin_values = origin_values[key] - - new_values = all_values - for key in key_sequence: - new_values = new_values[key] - origin_values.update(new_values) - - output_path = os.path.join( - config.studio_presets_path, subpath - ) - dirpath = os.path.dirname(output_path) - if not os.path.exists(dirpath): - os.makedirs(dirpath) - - with open(output_path, "w") as file_stream: - json.dump(origin_values, file_stream, indent=4) - - def add_children_gui(self, child_configuration, values): - item_type = child_configuration["type"] - klass = TypeToKlass.types.get(item_type) - item = klass( - child_configuration, values, self.keys, self - ) - self.input_fields.append(item) - self.content_layout.addWidget(item) - - -class ProjectListView(QtWidgets.QListView): - left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex) - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: - index = self.indexAt(event.pos()) - self.left_mouse_released_at.emit(index) - super(ProjectListView, self).mouseReleaseEvent(event) - - -class ProjectListWidget(QtWidgets.QWidget): - default = "< Default >" - project_changed = QtCore.Signal() - - def __init__(self, parent): - self._parent = parent - - self.current_project = None - - super(ProjectListWidget, self).__init__(parent) - - label_widget = QtWidgets.QLabel("Projects") - project_list = ProjectListView(self) - project_list.setModel(QtGui.QStandardItemModel()) - - # Do not allow editing - project_list.setEditTriggers( - QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers - ) - # Do not automatically handle selection - project_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - - layout = QtWidgets.QVBoxLayout(self) - layout.setSpacing(3) - layout.addWidget(label_widget, 0) - layout.addWidget(project_list, 1) - - project_list.left_mouse_released_at.connect(self.on_item_clicked) - - self.project_list = project_list - - self.refresh() - - def on_item_clicked(self, new_index): - new_project_name = new_index.data(QtCore.Qt.DisplayRole) - if new_project_name is None: - return - - if self.current_project == new_project_name: - return - - save_changes = False - change_project = False - if self.validate_context_change(): - change_project = True - - else: - dialog = UnsavedChangesDialog(self) - result = dialog.exec_() - if result == 1: - save_changes = True - change_project = True - - elif result == 2: - change_project = True - - if save_changes: - self._parent._save() - - if change_project: - self.select_project(new_project_name) - self.current_project = new_project_name - self.project_changed.emit() - else: - self.select_project(self.current_project) - - def validate_context_change(self): - # TODO add check if project can be changed (is modified) - for item in self._parent.input_fields: - is_modified = item.child_modified - if is_modified: - return False - return True - - def project_name(self): - if self.current_project == self.default: - return None - return self.current_project - - def select_project(self, project_name): - model = self.project_list.model() - found_items = model.findItems(project_name) - if not found_items: - found_items = model.findItems(self.default) - - index = model.indexFromItem(found_items[0]) - self.project_list.selectionModel().clear() - self.project_list.selectionModel().setCurrentIndex( - index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent - ) - - def refresh(self): - selected_project = None - for index in self.project_list.selectedIndexes(): - selected_project = index.data(QtCore.Qt.DisplayRole) - break - - model = self.project_list.model() - model.clear() - items = [self.default] - io.install() - for project_doc in tuple(io.projects()): - items.append(project_doc["name"]) - - for item in items: - model.appendRow(QtGui.QStandardItem(item)) - - self.select_project(selected_project) - - self.current_project = self.project_list.currentIndex().data( - QtCore.Qt.DisplayRole - ) - - -class ProjectWidget(QtWidgets.QWidget, PypeConfigurationWidget): - is_overriden = False - is_group = False - any_parent_is_group = False - - def __init__(self, parent=None): - super(ProjectWidget, self).__init__(parent) - - self.is_overidable = False - self.ignore_value_changes = False - - self.input_fields = [] - - scroll_widget = QtWidgets.QScrollArea(self) - content_widget = QtWidgets.QWidget(scroll_widget) - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(3, 3, 3, 3) - content_layout.setSpacing(0) - content_layout.setAlignment(QtCore.Qt.AlignTop) - content_widget.setLayout(content_layout) - - scroll_widget.setWidgetResizable(True) - scroll_widget.setWidget(content_widget) - - project_list_widget = ProjectListWidget(self) - content_layout.addWidget(project_list_widget) - - footer_widget = QtWidgets.QWidget() - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - - save_btn = QtWidgets.QPushButton("Save") - spacer_widget = QtWidgets.QWidget() - footer_layout.addWidget(spacer_widget, 1) - footer_layout.addWidget(save_btn, 0) - - presets_widget = QtWidgets.QWidget() - presets_layout = QtWidgets.QVBoxLayout(presets_widget) - presets_layout.setContentsMargins(0, 0, 0, 0) - presets_layout.setSpacing(0) - - presets_layout.addWidget(scroll_widget, 1) - presets_layout.addWidget(footer_widget, 0) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - self.setLayout(layout) - - layout.addWidget(project_list_widget, 0) - layout.addWidget(presets_widget, 1) - - save_btn.clicked.connect(self._save) - project_list_widget.project_changed.connect(self._on_project_change) - - self.project_list_widget = project_list_widget - self.scroll_widget = scroll_widget - self.content_layout = content_layout - self.content_widget = content_widget - - self.reset() - - def reset(self): - values = config.global_project_presets() - schema = config.gui_schema("projects_schema", "project_gui_schema") - self.keys = schema.get("keys", []) - self.add_children_gui(schema, values) - - def add_children_gui(self, child_configuration, values): - item_type = child_configuration["type"] - klass = TypeToKlass.types.get(item_type) - - item = klass( - child_configuration, values, self.keys, self - ) - self.input_fields.append(item) - self.content_layout.addWidget(item) - - def _on_project_change(self): - project_name = self.project_list_widget.project_name() - - if project_name is None: - overrides = None - self.is_overidable = False - else: - overrides = config.project_preset_overrides(project_name) - self.is_overidable = True - - self.ignore_value_changes = True - for item in self.input_fields: - item.apply_overrides(overrides) - self.ignore_value_changes = False - - def _save(self): - output = {} - for item in self.input_fields: - if hasattr(item, "override_value"): - print(item.override_value()) - else: - print("*** missing `override_value`", item) - - # for item in self.input_fields: - # output.update(item.config_value()) - # - # for key in reversed(self.keys): - # _output = {key: output} - # output = _output - - print(json.dumps(output, indent=4)) diff --git a/pype/tools/config_setting/widgets/inputs0.py b/pype/tools/config_setting/widgets/inputs0.py deleted file mode 100644 index 7ab9c7fa01..0000000000 --- a/pype/tools/config_setting/widgets/inputs0.py +++ /dev/null @@ -1,2145 +0,0 @@ -import json -from Qt import QtWidgets, QtCore, QtGui -from . import config -from .base import PypeConfigurationWidget, TypeToKlass -from .widgets import ( - ClickableWidget, - ExpandingWidget, - ModifiedIntSpinBox, - ModifiedFloatSpinBox -) -from .lib import NOT_SET, AS_WIDGET - - -class SchemeGroupHierarchyBug(Exception): - def __init__(self, msg=None): - if not msg: - # TODO better message - msg = "SCHEME BUG: Attribute `is_group` is mixed in the hierarchy" - super(SchemeGroupHierarchyBug, self).__init__(msg) - - -class BooleanWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - self._as_widget = as_widget - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - self.override_value = None - - super(BooleanWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.checkbox = QtWidgets.QCheckBox() - self.checkbox.setAttribute(QtCore.Qt.WA_StyledBackground) - if not self._as_widget and not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - label_widget.setAttribute(QtCore.Qt.WA_StyledBackground) - layout.addWidget(label_widget) - - if not self._as_widget: - self.label_widget = label_widget - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - layout.addWidget(self.checkbox) - - self.default_value = self.item_value() - - self.checkbox.stateChanged.connect(self._on_value_change) - - def set_default_values(self, default_values): - value = self.value_from_values(default_values) - if isinstance(value, bool): - self.set_value(value, default_value=True) - self.default_value = self.item_value() - else: - self.default_value = value - - def set_value(self, value, *, default_value=False): - # Ignore value change because if `self.isChecked()` has same - # value as `value` the `_on_value_change` is not triggered - self.checkbox.setChecked(value) - - if default_value: - self.default_value = self.item_value() - - self._on_value_change() - - def reset_value(self): - if self.is_overidable and self.override_value is not None: - self.set_value(self.override_value) - else: - self.set_value(self.default_value) - - def clear_value(self): - self.reset_value() - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - self._was_overriden = False - value = self.default_value - else: - self._is_overriden = True - self._was_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - _value = self.item_value() - is_modified = None - if self.is_overidable: - self._is_overriden = True - if self.override_value is not None: - is_modified = _value != self.override_value - - if is_modified is None: - is_modified = _value != self.default_value - - self._is_modified = is_modified - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - else: - property_name = "state" - - self.label_widget.setProperty(property_name, state) - self.label_widget.style().polish(self.label_widget) - self._state = state - - def item_value(self): - return self.checkbox.isChecked() - - def config_value(self): - return {self.key: self.item_value()} - - -class IntegerWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - self._parent = parent - self._as_widget = as_widget - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(IntegerWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.int_input = ModifiedIntSpinBox() - - if not self._as_widget and not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - layout.addWidget(self.int_input) - - if not self._as_widget: - self.label_widget = label_widget - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - self.default_value = self.item_value() - self.override_value = None - - self.int_input.valueChanged.connect(self._on_value_change) - - def set_default_values(self, default_values): - value = self.value_from_values(default_values) - if isinstance(value, int): - self.set_value(value, default_value=True) - self.default_value = self.item_value() - else: - self.default_value = value - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def set_value(self, value, *, default_value=False): - self.int_input.setValue(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def clear_value(self): - self.set_value(0) - - def reset_value(self): - self.set_value(self.default_value) - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - self._was_overriden = False - value = self.default_value - else: - self._is_overriden = True - self._was_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - widget = self.int_input - else: - property_name = "state" - widget = self.label_widget - - widget.setProperty(property_name, state) - widget.style().polish(widget) - - def item_value(self): - return self.int_input.value() - - def config_value(self): - return {self.key: self.item_value()} - - -class FloatWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - self._parent = parent - self._as_widget = as_widget - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(FloatWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.float_input = ModifiedFloatSpinBox() - - decimals = input_data.get("decimals", 5) - maximum = input_data.get("maximum") - minimum = input_data.get("minimum") - - self.float_input.setDecimals(decimals) - if maximum is not None: - self.float_input.setMaximum(float(maximum)) - if minimum is not None: - self.float_input.setMinimum(float(minimum)) - - if not self._as_widget and not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - layout.addWidget(self.float_input) - - if not self._as_widget: - self.label_widget = label_widget - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - self.default_value = self.item_value() - self.override_value = None - - self.float_input.valueChanged.connect(self._on_value_change) - - def set_default_values(self, default_values): - value = self.value_from_values(default_values) - if isinstance(value, float): - self.set_value(value, default_value=True) - self.default_value = self.item_value() - else: - self.default_value = value - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def set_value(self, value, *, default_value=False): - self.float_input.setValue(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - value = self.default_value - else: - self._is_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def clear_value(self): - self.set_value(0) - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - widget = self.float_input - else: - property_name = "state" - widget = self.label_widget - - widget.setProperty(property_name, state) - widget.style().polish(widget) - - def item_value(self): - return self.float_input.value() - - def config_value(self): - return {self.key: self.item_value()} - - -class TextSingleLineWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - self._parent = parent - self._as_widget = as_widget - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(TextSingleLineWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.text_input = QtWidgets.QLineEdit() - - if not self._as_widget and not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - layout.addWidget(self.text_input) - - if not self._as_widget: - self.label_widget = label_widget - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - self.default_value = self.item_value() - self.override_value = None - - self.text_input.textChanged.connect(self._on_value_change) - - def set_default_values(self, default_values): - value = self.value_from_values(default_values) - if isinstance(value, str): - self.set_value(value, default_value=True) - self.default_value = self.item_value() - else: - self.default_value = value - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def set_value(self, value, *, default_value=False): - self.text_input.setText(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - self._was_overriden = False - value = self.default_value - else: - self._is_overriden = True - self._was_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def clear_value(self): - self.set_value("") - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - widget = self.text_input - else: - property_name = "state" - widget = self.label_widget - - widget.setProperty(property_name, state) - widget.style().polish(widget) - - def item_value(self): - return self.text_input.text() - - def config_value(self): - return {self.key: self.item_value()} - - -class TextMultiLineWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - self._parent = parent - self._as_widget = as_widget - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(TextMultiLineWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.text_input = QtWidgets.QPlainTextEdit() - if not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - layout.addWidget(self.text_input) - - self.label_widget = label_widget - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - self.default_value = self.item_value() - self.override_value = None - - self.text_input.textChanged.connect(self._on_value_change) - - def set_default_values(self, default_values): - value = self.value_from_values(default_values) - if isinstance(value, str): - self.set_value(value, default_value=True) - self.default_value = self.item_value() - else: - self.default_value = value - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def set_value(self, value, *, default_value=False): - self.text_input.setPlainText(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - value = self.default_value - else: - self._is_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def clear_value(self): - self.set_value("") - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - widget = self.text_input - else: - property_name = "state" - widget = self.label_widget - - widget.setProperty(property_name, state) - widget.style().polish(widget) - - def item_value(self): - return self.text_input.toPlainText() - - def config_value(self): - return {self.key: self.item_value()} - - -class RawJsonInput(QtWidgets.QPlainTextEdit): - tab_length = 4 - - def __init__(self, *args, **kwargs): - super(RawJsonInput, self).__init__(*args, **kwargs) - self.setObjectName("RawJsonInput") - self.setTabStopDistance( - QtGui.QFontMetricsF( - self.font() - ).horizontalAdvance(" ") * self.tab_length - ) - - self.is_valid = None - - def set_value(self, value, *, default_value=False): - self.setPlainText(value) - - def setPlainText(self, *args, **kwargs): - super(RawJsonInput, self).setPlainText(*args, **kwargs) - self.validate() - - def focusOutEvent(self, event): - super(RawJsonInput, self).focusOutEvent(event) - self.validate() - - def validate_value(self, value): - if isinstance(value, str) and not value: - return True - - try: - json.loads(value) - return True - except Exception: - return False - - def update_style(self, is_valid=None): - if is_valid is None: - return self.validate() - - if is_valid != self.is_valid: - self.is_valid = is_valid - if is_valid: - state = "" - else: - state = "invalid" - self.setProperty("state", state) - self.style().polish(self) - - def value(self): - return self.toPlainText() - - def validate(self): - value = self.value() - is_valid = self.validate_value(value) - self.update_style(is_valid) - - -class RawJsonWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - self._parent = parent - self._as_widget = as_widget - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(RawJsonWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.text_input = RawJsonInput() - - if not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - layout.addWidget(self.text_input) - - self.label_widget = label_widget - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - self.default_value = self.item_value() - self.override_value = None - - self.text_input.textChanged.connect(self._on_value_change) - - def set_default_values(self, default_values): - value = self.value_from_values(default_values) - if isinstance(value, str): - self.set_value(value, default_value=True) - self.default_value = self.item_value() - else: - self.default_value = value - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def set_value(self, value, *, default_value=False): - self.text_input.setPlainText(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def clear_value(self): - self.set_value("") - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - value = self.default_value - else: - self._is_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - widget = self.text_input - else: - property_name = "state" - widget = self.label_widget - - widget.setProperty(property_name, state) - widget.style().polish(widget) - - def item_value(self): - return self.text_input.toPlainText() - - def config_value(self): - return {self.key: self.item_value()} - - -class TextListItem(QtWidgets.QWidget, PypeConfigurationWidget): - _btn_size = 20 - value_changed = QtCore.Signal(object) - - def __init__(self, parent): - super(TextListItem, self).__init__(parent) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(3) - - self.text_input = QtWidgets.QLineEdit() - self.add_btn = QtWidgets.QPushButton("+") - self.remove_btn = QtWidgets.QPushButton("-") - - self.add_btn.setProperty("btn-type", "text-list") - self.remove_btn.setProperty("btn-type", "text-list") - - layout.addWidget(self.text_input, 1) - layout.addWidget(self.add_btn, 0) - layout.addWidget(self.remove_btn, 0) - - self.add_btn.setFixedSize(self._btn_size, self._btn_size) - self.remove_btn.setFixedSize(self._btn_size, self._btn_size) - self.add_btn.clicked.connect(self.on_add_clicked) - self.remove_btn.clicked.connect(self.on_remove_clicked) - - self.text_input.textChanged.connect(self._on_value_change) - - self.is_single = False - - def _on_value_change(self, item=None): - self.value_changed.emit(self) - - def row(self): - return self.parent().input_fields.index(self) - - def on_add_clicked(self): - self.parent().add_row(row=self.row() + 1) - - def on_remove_clicked(self): - if self.is_single: - self.text_input.setText("") - else: - self.parent().remove_row(self) - - def config_value(self): - return self.text_input.text() - - -class TextListSubWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__(self, input_data, as_widget, parent_keys, parent): - super(TextListSubWidget, self).__init__(parent) - self.setObjectName("TextListSubWidget") - - self.as_widget = as_widget - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(5, 5, 5, 5) - layout.setSpacing(5) - self.setLayout(layout) - - self.input_fields = [] - self.add_row() - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - self.default_value = self.item_value() - self.override_value = None - - def set_default_values(self, default_values): - value = self.value_from_values(default_values) - if isinstance(value, (list, tuple)): - self.set_value(value, default_value=True) - self.default_value = self.item_value() - else: - self.default_value = value - - def set_value(self, value, *, default_value=False): - for input_field in self.input_fields: - self.remove_row(input_field) - - for item_text in value: - self.add_row(text=item_text) - - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def clear_value(self): - self.set_value([]) - - def _on_value_change(self, item=None): - self.value_changed.emit(self) - - def count(self): - return len(self.input_fields) - - def add_row(self, row=None, text=None): - # Create new item - item_widget = TextListItem(self) - - # Set/unset if new item is single item - current_count = self.count() - if current_count == 0: - item_widget.is_single = True - elif current_count == 1: - for _input_field in self.input_fields: - _input_field.is_single = False - - item_widget.value_changed.connect(self._on_value_change) - - if row is None: - self.layout().addWidget(item_widget) - self.input_fields.append(item_widget) - else: - self.layout().insertWidget(row, item_widget) - self.input_fields.insert(row, item_widget) - - # Set text if entered text is not None - # else (when add button clicked) trigger `_on_value_change` - if text is not None: - item_widget.text_input.setText(text) - else: - self._on_value_change() - self.parent().updateGeometry() - - def remove_row(self, item_widget): - item_widget.value_changed.disconnect() - - self.layout().removeWidget(item_widget) - self.input_fields.remove(item_widget) - item_widget.setParent(None) - item_widget.deleteLater() - - current_count = self.count() - if current_count == 0: - self.add_row() - elif current_count == 1: - for _input_field in self.input_fields: - _input_field.is_single = True - - self._on_value_change() - self.parent().updateGeometry() - - def item_value(self): - output = [] - for item in self.input_fields: - text = item.config_value() - if text: - output.append(text) - - return output - - def config_value(self): - return {self.key: self.item_value()} - - -class TextListWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - self._parent = parent - self._as_widget = as_widget - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self._is_modified = False - self.is_group = is_group - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(TextListWidget, self).__init__(parent) - self.setObjectName("TextListWidget") - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - if not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - - self.label_widget = label_widget - # keys = list(parent_keys) - # keys.append(input_data["key"]) - # self.keys = keys - - self.value_widget = TextListSubWidget( - input_data, values, parent_keys, self - ) - self.value_widget.setAttribute(QtCore.Qt.WA_StyledBackground) - self.value_widget.value_changed.connect(self._on_value_change) - - # self.value_widget.se - self.key = input_data["key"] - layout.addWidget(self.value_widget) - self.setLayout(layout) - - self.default_value = self.item_value() - self.override_value = None - - def set_default_values(self, default_values): - value = self.value_from_values(default_values) - if isinstance(value, (list, tuple)): - self.set_value(value, default_value=True) - self.default_value = self.item_value() - else: - self.default_value = value - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def set_value(self, value, *, default_value=False): - self.value_widget.set_value(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def clear_value(self): - self.set_value([]) - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - value = self.default_value - else: - self._is_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - def item_value(self): - return self.value_widget.config_value() - - def config_value(self): - return {self.key: self.item_value()} - - -class DictExpandWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - if as_widget: - raise TypeError("Can't use \"{}\" as widget item.".format( - self.__class__.__name__ - )) - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - self.any_parent_is_group = any_parent_is_group - - self._is_modified = False - self._is_overriden = False - self.is_group = is_group - - self._state = None - self._child_state = None - - super(DictExpandWidget, self).__init__(parent) - self.setObjectName("DictExpandWidget") - top_part = ClickableWidget(parent=self) - - button_size = QtCore.QSize(5, 5) - button_toggle = QtWidgets.QToolButton(parent=top_part) - button_toggle.setProperty("btn-type", "expand-toggle") - button_toggle.setIconSize(button_size) - button_toggle.setArrowType(QtCore.Qt.RightArrow) - button_toggle.setCheckable(True) - button_toggle.setChecked(False) - - label = input_data["label"] - button_toggle_text = QtWidgets.QLabel(label, parent=top_part) - button_toggle_text.setObjectName("ExpandLabel") - - layout = QtWidgets.QHBoxLayout(top_part) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - layout.addWidget(button_toggle) - layout.addWidget(button_toggle_text) - top_part.setLayout(layout) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.setContentsMargins(9, 9, 9, 9) - - content_widget = QtWidgets.QWidget(self) - content_widget.setVisible(False) - - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(3, 3, 3, 3) - - main_layout.addWidget(top_part) - main_layout.addWidget(content_widget) - self.setLayout(main_layout) - - self.setAttribute(QtCore.Qt.WA_StyledBackground) - - self.top_part = top_part - self.button_toggle = button_toggle - self.button_toggle_text = button_toggle_text - - self.content_widget = content_widget - self.content_layout = content_layout - - self.top_part.clicked.connect(self._top_part_clicked) - self.button_toggle.clicked.connect(self.toggle_content) - - self.input_fields = [] - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - for child_data in input_data.get("children", []): - self.add_children_gui(child_data, values) - - def set_default_values(self, default_values): - for input_field in self.input_fields: - input_field.set_default_values(default_values) - - def _top_part_clicked(self): - self.toggle_content(not self.button_toggle.isChecked()) - - def toggle_content(self, *args): - if len(args) > 0: - checked = args[0] - else: - checked = self.button_toggle.isChecked() - arrow_type = QtCore.Qt.RightArrow - if checked: - arrow_type = QtCore.Qt.DownArrow - self.button_toggle.setChecked(checked) - self.button_toggle.setArrowType(arrow_type) - self.content_widget.setVisible(checked) - self.parent().updateGeometry() - - def resizeEvent(self, event): - super(DictExpandWidget, self).resizeEvent(event) - self.content_widget.updateGeometry() - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def apply_overrides(self, override_value): - # Make sure this is set to False - self._is_overriden = False - self._state = None - self._child_state = None - for item in self.input_fields: - if override_value is None: - child_value = None - else: - child_value = override_value.get(item.key) - - item.apply_overrides(child_value) - - self._is_overriden = ( - self.is_group - and self.is_overidable - and ( - override_value is not None - or self.child_overriden - ) - ) - self.update_style() - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - if self.is_group: - if self.is_overidable: - self._is_overriden = True - - # TODO update items - if item is not None: - for _item in self.input_fields: - if _item is not item: - _item.update_style() - - self.value_changed.emit(self) - - self.update_style() - - def update_style(self, is_overriden=None): - child_modified = self.child_modified - child_state = self.style_state(self.child_overriden, child_modified) - if child_state: - child_state = "child-{}".format(child_state) - - if child_state != self._child_state: - self.setProperty("state", child_state) - self.style().polish(self) - self._child_state = child_state - - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - self.button_toggle_text.setProperty("state", state) - self.button_toggle_text.style().polish(self.button_toggle_text) - - self._state = state - - @property - def is_modified(self): - if self.is_group: - return self.child_modified - return False - - @property - def child_modified(self): - for input_field in self.input_fields: - if input_field.child_modified: - return True - return False - - @property - def child_overriden(self): - for input_field in self.input_fields: - if input_field.child_overriden: - return True - return False - - def item_value(self): - output = {} - for input_field in self.input_fields: - # TODO maybe merge instead of update should be used - # NOTE merge is custom function which merges 2 dicts - output.update(input_field.config_value()) - return output - - def config_value(self): - return {self.key: self.item_value()} - - @property - def is_overidable(self): - return self._parent.is_overidable - - def add_children_gui(self, child_configuration, values): - item_type = child_configuration["type"] - klass = TypeToKlass.types.get(item_type) - - item = klass( - child_configuration, values, self.keys, self - ) - item.value_changed.connect(self._on_value_change) - self.content_layout.addWidget(item) - - self.input_fields.append(item) - return item - - -class DictInvisible(QtWidgets.QWidget, PypeConfigurationWidget): - # TODO is not overridable by itself - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - self.any_parent_is_group = any_parent_is_group - - self._is_overriden = False - self.is_modified = False - self.is_group = is_group - - super(DictInvisible, self).__init__(parent) - self.setObjectName("DictInvisible") - - self.setAttribute(QtCore.Qt.WA_StyledBackground) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - - self.input_fields = [] - - self.key = input_data["key"] - self.keys = list(parent_keys) - self.keys.append(self.key) - - for child_data in input_data.get("children", []): - self.add_children_gui(child_data, values) - - def set_default_values(self, default_values): - for input_field in self.input_fields: - input_field.set_default_values(default_values) - - def update_style(self, *args, **kwargs): - return - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def child_modified(self): - for input_field in self.input_fields: - if input_field.child_modified: - return True - return False - - @property - def child_overriden(self): - for input_field in self.input_fields: - if input_field.child_overriden: - return True - return False - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def item_value(self): - output = {} - for input_field in self.input_fields: - # TODO maybe merge instead of update should be used - # NOTE merge is custom function which merges 2 dicts - output.update(input_field.config_value()) - return output - - def config_value(self): - return {self.key: self.item_value()} - - def add_children_gui(self, child_configuration, values): - item_type = child_configuration["type"] - if item_type == "schema": - for _schema in child_configuration["children"]: - children = config.gui_schema(_schema) - self.add_children_gui(children, values) - return - - klass = TypeToKlass.types.get(item_type) - item = klass( - child_configuration, values, self.keys, self - ) - self.layout().addWidget(item) - - item.value_changed.connect(self._on_value_change) - - self.input_fields.append(item) - return item - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - if self.is_group: - if self.is_overidable: - self._is_overriden = True - # TODO update items - if item is not None: - is_overriden = self.is_overriden - for _item in self.input_fields: - if _item is not item: - _item.update_style(is_overriden) - - self.value_changed.emit(self) - - def apply_overrides(self, override_value): - self._is_overriden = False - for item in self.input_fields: - if override_value is None: - child_value = None - else: - child_value = override_value.get(item.key) - item.apply_overrides(child_value) - - self._is_overriden = ( - self.is_group - and self.is_overidable - and ( - override_value is not None - or self.child_overriden - ) - ) - self.update_style() - - -class DictFormWidget(QtWidgets.QWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - self.any_parent_is_group = any_parent_is_group - - self.is_modified = False - self.is_overriden = False - self.is_group = False - - super(DictFormWidget, self).__init__(parent) - - self.input_fields = {} - self.content_layout = QtWidgets.QFormLayout(self) - - self.keys = list(parent_keys) - - for child_data in input_data.get("children", []): - self.add_children_gui(child_data, values) - - def set_default_values(self, default_values): - for key, input_field in self.input_fields.items(): - input_field.set_default_values(default_values) - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - self.value_changed.emit(self) - - def item_value(self): - output = {} - for input_field in self.input_fields.values(): - # TODO maybe merge instead of update should be used - # NOTE merge is custom function which merges 2 dicts - output.update(input_field.config_value()) - return output - - @property - def child_modified(self): - for input_field in self.input_fields.values(): - if input_field.child_modified: - return True - return False - - @property - def child_overriden(self): - for input_field in self.input_fields.values(): - if input_field.child_overriden: - return True - return False - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def config_value(self): - return self.item_value() - - def add_children_gui(self, child_configuration, values): - item_type = child_configuration["type"] - key = child_configuration["key"] - # Pop label to not be set in child - label = child_configuration["label"] - - klass = TypeToKlass.types.get(item_type) - - label_widget = QtWidgets.QLabel(label) - - item = klass( - child_configuration, values, self.keys, self, label_widget - ) - item.value_changed.connect(self._on_value_change) - self.content_layout.addRow(label_widget, item) - self.input_fields[key] = item - return item - - -class ModifiableDictItem(QtWidgets.QWidget, PypeConfigurationWidget): - _btn_size = 20 - value_changed = QtCore.Signal(object) - - def __init__(self, object_type, parent): - self._parent = parent - - super(ModifiableDictItem, self).__init__(parent) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(3) - - ItemKlass = TypeToKlass.types[object_type] - - self.key_input = QtWidgets.QLineEdit() - self.key_input.setObjectName("DictKey") - - self.value_input = ItemKlass( - {}, - AS_WIDGET, - [], - self, - None - ) - self.add_btn = QtWidgets.QPushButton("+") - self.remove_btn = QtWidgets.QPushButton("-") - - self.add_btn.setProperty("btn-type", "text-list") - self.remove_btn.setProperty("btn-type", "text-list") - - layout.addWidget(self.key_input, 0) - layout.addWidget(self.value_input, 1) - layout.addWidget(self.add_btn, 0) - layout.addWidget(self.remove_btn, 0) - - self.add_btn.setFixedSize(self._btn_size, self._btn_size) - self.remove_btn.setFixedSize(self._btn_size, self._btn_size) - self.add_btn.clicked.connect(self.on_add_clicked) - self.remove_btn.clicked.connect(self.on_remove_clicked) - - self.key_input.textChanged.connect(self._on_value_change) - self.value_input.value_changed.connect(self._on_value_change) - - self.default_key = self._key() - self.default_value = self.value_input.item_value() - - self.override_key = None - self.override_value = None - - self.is_single = False - - def _key(self): - return self.key_input.text() - - def _on_value_change(self, item=None): - self.update_style() - self.value_changed.emit(self) - - @property - def is_group(self): - return self._parent.is_group - - @property - def any_parent_is_group(self): - return self._parent.any_parent_is_group - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def is_key_modified(self): - return self._key() != self.default_key - - def is_value_modified(self): - return self.value_input.is_modified - - @property - def is_modified(self): - return self.is_value_modified() or self.is_key_modified() - - def update_style(self): - if self.is_key_modified(): - state = "modified" - else: - state = "" - - self.key_input.setProperty("state", state) - self.key_input.style().polish(self.key_input) - - def row(self): - return self.parent().input_fields.index(self) - - def on_add_clicked(self): - self.parent().add_row(row=self.row() + 1) - - def on_remove_clicked(self): - if self.is_single: - self.value_input.clear_value() - self.key_input.setText("") - else: - self.parent().remove_row(self) - - def config_value(self): - key = self.key_input.text() - value = self.value_input.item_value() - if not key: - return {} - return {key: value} - - -class ModifiableDictSubWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__(self, input_data, as_widget, parent_keys, parent): - self._parent = parent - - super(ModifiableDictSubWidget, self).__init__(parent) - self.setObjectName("ModifiableDictSubWidget") - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(5, 5, 5, 5) - layout.setSpacing(5) - self.setLayout(layout) - - self.input_fields = [] - self.object_type = input_data["object_type"] - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - if self.count() == 0: - self.add_row() - - self.default_value = self.config_value() - self.override_value = None - - def set_default_values(self, default_values): - for input_field in self.input_fields: - self.remove_row(input_field) - - value = self.value_from_values(default_values) - if value is NOT_SET: - self.defaul_value = value - return - - for item_key, item_value in value.items(): - self.add_row(key=item_key, value=item_value) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._parent.is_overriden - - @property - def is_group(self): - return self._parent.is_group - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - @property - def any_parent_is_group(self): - return self._parent.any_parent_is_group - - def _on_value_change(self, item=None): - self.value_changed.emit(self) - - def count(self): - return len(self.input_fields) - - def add_row(self, row=None, key=None, value=None): - # Create new item - item_widget = ModifiableDictItem(self.object_type, self) - - # Set/unset if new item is single item - current_count = self.count() - if current_count == 0: - item_widget.is_single = True - elif current_count == 1: - for _input_field in self.input_fields: - _input_field.is_single = False - - item_widget.value_changed.connect(self._on_value_change) - - if row is None: - self.layout().addWidget(item_widget) - self.input_fields.append(item_widget) - else: - self.layout().insertWidget(row, item_widget) - self.input_fields.insert(row, item_widget) - - # Set value if entered value is not None - # else (when add button clicked) trigger `_on_value_change` - if value is not None and key is not None: - item_widget.default_key = key - item_widget.key_input.setText(key) - item_widget.value_input.set_value(value, default_value=True) - else: - self._on_value_change() - self.parent().updateGeometry() - - def remove_row(self, item_widget): - item_widget.value_changed.disconnect() - - self.layout().removeWidget(item_widget) - self.input_fields.remove(item_widget) - item_widget.setParent(None) - item_widget.deleteLater() - - current_count = self.count() - if current_count == 0: - self.add_row() - elif current_count == 1: - for _input_field in self.input_fields: - _input_field.is_single = True - - self._on_value_change() - self.parent().updateGeometry() - - def config_value(self): - output = {} - for item in self.input_fields: - item_value = item.config_value() - if item_value: - output.update(item_value) - return output - - -class ModifiableDict(ExpandingWidget, PypeConfigurationWidget): - # Should be used only for dictionary with one datatype as value - # TODO this is actually input field (do not care if is group or not) - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent_keys, parent, - as_widget=False, label_widget=None - ): - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.any_parent_is_group = any_parent_is_group - - self.is_group = is_group - self._is_modified = False - self._is_overriden = False - self._was_overriden = False - self._state = None - - super(ModifiableDict, self).__init__(input_data["label"], parent) - self.setObjectName("ModifiableDict") - - self.value_widget = ModifiableDictSubWidget( - input_data, as_widget, parent_keys, self - ) - self.value_widget.setAttribute(QtCore.Qt.WA_StyledBackground) - self.value_widget.value_changed.connect(self._on_value_change) - - self.set_content_widget(self.value_widget) - - self.key = input_data["key"] - - self.default_value = self.item_value() - self.override_value = None - - def set_default_values(self, default_values): - self.value_widget.set_default_values(default_values) - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - if self.is_overidable: - self._is_overriden = True - - if self.is_overriden: - self._is_modified = self.item_value() != self.override_value - else: - self._is_modified = self.item_value() != self.default_value - - self.value_changed.emit(self) - - self.update_style() - - @property - def child_modified(self): - return self.is_modified - - @property - def is_modified(self): - return self._is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def is_modified(self): - return self._is_modified - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def apply_overrides(self, override_value): - self._state = None - self._is_modified = False - self.override_value = override_value - if override_value is None: - self._is_overriden = False - self._was_overriden = False - value = self.default_value - else: - self._is_overriden = True - self._was_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if state: - child_state = "child-{}".format(state) - else: - child_state = "" - - self.setProperty("state", child_state) - self.style().polish(self) - - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - self._state = state - - def item_value(self): - return self.value_widget.config_value() - - def config_value(self): - return {self.key: self.item_value()} - - -TypeToKlass.types["boolean"] = BooleanWidget -TypeToKlass.types["text-singleline"] = TextSingleLineWidget -TypeToKlass.types["text-multiline"] = TextMultiLineWidget -TypeToKlass.types["raw-json"] = RawJsonWidget -TypeToKlass.types["int"] = IntegerWidget -TypeToKlass.types["float"] = FloatWidget -TypeToKlass.types["dict-expanding"] = DictExpandWidget -TypeToKlass.types["dict-form"] = DictFormWidget -TypeToKlass.types["dict-invisible"] = DictInvisible -TypeToKlass.types["dict-modifiable"] = ModifiableDict -TypeToKlass.types["list-text"] = TextListWidget diff --git a/pype/tools/config_setting/widgets/inputs1.py b/pype/tools/config_setting/widgets/inputs1.py deleted file mode 100644 index f9eb60f31a..0000000000 --- a/pype/tools/config_setting/widgets/inputs1.py +++ /dev/null @@ -1,2131 +0,0 @@ -import json -from Qt import QtWidgets, QtCore, QtGui -from . import config -from .base import PypeConfigurationWidget, TypeToKlass -from .widgets import ( - ClickableWidget, - ExpandingWidget, - ModifiedIntSpinBox, - ModifiedFloatSpinBox -) -from .lib import NOT_SET, AS_WIDGET - - -class SchemeGroupHierarchyBug(Exception): - def __init__(self, msg=None): - if not msg: - # TODO better message - msg = "SCHEME BUG: Attribute `is_group` is mixed in the hierarchy" - super(SchemeGroupHierarchyBug, self).__init__(msg) - - -class BooleanWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, label_widget=None - ): - self._as_widget = values is AS_WIDGET - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(BooleanWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.checkbox = QtWidgets.QCheckBox() - self.checkbox.setAttribute(QtCore.Qt.WA_StyledBackground) - if not self._as_widget and not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - label_widget.setAttribute(QtCore.Qt.WA_StyledBackground) - layout.addWidget(label_widget) - - layout.addWidget(self.checkbox) - - if not self._as_widget: - self.label_widget = label_widget - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - value = self.value_from_values(values) - if value is not NOT_SET: - self.checkbox.setChecked(value) - - self.default_value = self.item_value() - self.override_value = None - - self.checkbox.stateChanged.connect(self._on_value_change) - - def set_value(self, value, *, default_value=False): - # Ignore value change because if `self.isChecked()` has same - # value as `value` the `_on_value_change` is not triggered - self.checkbox.setChecked(value) - - if default_value: - self.default_value = self.item_value() - - self._on_value_change() - - def reset_value(self): - if self.is_overidable and self.override_value is not None: - self.set_value(self.override_value) - else: - self.set_value(self.default_value) - - def clear_value(self): - self.reset_value() - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - self._was_overriden = False - value = self.default_value - else: - self._is_overriden = True - self._was_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - _value = self.item_value() - is_modified = None - if self.is_overidable: - self._is_overriden = True - if self.override_value is not None: - is_modified = _value != self.override_value - - if is_modified is None: - is_modified = _value != self.default_value - - self._is_modified = is_modified - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - else: - property_name = "state" - - self.label_widget.setProperty(property_name, state) - self.label_widget.style().polish(self.label_widget) - self._state = state - - def item_value(self): - return self.checkbox.isChecked() - - def config_value(self): - return {self.key: self.item_value()} - - def override_value(self): - if self.is_overriden: - output = { - "is_group": self.is_group, - "value": self.config_value() - } - return output - - -class IntegerWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, label_widget=None - ): - self._parent = parent - self._as_widget = values is AS_WIDGET - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(IntegerWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.int_input = ModifiedIntSpinBox() - - if not self._as_widget and not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - layout.addWidget(self.int_input) - - if not self._as_widget: - self.label_widget = label_widget - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - value = self.value_from_values(values) - if value is not NOT_SET: - self.int_input.setValue(value) - - self.default_value = self.item_value() - self.override_value = None - - self.int_input.valueChanged.connect(self._on_value_change) - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def set_value(self, value, *, default_value=False): - self.int_input.setValue(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def clear_value(self): - self.set_value(0) - - def reset_value(self): - self.set_value(self.default_value) - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - self._was_overriden = False - value = self.default_value - else: - self._is_overriden = True - self._was_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - widget = self.int_input - else: - property_name = "state" - widget = self.label_widget - - widget.setProperty(property_name, state) - widget.style().polish(widget) - - def item_value(self): - return self.int_input.value() - - def config_value(self): - return {self.key: self.item_value()} - - -class FloatWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, label_widget=None - ): - self._parent = parent - self._as_widget = values is AS_WIDGET - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(FloatWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.float_input = ModifiedFloatSpinBox() - - decimals = input_data.get("decimals", 5) - maximum = input_data.get("maximum") - minimum = input_data.get("minimum") - - self.float_input.setDecimals(decimals) - if maximum is not None: - self.float_input.setMaximum(float(maximum)) - if minimum is not None: - self.float_input.setMinimum(float(minimum)) - - if not self._as_widget and not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - layout.addWidget(self.float_input) - - if not self._as_widget: - self.label_widget = label_widget - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - value = self.value_from_values(values) - if value is not NOT_SET: - self.float_input.setValue(value) - - self.default_value = self.item_value() - self.override_value = None - - self.float_input.valueChanged.connect(self._on_value_change) - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def set_value(self, value, *, default_value=False): - self.float_input.setValue(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - value = self.default_value - else: - self._is_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def clear_value(self): - self.set_value(0) - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - widget = self.float_input - else: - property_name = "state" - widget = self.label_widget - - widget.setProperty(property_name, state) - widget.style().polish(widget) - - def item_value(self): - return self.float_input.value() - - def config_value(self): - return {self.key: self.item_value()} - - -class TextSingleLineWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, label_widget=None - ): - self._parent = parent - self._as_widget = values is AS_WIDGET - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(TextSingleLineWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.text_input = QtWidgets.QLineEdit() - - if not self._as_widget and not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - layout.addWidget(self.text_input) - - if not self._as_widget: - self.label_widget = label_widget - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - value = self.value_from_values(values) - if value is not NOT_SET: - self.text_input.setText(value) - - self.default_value = self.item_value() - self.override_value = None - - self.text_input.textChanged.connect(self._on_value_change) - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def set_value(self, value, *, default_value=False): - self.text_input.setText(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - self._was_overriden = False - value = self.default_value - else: - self._is_overriden = True - self._was_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def clear_value(self): - self.set_value("") - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - widget = self.text_input - else: - property_name = "state" - widget = self.label_widget - - widget.setProperty(property_name, state) - widget.style().polish(widget) - - def item_value(self): - return self.text_input.text() - - def config_value(self): - return {self.key: self.item_value()} - - -class TextMultiLineWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, label_widget=None - ): - self._parent = parent - self._as_widget = values is AS_WIDGET - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(TextMultiLineWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.text_input = QtWidgets.QPlainTextEdit() - if not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - layout.addWidget(self.text_input) - - self.label_widget = label_widget - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - value = self.value_from_values(values) - if value is not NOT_SET: - self.text_input.setPlainText(value) - - self.default_value = self.item_value() - self.override_value = None - - self.text_input.textChanged.connect(self._on_value_change) - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def set_value(self, value, *, default_value=False): - self.text_input.setPlainText(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - value = self.default_value - else: - self._is_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def clear_value(self): - self.set_value("") - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - widget = self.text_input - else: - property_name = "state" - widget = self.label_widget - - widget.setProperty(property_name, state) - widget.style().polish(widget) - - def item_value(self): - return self.text_input.toPlainText() - - def config_value(self): - return {self.key: self.item_value()} - - -class RawJsonInput(QtWidgets.QPlainTextEdit): - tab_length = 4 - - def __init__(self, *args, **kwargs): - super(RawJsonInput, self).__init__(*args, **kwargs) - self.setObjectName("RawJsonInput") - self.setTabStopDistance( - QtGui.QFontMetricsF( - self.font() - ).horizontalAdvance(" ") * self.tab_length - ) - - self.is_valid = None - - def set_value(self, value, *, default_value=False): - self.setPlainText(value) - - def setPlainText(self, *args, **kwargs): - super(RawJsonInput, self).setPlainText(*args, **kwargs) - self.validate() - - def focusOutEvent(self, event): - super(RawJsonInput, self).focusOutEvent(event) - self.validate() - - def validate_value(self, value): - if isinstance(value, str) and not value: - return True - - try: - json.loads(value) - return True - except Exception: - return False - - def update_style(self, is_valid=None): - if is_valid is None: - return self.validate() - - if is_valid != self.is_valid: - self.is_valid = is_valid - if is_valid: - state = "" - else: - state = "invalid" - self.setProperty("state", state) - self.style().polish(self) - - def value(self): - return self.toPlainText() - - def validate(self): - value = self.value() - is_valid = self.validate_value(value) - self.update_style(is_valid) - - -class RawJsonWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, label_widget=None - ): - self._parent = parent - self._as_widget = values is AS_WIDGET - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.is_group = is_group - self._is_modified = False - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(RawJsonWidget, self).__init__(parent) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.text_input = RawJsonInput() - - if not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - layout.addWidget(self.text_input) - - self.label_widget = label_widget - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - value = self.value_from_values(values) - if value is not NOT_SET: - self.text_input.setPlainText(value) - - self.default_value = self.item_value() - self.override_value = None - - self.text_input.textChanged.connect(self._on_value_change) - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def set_value(self, value, *, default_value=False): - self.text_input.setPlainText(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def clear_value(self): - self.set_value("") - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - value = self.default_value - else: - self._is_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - widget = self.text_input - else: - property_name = "state" - widget = self.label_widget - - widget.setProperty(property_name, state) - widget.style().polish(widget) - - def item_value(self): - return self.text_input.toPlainText() - - def config_value(self): - return {self.key: self.item_value()} - - -class TextListItem(QtWidgets.QWidget, PypeConfigurationWidget): - _btn_size = 20 - value_changed = QtCore.Signal(object) - - def __init__(self, parent): - super(TextListItem, self).__init__(parent) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(3) - - self.text_input = QtWidgets.QLineEdit() - self.add_btn = QtWidgets.QPushButton("+") - self.remove_btn = QtWidgets.QPushButton("-") - - self.add_btn.setProperty("btn-type", "text-list") - self.remove_btn.setProperty("btn-type", "text-list") - - layout.addWidget(self.text_input, 1) - layout.addWidget(self.add_btn, 0) - layout.addWidget(self.remove_btn, 0) - - self.add_btn.setFixedSize(self._btn_size, self._btn_size) - self.remove_btn.setFixedSize(self._btn_size, self._btn_size) - self.add_btn.clicked.connect(self.on_add_clicked) - self.remove_btn.clicked.connect(self.on_remove_clicked) - - self.text_input.textChanged.connect(self._on_value_change) - - self.is_single = False - - def _on_value_change(self, item=None): - self.value_changed.emit(self) - - def row(self): - return self.parent().input_fields.index(self) - - def on_add_clicked(self): - self.parent().add_row(row=self.row() + 1) - - def on_remove_clicked(self): - if self.is_single: - self.text_input.setText("") - else: - self.parent().remove_row(self) - - def config_value(self): - return self.text_input.text() - - -class TextListSubWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__(self, input_data, values, parent_keys, parent): - super(TextListSubWidget, self).__init__(parent) - self.setObjectName("TextListSubWidget") - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(5, 5, 5, 5) - layout.setSpacing(5) - self.setLayout(layout) - - self.input_fields = [] - self.add_row() - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - value = self.value_from_values(values) - if value is not NOT_SET: - self.set_value(value) - - self.default_value = self.item_value() - self.override_value = None - - def set_value(self, value, *, default_value=False): - for input_field in self.input_fields: - self.remove_row(input_field) - - for item_text in value: - self.add_row(text=item_text) - - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def clear_value(self): - self.set_value([]) - - def _on_value_change(self, item=None): - self.value_changed.emit(self) - - def count(self): - return len(self.input_fields) - - def add_row(self, row=None, text=None): - # Create new item - item_widget = TextListItem(self) - - # Set/unset if new item is single item - current_count = self.count() - if current_count == 0: - item_widget.is_single = True - elif current_count == 1: - for _input_field in self.input_fields: - _input_field.is_single = False - - item_widget.value_changed.connect(self._on_value_change) - - if row is None: - self.layout().addWidget(item_widget) - self.input_fields.append(item_widget) - else: - self.layout().insertWidget(row, item_widget) - self.input_fields.insert(row, item_widget) - - # Set text if entered text is not None - # else (when add button clicked) trigger `_on_value_change` - if text is not None: - item_widget.text_input.setText(text) - else: - self._on_value_change() - self.parent().updateGeometry() - - def remove_row(self, item_widget): - item_widget.value_changed.disconnect() - - self.layout().removeWidget(item_widget) - self.input_fields.remove(item_widget) - item_widget.setParent(None) - item_widget.deleteLater() - - current_count = self.count() - if current_count == 0: - self.add_row() - elif current_count == 1: - for _input_field in self.input_fields: - _input_field.is_single = True - - self._on_value_change() - self.parent().updateGeometry() - - def item_value(self): - output = [] - for item in self.input_fields: - text = item.config_value() - if text: - output.append(text) - - return output - - def config_value(self): - return {self.key: self.item_value()} - - -class TextListWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, label_widget=None - ): - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self._is_modified = False - self.is_group = is_group - self._was_overriden = False - self._is_overriden = False - - self._state = None - - super(TextListWidget, self).__init__(parent) - self.setObjectName("TextListWidget") - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - if not label_widget: - label = input_data["label"] - label_widget = QtWidgets.QLabel(label) - layout.addWidget(label_widget) - - self.label_widget = label_widget - # keys = list(parent_keys) - # keys.append(input_data["key"]) - # self.keys = keys - - self.value_widget = TextListSubWidget( - input_data, values, parent_keys, self - ) - self.value_widget.setAttribute(QtCore.Qt.WA_StyledBackground) - self.value_widget.value_changed.connect(self._on_value_change) - - # self.value_widget.se - self.key = input_data["key"] - layout.addWidget(self.value_widget) - self.setLayout(layout) - - self.default_value = self.item_value() - self.override_value = None - - @property - def child_modified(self): - return self.is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_modified(self): - return self._is_modified or (self._was_overriden != self.is_overriden) - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - self._is_modified = self.item_value() != self.default_value - if self.is_overidable: - self._is_overriden = True - - self.update_style() - - self.value_changed.emit(self) - - def set_value(self, value, *, default_value=False): - self.value_widget.set_value(value) - if default_value: - self.default_value = self.item_value() - self._on_value_change() - - def reset_value(self): - self.set_value(self.default_value) - - def clear_value(self): - self.set_value([]) - - def apply_overrides(self, override_value): - self._is_modified = False - self._state = None - self.override_value = override_value - if override_value is None: - self._is_overriden = False - value = self.default_value - else: - self._is_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - def item_value(self): - return self.value_widget.config_value() - - def config_value(self): - return {self.key: self.item_value()} - - -class DictExpandWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, label_widget=None - ): - if values is AS_WIDGET: - raise TypeError("Can't use \"{}\" as widget item.".format( - self.__class__.__name__ - )) - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - self.any_parent_is_group = any_parent_is_group - - self._is_modified = False - self._is_overriden = False - self.is_group = is_group - - self._state = None - self._child_state = None - - super(DictExpandWidget, self).__init__(parent) - self.setObjectName("DictExpandWidget") - top_part = ClickableWidget(parent=self) - - button_size = QtCore.QSize(5, 5) - button_toggle = QtWidgets.QToolButton(parent=top_part) - button_toggle.setProperty("btn-type", "expand-toggle") - button_toggle.setIconSize(button_size) - button_toggle.setArrowType(QtCore.Qt.RightArrow) - button_toggle.setCheckable(True) - button_toggle.setChecked(False) - - label = input_data["label"] - button_toggle_text = QtWidgets.QLabel(label, parent=top_part) - button_toggle_text.setObjectName("ExpandLabel") - - layout = QtWidgets.QHBoxLayout(top_part) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - layout.addWidget(button_toggle) - layout.addWidget(button_toggle_text) - top_part.setLayout(layout) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.setContentsMargins(9, 9, 9, 9) - - content_widget = QtWidgets.QWidget(self) - content_widget.setVisible(False) - - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(3, 3, 3, 3) - - main_layout.addWidget(top_part) - main_layout.addWidget(content_widget) - self.setLayout(main_layout) - - self.setAttribute(QtCore.Qt.WA_StyledBackground) - - self.top_part = top_part - self.button_toggle = button_toggle - self.button_toggle_text = button_toggle_text - - self.content_widget = content_widget - self.content_layout = content_layout - - self.top_part.clicked.connect(self._top_part_clicked) - self.button_toggle.clicked.connect(self.toggle_content) - - self.input_fields = [] - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - for child_data in input_data.get("children", []): - self.add_children_gui(child_data, values) - - def _top_part_clicked(self): - self.toggle_content(not self.button_toggle.isChecked()) - - def toggle_content(self, *args): - if len(args) > 0: - checked = args[0] - else: - checked = self.button_toggle.isChecked() - arrow_type = QtCore.Qt.RightArrow - if checked: - arrow_type = QtCore.Qt.DownArrow - self.button_toggle.setChecked(checked) - self.button_toggle.setArrowType(arrow_type) - self.content_widget.setVisible(checked) - self.parent().updateGeometry() - - def resizeEvent(self, event): - super(DictExpandWidget, self).resizeEvent(event) - self.content_widget.updateGeometry() - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def apply_overrides(self, override_value): - # Make sure this is set to False - self._is_overriden = False - self._state = None - self._child_state = None - for item in self.input_fields: - if override_value is None: - child_value = None - else: - child_value = override_value.get(item.key) - - item.apply_overrides(child_value) - - self._is_overriden = ( - self.is_group - and self.is_overidable - and ( - override_value is not None - or self.child_overriden - ) - ) - self.update_style() - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - if self.is_group: - if self.is_overidable: - self._is_overriden = True - - # TODO update items - if item is not None: - for _item in self.input_fields: - if _item is not item: - _item.update_style() - - self.value_changed.emit(self) - - self.update_style() - - def update_style(self, is_overriden=None): - child_modified = self.child_modified - child_state = self.style_state(self.child_overriden, child_modified) - if child_state: - child_state = "child-{}".format(child_state) - - if child_state != self._child_state: - self.setProperty("state", child_state) - self.style().polish(self) - self._child_state = child_state - - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - self.button_toggle_text.setProperty("state", state) - self.button_toggle_text.style().polish(self.button_toggle_text) - - self._state = state - - @property - def is_modified(self): - if self.is_group: - return self.child_modified - return False - - @property - def child_modified(self): - for input_field in self.input_fields: - if input_field.child_modified: - return True - return False - - @property - def child_overriden(self): - for input_field in self.input_fields: - if input_field.child_overriden: - return True - return False - - def item_value(self): - output = {} - for input_field in self.input_fields: - # TODO maybe merge instead of update should be used - # NOTE merge is custom function which merges 2 dicts - output.update(input_field.config_value()) - return output - - def config_value(self): - return {self.key: self.item_value()} - - @property - def is_overidable(self): - return self._parent.is_overidable - - def add_children_gui(self, child_configuration, values): - item_type = child_configuration["type"] - klass = TypeToKlass.types.get(item_type) - - item = klass( - child_configuration, values, self.keys, self - ) - item.value_changed.connect(self._on_value_change) - self.content_layout.addWidget(item) - - self.input_fields.append(item) - return item - - def override_values(self): - if not self.is_overriden and not self.child_overriden: - return - - value = {} - for item in self.input_fields: - if hasattr(item, "override_values"): - print("*** HAVE `override_values`", item) - print(item.override_values()) - else: - print("*** missing `override_values`", item) - - if not value: - return - - output = { - "is_group": self.is_group, - "value": value - } - return output - - -class DictInvisible(QtWidgets.QWidget, PypeConfigurationWidget): - # TODO is not overridable by itself - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, label_widget=None - ): - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - self.any_parent_is_group = any_parent_is_group - - self._is_overriden = False - self.is_modified = False - self.is_group = is_group - - super(DictInvisible, self).__init__(parent) - self.setObjectName("DictInvisible") - - self.setAttribute(QtCore.Qt.WA_StyledBackground) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - - self.input_fields = [] - - if "key" not in input_data: - print(json.dumps(input_data, indent=4)) - - self.key = input_data["key"] - self.keys = list(parent_keys) - self.keys.append(self.key) - - for child_data in input_data.get("children", []): - self.add_children_gui(child_data, values) - - def update_style(self, *args, **kwargs): - return - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def child_modified(self): - for input_field in self.input_fields: - if input_field.child_modified: - return True - return False - - @property - def child_overriden(self): - for input_field in self.input_fields: - if input_field.child_overriden: - return True - return False - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def item_value(self): - output = {} - for input_field in self.input_fields: - # TODO maybe merge instead of update should be used - # NOTE merge is custom function which merges 2 dicts - output.update(input_field.config_value()) - return output - - def config_value(self): - return {self.key: self.item_value()} - - def add_children_gui(self, child_configuration, values): - item_type = child_configuration["type"] - if item_type == "schema": - for _schema in child_configuration["children"]: - children = config.gui_schema(_schema) - self.add_children_gui(children, values) - return - - klass = TypeToKlass.types.get(item_type) - item = klass( - child_configuration, values, self.keys, self - ) - self.layout().addWidget(item) - - item.value_changed.connect(self._on_value_change) - - self.input_fields.append(item) - return item - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - if self.is_group: - if self.is_overidable: - self._is_overriden = True - # TODO update items - if item is not None: - is_overriden = self.is_overriden - for _item in self.input_fields: - if _item is not item: - _item.update_style(is_overriden) - - self.value_changed.emit(self) - - def apply_overrides(self, override_value): - self._is_overriden = False - for item in self.input_fields: - if override_value is None: - child_value = None - else: - child_value = override_value.get(item.key) - item.apply_overrides(child_value) - - self._is_overriden = ( - self.is_group - and self.is_overidable - and ( - override_value is not None - or self.child_overriden - ) - ) - self.update_style() - - def override_values(self): - if not self.is_overriden and not self.child_overriden: - return - - value = {} - for item in self.input_fields: - if hasattr(item, "override_values"): - print("*** HAVE `override_values`", item) - print(item.override_values()) - else: - print("*** missing `override_values`", item) - - if not value: - return - - output = { - "is_group": self.is_group, - "value": value - } - return output - - -class DictFormWidget(QtWidgets.QWidget): - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, label_widget=None - ): - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - self.any_parent_is_group = any_parent_is_group - - self.is_modified = False - self.is_overriden = False - self.is_group = False - - super(DictFormWidget, self).__init__(parent) - - self.input_fields = {} - self.content_layout = QtWidgets.QFormLayout(self) - - self.keys = list(parent_keys) - - for child_data in input_data.get("children", []): - self.add_children_gui(child_data, values) - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - self.value_changed.emit(self) - - def item_value(self): - output = {} - for input_field in self.input_fields.values(): - # TODO maybe merge instead of update should be used - # NOTE merge is custom function which merges 2 dicts - output.update(input_field.config_value()) - return output - - @property - def child_modified(self): - for input_field in self.input_fields.values(): - if input_field.child_modified: - return True - return False - - @property - def child_overriden(self): - for input_field in self.input_fields.values(): - if input_field.child_overriden: - return True - return False - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def config_value(self): - return self.item_value() - - def add_children_gui(self, child_configuration, values): - item_type = child_configuration["type"] - key = child_configuration["key"] - # Pop label to not be set in child - label = child_configuration["label"] - - klass = TypeToKlass.types.get(item_type) - - label_widget = QtWidgets.QLabel(label) - - item = klass( - child_configuration, values, self.keys, self, label_widget - ) - item.value_changed.connect(self._on_value_change) - self.content_layout.addRow(label_widget, item) - self.input_fields[key] = item - return item - - -class ModifiableDictItem(QtWidgets.QWidget, PypeConfigurationWidget): - _btn_size = 20 - value_changed = QtCore.Signal(object) - - def __init__(self, object_type, parent): - self._parent = parent - - super(ModifiableDictItem, self).__init__(parent) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(3) - - ItemKlass = TypeToKlass.types[object_type] - - self.key_input = QtWidgets.QLineEdit() - self.key_input.setObjectName("DictKey") - - self.value_input = ItemKlass( - {}, - AS_WIDGET, - [], - self, - None - ) - self.add_btn = QtWidgets.QPushButton("+") - self.remove_btn = QtWidgets.QPushButton("-") - - self.add_btn.setProperty("btn-type", "text-list") - self.remove_btn.setProperty("btn-type", "text-list") - - layout.addWidget(self.key_input, 0) - layout.addWidget(self.value_input, 1) - layout.addWidget(self.add_btn, 0) - layout.addWidget(self.remove_btn, 0) - - self.add_btn.setFixedSize(self._btn_size, self._btn_size) - self.remove_btn.setFixedSize(self._btn_size, self._btn_size) - self.add_btn.clicked.connect(self.on_add_clicked) - self.remove_btn.clicked.connect(self.on_remove_clicked) - - self.key_input.textChanged.connect(self._on_value_change) - self.value_input.value_changed.connect(self._on_value_change) - - self.default_key = self._key() - self.default_value = self.value_input.item_value() - - self.override_key = None - self.override_value = None - - self.is_single = False - - def _key(self): - return self.key_input.text() - - def _on_value_change(self, item=None): - self.update_style() - self.value_changed.emit(self) - - @property - def is_group(self): - return self._parent.is_group - - @property - def any_parent_is_group(self): - return self._parent.any_parent_is_group - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._parent.is_overriden - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def is_key_modified(self): - return self._key() != self.default_key - - def is_value_modified(self): - return self.value_input.is_modified - - @property - def is_modified(self): - return self.is_value_modified() or self.is_key_modified() - - def update_style(self): - if self.is_key_modified(): - state = "modified" - else: - state = "" - - self.key_input.setProperty("state", state) - self.key_input.style().polish(self.key_input) - - def row(self): - return self.parent().input_fields.index(self) - - def on_add_clicked(self): - self.parent().add_row(row=self.row() + 1) - - def on_remove_clicked(self): - if self.is_single: - self.value_input.clear_value() - self.key_input.setText("") - else: - self.parent().remove_row(self) - - def config_value(self): - key = self.key_input.text() - value = self.value_input.item_value() - if not key: - return {} - return {key: value} - - -class ModifiableDictSubWidget(QtWidgets.QWidget, PypeConfigurationWidget): - value_changed = QtCore.Signal(object) - - def __init__(self, input_data, values, parent_keys, parent): - self._parent = parent - - super(ModifiableDictSubWidget, self).__init__(parent) - self.setObjectName("ModifiableDictSubWidget") - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(5, 5, 5, 5) - layout.setSpacing(5) - self.setLayout(layout) - - self.input_fields = [] - self.object_type = input_data["object_type"] - - self.key = input_data["key"] - keys = list(parent_keys) - keys.append(self.key) - self.keys = keys - - value = self.value_from_values(values) - if value is not NOT_SET: - for item_key, item_value in value.items(): - self.add_row(key=item_key, value=item_value) - - if self.count() == 0: - self.add_row() - - self.default_value = self.config_value() - self.override_value = None - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._parent.is_overriden - - @property - def is_group(self): - return self._parent.is_group - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - @property - def any_parent_is_group(self): - return self._parent.any_parent_is_group - - def _on_value_change(self, item=None): - self.value_changed.emit(self) - - def count(self): - return len(self.input_fields) - - def add_row(self, row=None, key=None, value=None): - # Create new item - item_widget = ModifiableDictItem(self.object_type, self) - - # Set/unset if new item is single item - current_count = self.count() - if current_count == 0: - item_widget.is_single = True - elif current_count == 1: - for _input_field in self.input_fields: - _input_field.is_single = False - - item_widget.value_changed.connect(self._on_value_change) - - if row is None: - self.layout().addWidget(item_widget) - self.input_fields.append(item_widget) - else: - self.layout().insertWidget(row, item_widget) - self.input_fields.insert(row, item_widget) - - # Set value if entered value is not None - # else (when add button clicked) trigger `_on_value_change` - if value is not None and key is not None: - item_widget.default_key = key - item_widget.key_input.setText(key) - item_widget.value_input.set_value(value, default_value=True) - else: - self._on_value_change() - self.parent().updateGeometry() - - def remove_row(self, item_widget): - item_widget.value_changed.disconnect() - - self.layout().removeWidget(item_widget) - self.input_fields.remove(item_widget) - item_widget.setParent(None) - item_widget.deleteLater() - - current_count = self.count() - if current_count == 0: - self.add_row() - elif current_count == 1: - for _input_field in self.input_fields: - _input_field.is_single = True - - self._on_value_change() - self.parent().updateGeometry() - - def config_value(self): - output = {} - for item in self.input_fields: - item_value = item.config_value() - if item_value: - output.update(item_value) - return output - - -class ModifiableDict(ExpandingWidget, PypeConfigurationWidget): - # Should be used only for dictionary with one datatype as value - # TODO this is actually input field (do not care if is group or not) - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, values, parent_keys, parent, - label_widget=None - ): - self._parent = parent - - any_parent_is_group = parent.is_group - if not any_parent_is_group: - any_parent_is_group = parent.any_parent_is_group - - is_group = input_data.get("is_group", False) - if is_group and any_parent_is_group: - raise SchemeGroupHierarchyBug() - - if not any_parent_is_group and not is_group: - is_group = True - - self.any_parent_is_group = any_parent_is_group - - self.is_group = is_group - self._is_modified = False - self._is_overriden = False - self._was_overriden = False - self._state = None - - super(ModifiableDict, self).__init__(input_data["label"], parent) - self.setObjectName("ModifiableDict") - - self.value_widget = ModifiableDictSubWidget( - input_data, values, parent_keys, self - ) - self.value_widget.setAttribute(QtCore.Qt.WA_StyledBackground) - self.value_widget.value_changed.connect(self._on_value_change) - - self.set_content_widget(self.value_widget) - - self.key = input_data["key"] - - self.default_value = self.item_value() - self.override_value = None - - def _on_value_change(self, item=None): - if self.ignore_value_changes: - return - - if self.is_overidable: - self._is_overriden = True - - if self.is_overriden: - self._is_modified = self.item_value() != self.override_value - else: - self._is_modified = self.item_value() != self.default_value - - self.value_changed.emit(self) - - self.update_style() - - @property - def child_modified(self): - return self.is_modified - - @property - def is_modified(self): - return self._is_modified - - @property - def child_overriden(self): - return self._is_overriden - - @property - def is_overidable(self): - return self._parent.is_overidable - - @property - def is_overriden(self): - return self._is_overriden or self._parent.is_overriden - - @property - def is_modified(self): - return self._is_modified - - @property - def ignore_value_changes(self): - return self._parent.ignore_value_changes - - def apply_overrides(self, override_value): - self._state = None - self._is_modified = False - self.override_value = override_value - if override_value is None: - self._is_overriden = False - self._was_overriden = False - value = self.default_value - else: - self._is_overriden = True - self._was_overriden = True - value = override_value - - self.set_value(value) - self.update_style() - - def update_style(self): - state = self.style_state(self.is_overriden, self.is_modified) - if self._state == state: - return - - if state: - child_state = "child-{}".format(state) - else: - child_state = "" - - self.setProperty("state", child_state) - self.style().polish(self) - - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) - - self._state = state - - def item_value(self): - return self.value_widget.config_value() - - def config_value(self): - return {self.key: self.item_value()} - - def override_value(self): - return - - -TypeToKlass.types["boolean"] = BooleanWidget -TypeToKlass.types["text-singleline"] = TextSingleLineWidget -TypeToKlass.types["text-multiline"] = TextMultiLineWidget -TypeToKlass.types["raw-json"] = RawJsonWidget -TypeToKlass.types["int"] = IntegerWidget -TypeToKlass.types["float"] = FloatWidget -TypeToKlass.types["dict-expanding"] = DictExpandWidget -TypeToKlass.types["dict-form"] = DictFormWidget -TypeToKlass.types["dict-invisible"] = DictInvisible -TypeToKlass.types["dict-modifiable"] = ModifiableDict -TypeToKlass.types["list-text"] = TextListWidget From cdda1babc33e47b3b69877e1e1fb1926680e2325 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 00:14:16 +0200 Subject: [PATCH 072/158] removed buttons from svg --- .../standalonepublish/widgets/__init__.py | 1 - .../widgets/button_from_svgs.py | 113 ------------------ 2 files changed, 114 deletions(-) delete mode 100644 pype/tools/standalonepublish/widgets/button_from_svgs.py diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 0db3643cf3..11da0aa1c9 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -9,7 +9,6 @@ PluginRole = QtCore.Qt.UserRole + 5 PluginKeyRole = QtCore.Qt.UserRole + 6 from pype.resources import get_resource -from .button_from_svgs import SvgResizable, SvgButton from .model_node import Node from .model_tree import TreeModel diff --git a/pype/tools/standalonepublish/widgets/button_from_svgs.py b/pype/tools/standalonepublish/widgets/button_from_svgs.py deleted file mode 100644 index 4255c5f29b..0000000000 --- a/pype/tools/standalonepublish/widgets/button_from_svgs.py +++ /dev/null @@ -1,113 +0,0 @@ -from xml.dom import minidom - -from . import QtGui, QtCore, QtWidgets -from PyQt5 import QtSvg, QtXml - - -class SvgResizable(QtSvg.QSvgWidget): - clicked = QtCore.Signal() - - def __init__(self, filepath, width=None, height=None, fill=None): - super().__init__() - self.xmldoc = minidom.parse(filepath) - itemlist = self.xmldoc.getElementsByTagName('svg') - for element in itemlist: - if fill: - element.setAttribute('fill', str(fill)) - # TODO auto scale if only one is set - if width is not None and height is not None: - self.setMaximumSize(width, height) - self.setMinimumSize(width, height) - xml_string = self.xmldoc.toxml() - svg_bytes = bytearray(xml_string, encoding='utf-8') - - self.load(svg_bytes) - - def change_color(self, color): - element = self.xmldoc.getElementsByTagName('svg')[0] - element.setAttribute('fill', str(color)) - xml_string = self.xmldoc.toxml() - svg_bytes = bytearray(xml_string, encoding='utf-8') - self.load(svg_bytes) - - def mousePressEvent(self, event): - self.clicked.emit() - - -class SvgButton(QtWidgets.QFrame): - clicked = QtCore.Signal() - def __init__( - self, filepath, width=None, height=None, fills=[], - parent=None, checkable=True - ): - super().__init__(parent) - self.checkable = checkable - self.checked = False - - xmldoc = minidom.parse(filepath) - element = xmldoc.getElementsByTagName('svg')[0] - c_actual = '#777777' - if element.hasAttribute('fill'): - c_actual = element.getAttribute('fill') - self.store_fills(fills, c_actual) - - self.installEventFilter(self) - self.svg_widget = SvgResizable(filepath, width, height, self.c_normal) - xmldoc = minidom.parse(filepath) - - layout = QtWidgets.QHBoxLayout(self) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.svg_widget) - - if width is not None and height is not None: - self.setMaximumSize(width, height) - self.setMinimumSize(width, height) - - def store_fills(self, fills, actual): - if len(fills) == 0: - fills = [actual, actual, actual, actual] - elif len(fills) == 1: - fills = [fills[0], fills[0], fills[0], fills[0]] - elif len(fills) == 2: - fills = [fills[0], fills[1], fills[1], fills[1]] - elif len(fills) == 3: - fills = [fills[0], fills[1], fills[2], fills[2]] - self.c_normal = fills[0] - self.c_hover = fills[1] - self.c_active = fills[2] - self.c_active_hover = fills[3] - - def eventFilter(self, object, event): - if event.type() == QtCore.QEvent.Enter: - self.hoverEnterEvent(event) - return True - elif event.type() == QtCore.QEvent.Leave: - self.hoverLeaveEvent(event) - return True - elif event.type() == QtCore.QEvent.MouseButtonRelease: - self.mousePressEvent(event) - return False - - def change_checked(self, hover=True): - if self.checkable: - self.checked = not self.checked - if hover: - self.hoverEnterEvent() - else: - self.hoverLeaveEvent() - - def hoverEnterEvent(self, event=None): - color = self.c_hover - if self.checked: - color = self.c_active_hover - self.svg_widget.change_color(color) - - def hoverLeaveEvent(self, event=None): - color = self.c_normal - if self.checked: - color = self.c_active - self.svg_widget.change_color(color) - - def mousePressEvent(self, event=None): - self.clicked.emit() From 11032200c595ffdfb9bf11f2c301bd2f062dded2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 00:16:51 +0200 Subject: [PATCH 073/158] cleaned imports --- .../tools/standalonepublish/widgets/__init__.py | 6 +----- .../standalonepublish/widgets/model_asset.py | 5 +++-- .../widgets/model_filter_proxy_exact_match.py | 2 +- .../model_filter_proxy_recursive_sort.py | 2 +- .../widgets/model_tasks_template.py | 7 ++++--- .../standalonepublish/widgets/model_tree.py | 2 +- .../widgets/model_tree_view_deselectable.py | 2 +- .../standalonepublish/widgets/widget_asset.py | 5 +++-- .../widgets/widget_component_item.py | 4 ++-- .../widgets/widget_components.py | 1 - .../widgets/widget_components_list.py | 2 +- .../widgets/widget_drop_empty.py | 17 ++++++++--------- .../widgets/widget_drop_frame.py | 3 +-- .../standalonepublish/widgets/widget_family.py | 6 +----- .../widgets/widget_family_desc.py | 12 +++--------- .../standalonepublish/widgets/widget_shadow.py | 8 +++++--- 16 files changed, 36 insertions(+), 48 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/__init__.py b/pype/tools/standalonepublish/widgets/__init__.py index 11da0aa1c9..e61897f807 100644 --- a/pype/tools/standalonepublish/widgets/__init__.py +++ b/pype/tools/standalonepublish/widgets/__init__.py @@ -1,6 +1,4 @@ -from avalon.vendor.Qt import * -from avalon.vendor import qtawesome -from avalon import style +from Qt import QtCore HelpRole = QtCore.Qt.UserRole + 2 FamilyRole = QtCore.Qt.UserRole + 3 @@ -8,8 +6,6 @@ ExistsRole = QtCore.Qt.UserRole + 4 PluginRole = QtCore.Qt.UserRole + 5 PluginKeyRole = QtCore.Qt.UserRole + 6 -from pype.resources import get_resource - from .model_node import Node from .model_tree import TreeModel from .model_asset import AssetModel, _iter_model_rows diff --git a/pype/tools/standalonepublish/widgets/model_asset.py b/pype/tools/standalonepublish/widgets/model_asset.py index 6bea35ebd7..44649b3dc3 100644 --- a/pype/tools/standalonepublish/widgets/model_asset.py +++ b/pype/tools/standalonepublish/widgets/model_asset.py @@ -1,8 +1,9 @@ import logging import collections -from . import QtCore, QtGui +from Qt import QtCore, QtGui from . import TreeModel, Node -from . import style, qtawesome +from avalon.vendor import qtawesome +from avalon import style log = logging.getLogger(__name__) diff --git a/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py b/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py index 862e4071db..604ae30934 100644 --- a/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py +++ b/pype/tools/standalonepublish/widgets/model_filter_proxy_exact_match.py @@ -1,4 +1,4 @@ -from . import QtCore +from Qt import QtCore class ExactMatchesFilterProxyModel(QtCore.QSortFilterProxyModel): diff --git a/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py b/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py index 9528e96ebf..71ecdf41dc 100644 --- a/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py +++ b/pype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py @@ -1,4 +1,4 @@ -from . import QtCore +from Qt import QtCore import re diff --git a/pype/tools/standalonepublish/widgets/model_tasks_template.py b/pype/tools/standalonepublish/widgets/model_tasks_template.py index 336921b37a..476f45391d 100644 --- a/pype/tools/standalonepublish/widgets/model_tasks_template.py +++ b/pype/tools/standalonepublish/widgets/model_tasks_template.py @@ -1,6 +1,7 @@ -from . import QtCore, TreeModel -from . import Node -from . import qtawesome, style +from Qt import QtCore +from . import Node, TreeModel +from avalon.vendor import qtawesome +from avalon import style class TasksTemplateModel(TreeModel): diff --git a/pype/tools/standalonepublish/widgets/model_tree.py b/pype/tools/standalonepublish/widgets/model_tree.py index f37b7a00b2..efac0d6b78 100644 --- a/pype/tools/standalonepublish/widgets/model_tree.py +++ b/pype/tools/standalonepublish/widgets/model_tree.py @@ -1,4 +1,4 @@ -from . import QtCore +from Qt import QtCore from . import Node diff --git a/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py b/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py index 78bec44d36..6a15916981 100644 --- a/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py +++ b/pype/tools/standalonepublish/widgets/model_tree_view_deselectable.py @@ -1,4 +1,4 @@ -from . import QtWidgets, QtCore +from Qt import QtWidgets, QtCore class DeselectableTreeView(QtWidgets.QTreeView): diff --git a/pype/tools/standalonepublish/widgets/widget_asset.py b/pype/tools/standalonepublish/widgets/widget_asset.py index d9241bd91f..c468c9627b 100644 --- a/pype/tools/standalonepublish/widgets/widget_asset.py +++ b/pype/tools/standalonepublish/widgets/widget_asset.py @@ -1,7 +1,8 @@ import contextlib -from . import QtWidgets, QtCore +from Qt import QtWidgets, QtCore from . import RecursiveSortFilterProxyModel, AssetModel -from . import qtawesome, style +from avalon.vendor import qtawesome +from avalon import style from . import TasksTemplateModel, DeselectableTreeView from . import _iter_model_rows diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index 40298520b1..dd838075e3 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -1,6 +1,6 @@ import os -from . import QtCore, QtGui, QtWidgets -from . import get_resource +from Qt import QtCore, QtGui, QtWidgets +from pype.resources import get_resource from avalon import style diff --git a/pype/tools/standalonepublish/widgets/widget_components.py b/pype/tools/standalonepublish/widgets/widget_components.py index 3b6c326af0..7e0327f00a 100644 --- a/pype/tools/standalonepublish/widgets/widget_components.py +++ b/pype/tools/standalonepublish/widgets/widget_components.py @@ -7,7 +7,6 @@ import string from Qt import QtWidgets, QtCore from . import DropDataFrame -from avalon.tools import publish from avalon import io from pype.api import execute, Logger diff --git a/pype/tools/standalonepublish/widgets/widget_components_list.py b/pype/tools/standalonepublish/widgets/widget_components_list.py index f85e9f0aa6..4e502a2e5f 100644 --- a/pype/tools/standalonepublish/widgets/widget_components_list.py +++ b/pype/tools/standalonepublish/widgets/widget_components_list.py @@ -1,4 +1,4 @@ -from . import QtCore, QtGui, QtWidgets +from Qt import QtWidgets class ComponentsList(QtWidgets.QTableWidget): diff --git a/pype/tools/standalonepublish/widgets/widget_drop_empty.py b/pype/tools/standalonepublish/widgets/widget_drop_empty.py index d352e70355..ed526f2a78 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_empty.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_empty.py @@ -1,7 +1,4 @@ -import os -import logging -import clique -from . import QtWidgets, QtCore, QtGui +from Qt import QtWidgets, QtCore, QtGui class DropEmpty(QtWidgets.QWidget): @@ -42,11 +39,13 @@ class DropEmpty(QtWidgets.QWidget): super().paintEvent(event) painter = QtGui.QPainter(self) pen = QtGui.QPen() - pen.setWidth(1); - pen.setBrush(QtCore.Qt.darkGray); - pen.setStyle(QtCore.Qt.DashLine); + pen.setWidth(1) + pen.setBrush(QtCore.Qt.darkGray) + pen.setStyle(QtCore.Qt.DashLine) painter.setPen(pen) painter.drawRect( - 10, 10, - self.rect().width()-15, self.rect().height()-15 + 10, + 10, + self.rect().width() - 15, + self.rect().height() - 15 ) diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py index 37d22cf887..e13f701b30 100644 --- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -3,9 +3,8 @@ import re import json import clique import subprocess -from pype.api import config import pype.lib -from . import QtWidgets, QtCore +from Qt import QtWidgets, QtCore from . import DropEmpty, ComponentsList, ComponentItem diff --git a/pype/tools/standalonepublish/widgets/widget_family.py b/pype/tools/standalonepublish/widgets/widget_family.py index 29a0812a91..1c8f2238fc 100644 --- a/pype/tools/standalonepublish/widgets/widget_family.py +++ b/pype/tools/standalonepublish/widgets/widget_family.py @@ -1,10 +1,6 @@ -import os -import sys -import inspect -import json from collections import namedtuple -from . import QtWidgets, QtCore +from Qt import QtWidgets, QtCore from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole from . import FamilyDescriptionWidget diff --git a/pype/tools/standalonepublish/widgets/widget_family_desc.py b/pype/tools/standalonepublish/widgets/widget_family_desc.py index 7c80dcfd57..8c95ddf2e4 100644 --- a/pype/tools/standalonepublish/widgets/widget_family_desc.py +++ b/pype/tools/standalonepublish/widgets/widget_family_desc.py @@ -1,13 +1,7 @@ -import os -import sys -import inspect -import json - -from . import QtWidgets, QtCore, QtGui -from . import HelpRole, FamilyRole, ExistsRole, PluginRole -from . import qtawesome +from Qt import QtWidgets, QtCore, QtGui +from . import FamilyRole, PluginRole +from avalon.vendor import qtawesome import six -from pype import lib as pypelib class FamilyDescriptionWidget(QtWidgets.QWidget): diff --git a/pype/tools/standalonepublish/widgets/widget_shadow.py b/pype/tools/standalonepublish/widgets/widget_shadow.py index 1bb9cee44b..de5fdf6be0 100644 --- a/pype/tools/standalonepublish/widgets/widget_shadow.py +++ b/pype/tools/standalonepublish/widgets/widget_shadow.py @@ -1,4 +1,4 @@ -from . import QtWidgets, QtCore, QtGui +from Qt import QtWidgets, QtCore, QtGui class ShadowWidget(QtWidgets.QWidget): @@ -26,7 +26,9 @@ class ShadowWidget(QtWidgets.QWidget): painter.begin(self) painter.setFont(self.font) painter.setRenderHint(QtGui.QPainter.Antialiasing) - painter.fillRect(event.rect(), QtGui.QBrush(QtGui.QColor(0, 0, 0, 127))) + painter.fillRect( + event.rect(), QtGui.QBrush(QtGui.QColor(0, 0, 0, 127)) + ) painter.drawText( QtCore.QRectF( 0.0, @@ -34,7 +36,7 @@ class ShadowWidget(QtWidgets.QWidget): self.parent_widget.frameGeometry().width(), self.parent_widget.frameGeometry().height() ), - QtCore.Qt.AlignCenter|QtCore.Qt.AlignCenter, + QtCore.Qt.AlignCenter | QtCore.Qt.AlignCenter, self.message ) painter.end() From f4a14bbc109923852768f46d61a73b1575d597a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 00:17:50 +0200 Subject: [PATCH 074/158] one more imports cleanup --- pype/tools/standalonepublish/__main__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pype/tools/standalonepublish/__main__.py b/pype/tools/standalonepublish/__main__.py index 5bcf514994..aba8e6c0a4 100644 --- a/pype/tools/standalonepublish/__main__.py +++ b/pype/tools/standalonepublish/__main__.py @@ -4,8 +4,6 @@ import app import signal from Qt import QtWidgets from avalon import style -import pype -import pyblish.api if __name__ == "__main__": From 62160ab368237ba013cb35052ddb95a242f2aed8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 00:37:09 +0200 Subject: [PATCH 075/158] replaced api.Session with io.Session --- pype/tools/launcher/app.py | 10 +++++----- pype/tools/launcher/models.py | 1 - pype/tools/launcher/widgets.py | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index 78c5406fa8..ef00880585 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -2,7 +2,7 @@ import sys import copy from avalon.vendor.Qt import QtWidgets, QtCore, QtGui -from avalon import io, api, style +from avalon import io, style from avalon.tools import lib as tools_lib from avalon.tools.widgets import AssetWidget @@ -197,7 +197,7 @@ class AssetsPanel(QtWidgets.QWidget): def on_project_changed(self): project_name = self.project_bar.get_current_project() - api.Session["AVALON_PROJECT"] = project_name + io.Session["AVALON_PROJECT"] = project_name self.assets_widget.refresh() # Force asset change callback to ensure tasks are correctly reset @@ -220,7 +220,7 @@ class AssetsPanel(QtWidgets.QWidget): def get_current_session(self): asset_doc = self.assets_widget.get_active_asset_document() - session = copy.deepcopy(api.Session) + session = copy.deepcopy(io.Session) # Clear some values that we are about to collect if available session.pop("AVALON_SILO", None) @@ -407,7 +407,7 @@ class Window(QtWidgets.QDialog): # Assets page return self.asset_panel.get_current_session() - session = copy.deepcopy(api.Session) + session = copy.deepcopy(io.Session) # Remove some potential invalid session values # that we know are not set when not browsing in @@ -664,7 +664,7 @@ def cli(args): actions.register_environment_actions() io.install() - #api.Session["AVALON_PROJECT"] = project + #io.Session["AVALON_PROJECT"] = project import traceback sys.excepthook = lambda typ, val, tb: traceback.print_last() diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index ce6e0c722e..b78bff950b 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -1,4 +1,3 @@ -import os import copy import logging import collections diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index a264466dbc..9bcd9000d1 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -1,7 +1,7 @@ import copy from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome -from avalon import api +from avalon import io from .delegates import ActionDelegate from .models import TaskModel, ActionModel, ProjectModel @@ -37,7 +37,7 @@ class ProjectBar(QtWidgets.QWidget): self.project_combobox.currentIndexChanged.connect(self.project_changed) # Set current project by default if it's set. - project_name = api.Session.get("AVALON_PROJECT") + project_name = io.Session.get("AVALON_PROJECT") if project_name: self.set_project(project_name) From 2bae09f4e024936017444fe004f32a5fac7f294a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 01:10:25 +0200 Subject: [PATCH 076/158] using custom dbconnector instead of avalon.io --- pype/tools/launcher/app.py | 48 ++++++++++++++++++++-------------- pype/tools/launcher/lib.py | 8 +----- pype/tools/launcher/models.py | 28 +++++++++++++------- pype/tools/launcher/widgets.py | 25 +++++++++++------- 4 files changed, 63 insertions(+), 46 deletions(-) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index ef00880585..6f554110dc 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -2,7 +2,9 @@ import sys import copy from avalon.vendor.Qt import QtWidgets, QtCore, QtGui -from avalon import io, style +from avalon import style + +from pype.modules.ftrack.lib.io_nonsingleton import DbConnector from avalon.tools import lib as tools_lib from avalon.tools.widgets import AssetWidget @@ -86,17 +88,19 @@ class ProjectsPanel(QtWidgets.QWidget): project_clicked = QtCore.Signal(str) - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(ProjectsPanel, self).__init__(parent=parent) layout = QtWidgets.QVBoxLayout(self) - io.install() + self.dbcon = dbcon + self.dbcon.install() + view = IconListView(parent=self) view.setSelectionMode(QtWidgets.QListView.NoSelection) flick = FlickCharm(parent=self) flick.activateOn(view) - model = ProjectModel() + model = ProjectModel(self.dbcon) model.hide_invisible = True model.refresh() view.setModel(model) @@ -118,31 +122,35 @@ class AssetsPanel(QtWidgets.QWidget): """Assets page""" back_clicked = QtCore.Signal() - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(AssetsPanel, self).__init__(parent=parent) + self.dbcon = dbcon + # project bar - project_bar_widget = QtWidgets.QWidget() + project_bar_widget = QtWidgets.QWidget(self) layout = QtWidgets.QHBoxLayout(project_bar_widget) layout.setSpacing(4) btn_back_icon = qtawesome.icon("fa.angle-left", color="white") - btn_back = QtWidgets.QPushButton() + btn_back = QtWidgets.QPushButton(project_bar_widget) btn_back.setIcon(btn_back_icon) btn_back.setFixedWidth(23) btn_back.setFixedHeight(23) - project_bar = ProjectBar() + project_bar = ProjectBar(self.dbcon, project_bar_widget) layout.addWidget(btn_back) layout.addWidget(project_bar) # assets - assets_proxy_widgets = QtWidgets.QWidget() + assets_proxy_widgets = QtWidgets.QWidget(self) assets_proxy_widgets.setContentsMargins(0, 0, 0, 0) assets_layout = QtWidgets.QVBoxLayout(assets_proxy_widgets) - assets_widget = AssetWidget() + assets_widget = AssetWidget( + dbcon=self.dbcon, parent=assets_proxy_widgets + ) # Make assets view flickable flick = FlickCharm(parent=self) @@ -153,7 +161,7 @@ class AssetsPanel(QtWidgets.QWidget): assets_layout.addWidget(assets_widget) # tasks - tasks_widget = TasksWidget() + tasks_widget = TasksWidget(self.dbcon, self) body = QtWidgets.QSplitter() body.setContentsMargins(0, 0, 0, 0) body.setSizePolicy( @@ -197,7 +205,7 @@ class AssetsPanel(QtWidgets.QWidget): def on_project_changed(self): project_name = self.project_bar.get_current_project() - io.Session["AVALON_PROJECT"] = project_name + self.dbcon.Session["AVALON_PROJECT"] = project_name self.assets_widget.refresh() # Force asset change callback to ensure tasks are correctly reset @@ -220,7 +228,7 @@ class AssetsPanel(QtWidgets.QWidget): def get_current_session(self): asset_doc = self.assets_widget.get_active_asset_document() - session = copy.deepcopy(io.Session) + session = copy.deepcopy(self.dbcon.Session) # Clear some values that we are about to collect if available session.pop("AVALON_SILO", None) @@ -242,6 +250,8 @@ class Window(QtWidgets.QDialog): def __init__(self, parent=None): super(Window, self).__init__(parent) + self.dbcon = DbConnector() + self.setWindowTitle("Launcher") self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) @@ -251,15 +261,15 @@ class Window(QtWidgets.QDialog): self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint ) - project_panel = ProjectsPanel() - asset_panel = AssetsPanel() + project_panel = ProjectsPanel(self.dbcon) + asset_panel = AssetsPanel(self.dbcon) page_slider = SlidePageWidget() page_slider.addWidget(project_panel) page_slider.addWidget(asset_panel) # actions - actions_bar = ActionBar() + actions_bar = ActionBar(self.dbcon, self) # statusbar statusbar = QtWidgets.QWidget() @@ -356,7 +366,7 @@ class Window(QtWidgets.QDialog): def on_project_changed(self): project_name = self.asset_panel.project_bar.get_current_project() - io.Session["AVALON_PROJECT"] = project_name + self.dbcon.Session["AVALON_PROJECT"] = project_name # Update the Action plug-ins available for the current project self.actions_bar.model.discover() @@ -368,7 +378,7 @@ class Window(QtWidgets.QDialog): tools_lib.schedule(self.on_refresh_actions, delay) def on_project_clicked(self, project_name): - io.Session["AVALON_PROJECT"] = project_name + self.dbcon.Session["AVALON_PROJECT"] = project_name # Refresh projects self.asset_panel.project_bar.refresh() self.asset_panel.set_project(project_name) @@ -407,7 +417,7 @@ class Window(QtWidgets.QDialog): # Assets page return self.asset_panel.get_current_session() - session = copy.deepcopy(io.Session) + session = copy.deepcopy(self.dbcon.Session) # Remove some potential invalid session values # that we know are not set when not browsing in diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index e7933e9843..0bbbb55560 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -16,7 +16,7 @@ provides a bridge between the file-based project inventory and configuration. import os from Qt import QtGui -from avalon import io, lib, pipeline +from avalon import lib, pipeline from avalon.vendor import qtawesome from pype.api import resources @@ -24,12 +24,6 @@ ICON_CACHE = {} NOT_FOUND = type("NotFound", (object, ), {}) -def list_project_tasks(): - """List the project task types available in the current project""" - project = io.find_one({"type": "project"}) - return [task["name"] for task in project["config"]["tasks"]] - - def get_application_actions(project): """Define dynamic Application classes for project using `.toml` files diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index b78bff950b..61e240c2eb 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -5,7 +5,7 @@ import collections from . import lib from Qt import QtCore, QtGui from avalon.vendor import qtawesome -from avalon import io, style, api +from avalon import style, api log = logging.getLogger(__name__) @@ -13,8 +13,10 @@ log = logging.getLogger(__name__) class TaskModel(QtGui.QStandardItemModel): """A model listing the tasks combined for a list of assets""" - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(TaskModel, self).__init__(parent=parent) + self.dbcon = dbcon + self._num_assets = 0 self.default_icon = qtawesome.icon( @@ -29,11 +31,11 @@ class TaskModel(QtGui.QStandardItemModel): self._get_task_icons() def _get_task_icons(self): - if io.Session.get("AVALON_PROJECT") is None: + if not self.dbcon.Session.get("AVALON_PROJECT"): return # Get the project configured icons from database - project = io.find_one({"type": "project"}) + project = self.dbcon.find_one({"type": "project"}) for task in project["config"].get("tasks") or []: icon_name = task.get("icon") if icon_name: @@ -52,7 +54,7 @@ class TaskModel(QtGui.QStandardItemModel): if asset_docs is None and asset_ids is not None: # find assets in db by query - asset_docs = list(io.find({ + asset_docs = list(self.dbcon.find({ "type": "asset", "_id": {"$in": asset_ids} })) @@ -108,8 +110,10 @@ class ActionModel(QtGui.QStandardItemModel): ACTION_ROLE = QtCore.Qt.UserRole GROUP_ROLE = QtCore.Qt.UserRole + 1 - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(ActionModel, self).__init__(parent=parent) + self.dbcon = dbcon + self._session = {} self._groups = {} self.default_icon = qtawesome.icon("fa.cube", color="white") @@ -120,7 +124,7 @@ class ActionModel(QtGui.QStandardItemModel): def discover(self): """Set up Actions cache. Run this for each new project.""" - if not io.Session.get("AVALON_PROJECT"): + if not self.dbcon.Session.get("AVALON_PROJECT"): self._registered_actions = list() return @@ -128,7 +132,7 @@ class ActionModel(QtGui.QStandardItemModel): actions = api.discover(api.Action) # Get available project actions and the application actions - project_doc = io.find_one({"type": "project"}) + project_doc = self.dbcon.find_one({"type": "project"}) app_actions = lib.get_application_actions(project_doc) actions.extend(app_actions) @@ -233,9 +237,11 @@ class ActionModel(QtGui.QStandardItemModel): class ProjectModel(QtGui.QStandardItemModel): """List of projects""" - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(ProjectModel, self).__init__(parent=parent) + self.dbcon = dbcon + self.hide_invisible = False self.project_icon = qtawesome.icon("fa.map", color="white") @@ -251,7 +257,9 @@ class ProjectModel(QtGui.QStandardItemModel): def get_projects(self): project_docs = [] - for project_doc in sorted(io.projects(), key=lambda x: x["name"]): + for project_doc in sorted( + self.dbcon.projects(), key=lambda x: x["name"] + ): if ( self.hide_invisible and not project_doc["data"].get("visible", True) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 9bcd9000d1..4fc7d166cb 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -1,7 +1,6 @@ import copy from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome -from avalon import io from .delegates import ActionDelegate from .models import TaskModel, ActionModel, ProjectModel @@ -11,10 +10,12 @@ from .flickcharm import FlickCharm class ProjectBar(QtWidgets.QWidget): project_changed = QtCore.Signal(int) - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(ProjectBar, self).__init__(parent) - self.model = ProjectModel() + self.dbcon = dbcon + + self.model = ProjectModel(self.dbcon) self.model.hide_invisible = True self.project_combobox = QtWidgets.QComboBox() @@ -37,7 +38,7 @@ class ProjectBar(QtWidgets.QWidget): self.project_combobox.currentIndexChanged.connect(self.project_changed) # Set current project by default if it's set. - project_name = io.Session.get("AVALON_PROJECT") + project_name = self.dbcon.Session.get("AVALON_PROJECT") if project_name: self.set_project(project_name) @@ -68,9 +69,11 @@ class ActionBar(QtWidgets.QWidget): action_clicked = QtCore.Signal(object) - def __init__(self, parent=None): + def __init__(self, dbcon, parent=None): super(ActionBar, self).__init__(parent) + self.dbcon = dbcon + layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(8, 0, 8, 0) @@ -86,7 +89,7 @@ class ActionBar(QtWidgets.QWidget): view.setSpacing(0) view.setWordWrap(True) - model = ActionModel(self) + model = ActionModel(self.dbcon, self) view.setModel(model) delegate = ActionDelegate(model.GROUP_ROLE, self) @@ -140,12 +143,14 @@ class TasksWidget(QtWidgets.QWidget): QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows ) - def __init__(self): - super(TasksWidget, self).__init__() + def __init__(self, dbcon, parent=None): + super(TasksWidget, self).__init__(parent) - view = QtWidgets.QTreeView() + self.dbcon = dbcon + + view = QtWidgets.QTreeView(self) view.setIndentation(0) - model = TaskModel() + model = TaskModel(self.dbcon) view.setModel(model) layout = QtWidgets.QVBoxLayout(self) From 7f7b220d32a689643c341c9a9ea17308e9936c62 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 01:39:18 +0200 Subject: [PATCH 077/158] moved app launching to pype.lib --- pype/lib.py | 176 ++++++++++++++- pype/modules/ftrack/lib/ftrack_app_handler.py | 212 ++---------------- 2 files changed, 188 insertions(+), 200 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index f99cd73e09..ff0c0c0e82 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -7,15 +7,19 @@ import json import collections import logging import itertools +import copy import contextlib import subprocess +import getpass import inspect +import acre +import platform from abc import ABCMeta, abstractmethod from avalon import io, pipeline import six import avalon.api -from .api import config +from .api import config, Anatomy log = logging.getLogger(__name__) @@ -1416,3 +1420,173 @@ def get_latest_version(asset_name, subset_name): assert version, "No version found, this is a bug" return version + + +class ApplicationLaunchFailed(Exception): + pass + + +def launch_application(project_name, asset_name, task_name, app_name): + database = get_avalon_database() + project_document = database[project_name].find_one({"type": "project"}) + asset_document = database[project_name].find_one({ + "type": "asset", + "name": asset_name + }) + + asset_doc_parents = asset_document["data"].get("parents") + hierarchy = "/".join(asset_doc_parents) + + app_def = avalon.lib.get_application(app_name) + app_label = app_def.get("ftrack_label", app_def.get("label", app_name)) + + host_name = app_def["application_dir"] + data = { + "project": { + "name": project_document["name"], + "code": project_document["data"].get("code") + }, + "task": task_name, + "asset": asset_name, + "app": host_name, + "hierarchy": hierarchy + } + + try: + anatomy = Anatomy(project_name) + anatomy_filled = anatomy.format(data) + workdir = os.path.normpath(anatomy_filled["work"]["folder"]) + + except Exception as exc: + raise ApplicationLaunchFailed( + "Error in anatomy.format: {}".format(str(exc)) + ) + + try: + os.makedirs(workdir) + except FileExistsError: + pass + + last_workfile_path = None + extensions = avalon.api.HOST_WORKFILE_EXTENSIONS.get(host_name) + if extensions: + # Find last workfile + file_template = anatomy.templates["work"]["file"] + data.update({ + "version": 1, + "user": os.environ.get("PYPE_USERNAME") or getpass.getuser(), + "ext": extensions[0] + }) + + last_workfile_path = avalon.api.last_workfile( + workdir, file_template, data, extensions, True + ) + + # set environments for Avalon + prep_env = copy.deepcopy(os.environ) + prep_env.update({ + "AVALON_PROJECT": project_name, + "AVALON_ASSET": asset_name, + "AVALON_TASK": task_name, + "AVALON_APP": host_name, + "AVALON_APP_NAME": app_name, + "AVALON_HIERARCHY": hierarchy, + "AVALON_WORKDIR": workdir + }) + + start_last_workfile = avalon.api.should_start_last_workfile( + project_name, host_name, task_name + ) + # Store boolean as "0"(False) or "1"(True) + prep_env["AVALON_OPEN_LAST_WORKFILE"] = ( + str(int(bool(start_last_workfile))) + ) + + if ( + start_last_workfile + and last_workfile_path + and os.path.exists(last_workfile_path) + ): + prep_env["AVALON_LAST_WORKFILE"] = last_workfile_path + + prep_env.update(anatomy.roots_obj.root_environments()) + + # collect all the 'environment' attributes from parents + tools_attr = [prep_env["AVALON_APP"], prep_env["AVALON_APP_NAME"]] + tools_env = asset_document["data"].get("tools_env") or [] + tools_attr.extend(tools_env) + + tools_env = acre.get_tools(tools_attr) + env = acre.compute(tools_env) + env = acre.merge(env, current_env=dict(prep_env)) + + # Get path to execute + st_temp_path = os.environ["PYPE_CONFIG"] + os_plat = platform.system().lower() + + # Path to folder with launchers + path = os.path.join(st_temp_path, "launchers", os_plat) + + # Full path to executable launcher + execfile = None + + launch_hook = app_def.get("launch_hook") + if launch_hook: + log.info("launching hook: {}".format(launch_hook)) + ret_val = execute_hook(launch_hook, env=env) + if not ret_val: + raise ApplicationLaunchFailed( + "Hook didn't finish successfully {}".format(app_label) + ) + + if sys.platform == "win32": + for ext in os.environ["PATHEXT"].split(os.pathsep): + fpath = os.path.join(path.strip('"'), app_def["executable"] + ext) + if os.path.isfile(fpath) and os.access(fpath, os.X_OK): + execfile = fpath + break + + # Run SW if was found executable + if execfile is None: + raise ApplicationLaunchFailed( + "We didn't find launcher for {}".format(app_label) + ) + + popen = avalon.lib.launch( + executable=execfile, args=[], environment=env + ) + + elif ( + sys.platform.startswith("linux") + or sys.platform.startswith("darwin") + ): + execfile = os.path.join(path.strip('"'), app_def["executable"]) + # Run SW if was found executable + if execfile is None: + raise ApplicationLaunchFailed( + "We didn't find launcher for {}".format(app_label) + ) + + if not os.path.isfile(execfile): + raise ApplicationLaunchFailed( + "Launcher doesn't exist - {}".format(execfile) + ) + + try: + fp = open(execfile) + except PermissionError as perm_exc: + raise ApplicationLaunchFailed( + "Access denied on launcher {} - {}".format(execfile, perm_exc) + ) + + fp.close() + # check executable permission + if not os.access(execfile, os.X_OK): + raise ApplicationLaunchFailed( + "No executable permission - {}".format(execfile) + ) + + popen = avalon.lib.launch( # noqa: F841 + "/usr/bin/env", args=["bash", execfile], environment=env + ) + return popen diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index 22fd6eeaab..4847464973 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -1,16 +1,6 @@ -import os -import sys -import copy -import platform -import avalon.lib -import acre -import getpass from pype import lib as pypelib -from pype.api import config, Anatomy +from pype.api import config from .ftrack_action_handler import BaseAction -from avalon.api import ( - last_workfile, HOST_WORKFILE_EXTENSIONS, should_start_last_workfile -) class AppAction(BaseAction): @@ -156,43 +146,21 @@ class AppAction(BaseAction): entity = entities[0] task_name = entity["name"] - - project_name = entity["project"]["full_name"] - - database = pypelib.get_avalon_database() - asset_name = entity["parent"]["name"] - asset_document = database[project_name].find_one({ - "type": "asset", - "name": asset_name - }) - - hierarchy = "" - asset_doc_parents = asset_document["data"].get("parents") - if asset_doc_parents: - hierarchy = os.path.join(*asset_doc_parents) - - application = avalon.lib.get_application(self.identifier) - host_name = application["application_dir"] - data = { - "project": { - "name": entity["project"]["full_name"], - "code": entity["project"]["name"] - }, - "task": task_name, - "asset": asset_name, - "app": host_name, - "hierarchy": hierarchy - } - + project_name = entity["project"]["full_name"] try: - anatomy = Anatomy(project_name) - anatomy_filled = anatomy.format(data) - workdir = os.path.normpath(anatomy_filled["work"]["folder"]) + pypelib.launch_application(project_name, asset_name, task_name) - except Exception as exc: - msg = "Error in anatomy.format: {}".format( - str(exc) + except pypelib.ApplicationLaunchFailed as exc: + self.log.error(str(exc)) + return { + "success": False, + "message": str(exc) + } + + except Exception: + msg = "Unexpected failure of application launch {}".format( + self.label ) self.log.error(msg, exc_info=True) return { @@ -200,160 +168,6 @@ class AppAction(BaseAction): "message": msg } - try: - os.makedirs(workdir) - except FileExistsError: - pass - - last_workfile_path = None - extensions = HOST_WORKFILE_EXTENSIONS.get(host_name) - if extensions: - # Find last workfile - file_template = anatomy.templates["work"]["file"] - data.update({ - "version": 1, - "user": getpass.getuser(), - "ext": extensions[0] - }) - - last_workfile_path = last_workfile( - workdir, file_template, data, extensions, True - ) - - # set environments for Avalon - prep_env = copy.deepcopy(os.environ) - prep_env.update({ - "AVALON_PROJECT": project_name, - "AVALON_ASSET": asset_name, - "AVALON_TASK": task_name, - "AVALON_APP": host_name, - "AVALON_APP_NAME": self.identifier, - "AVALON_HIERARCHY": hierarchy, - "AVALON_WORKDIR": workdir - }) - - start_last_workfile = should_start_last_workfile( - project_name, host_name, task_name - ) - # Store boolean as "0"(False) or "1"(True) - prep_env["AVALON_OPEN_LAST_WORKFILE"] = ( - str(int(bool(start_last_workfile))) - ) - - if ( - start_last_workfile - and last_workfile_path - and os.path.exists(last_workfile_path) - ): - prep_env["AVALON_LAST_WORKFILE"] = last_workfile_path - - prep_env.update(anatomy.roots_obj.root_environments()) - - # collect all parents from the task - parents = [] - for item in entity['link']: - parents.append(session.get(item['type'], item['id'])) - - # collect all the 'environment' attributes from parents - tools_attr = [prep_env["AVALON_APP"], prep_env["AVALON_APP_NAME"]] - tools_env = asset_document["data"].get("tools_env") or [] - tools_attr.extend(tools_env) - - tools_env = acre.get_tools(tools_attr) - env = acre.compute(tools_env) - env = acre.merge(env, current_env=dict(prep_env)) - - # Get path to execute - st_temp_path = os.environ["PYPE_CONFIG"] - os_plat = platform.system().lower() - - # Path to folder with launchers - path = os.path.join(st_temp_path, "launchers", os_plat) - - # Full path to executable launcher - execfile = None - - if application.get("launch_hook"): - hook = application.get("launch_hook") - self.log.info("launching hook: {}".format(hook)) - ret_val = pypelib.execute_hook( - application.get("launch_hook"), env=env) - if not ret_val: - return { - 'success': False, - 'message': "Hook didn't finish successfully {0}" - .format(self.label) - } - - if sys.platform == "win32": - for ext in os.environ["PATHEXT"].split(os.pathsep): - fpath = os.path.join(path.strip('"'), self.executable + ext) - if os.path.isfile(fpath) and os.access(fpath, os.X_OK): - execfile = fpath - break - - # Run SW if was found executable - if execfile is None: - return { - "success": False, - "message": "We didn't find launcher for {0}".format( - self.label - ) - } - - popen = avalon.lib.launch( - executable=execfile, args=[], environment=env - ) - - elif (sys.platform.startswith("linux") - or sys.platform.startswith("darwin")): - execfile = os.path.join(path.strip('"'), self.executable) - if not os.path.isfile(execfile): - msg = "Launcher doesn't exist - {}".format(execfile) - - self.log.error(msg) - return { - "success": False, - "message": msg - } - - try: - fp = open(execfile) - except PermissionError as perm_exc: - msg = "Access denied on launcher {} - {}".format( - execfile, perm_exc - ) - - self.log.exception(msg, exc_info=True) - return { - "success": False, - "message": msg - } - - fp.close() - # check executable permission - if not os.access(execfile, os.X_OK): - msg = "No executable permission - {}".format(execfile) - - self.log.error(msg) - return { - "success": False, - "message": msg - } - - # Run SW if was found executable - if execfile is None: - return { - "success": False, - "message": "We didn't found launcher for {0}".format( - self.label - ) - } - - popen = avalon.lib.launch( # noqa: F841 - "/usr/bin/env", args=["bash", execfile], environment=env - ) - # Change status of task to In progress presets = config.get_presets()["ftrack"]["ftrack_config"] From 83ac4df7b68db7f4983db6f37560e34c5b494fa9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 01:48:30 +0200 Subject: [PATCH 078/158] added application action to pype.lib --- pype/lib.py | 32 ++++++++++++++++++++++++++++++++ pype/tools/launcher/lib.py | 5 +++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index ff0c0c0e82..46dd2b781d 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1590,3 +1590,35 @@ def launch_application(project_name, asset_name, task_name, app_name): "/usr/bin/env", args=["bash", execfile], environment=env ) return popen + + +class ApplicationAction(avalon.api.Action): + """Default application launcher + + This is a convenience application Action that when "config" refers to a + parsed application `.toml` this can launch the application. + + """ + + config = None + required_session_keys = ( + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK" + ) + + def is_compatible(self, session): + for key in self.required_session_keys: + if key not in session: + return False + return True + + def process(self, session, **kwargs): + """Process the full Application action""" + + project_name = session["AVALON_PROJECT"] + asset_name = session["AVALON_ASSET"] + task_name = session["AVALON_TASK"] + return launch_application( + project_name, asset_name, task_name, self.name + ) diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index 0bbbb55560..027ae96e84 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -16,9 +16,10 @@ provides a bridge between the file-based project inventory and configuration. import os from Qt import QtGui -from avalon import lib, pipeline +from avalon import lib from avalon.vendor import qtawesome from pype.api import resources +from pype.lib import ApplicationAction ICON_CACHE = {} NOT_FOUND = type("NotFound", (object, ), {}) @@ -52,7 +53,7 @@ def get_application_actions(project): action = type( "app_{}".format(app_name), - (pipeline.Application,), + (ApplicationAction,), { "name": app_name, "label": label, From 4f2ffa6022af28355e49bc13c5713a2f8490b658 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 02:21:47 +0200 Subject: [PATCH 079/158] final launcher touches --- pype/tools/launcher/__init__.py | 11 +- pype/tools/launcher/__main__.py | 5 - pype/tools/launcher/actions.py | 12 +- pype/tools/launcher/app.py | 242 ++------------------------------ pype/tools/launcher/lib.py | 1 + 5 files changed, 24 insertions(+), 247 deletions(-) delete mode 100644 pype/tools/launcher/__main__.py diff --git a/pype/tools/launcher/__init__.py b/pype/tools/launcher/__init__.py index 3b88ebe984..8f7bf5a769 100644 --- a/pype/tools/launcher/__init__.py +++ b/pype/tools/launcher/__init__.py @@ -1,10 +1,7 @@ - -from .app import ( - show, - cli -) +from .app import LauncherWindow +from . import actions __all__ = [ - "show", - "cli", + "LauncherWindow", + "actions" ] diff --git a/pype/tools/launcher/__main__.py b/pype/tools/launcher/__main__.py deleted file mode 100644 index 50642c46cd..0000000000 --- a/pype/tools/launcher/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from app import cli - -if __name__ == '__main__': - import sys - sys.exit(cli(sys.argv[1:])) diff --git a/pype/tools/launcher/actions.py b/pype/tools/launcher/actions.py index 44ba9a3a60..80e6f71ae7 100644 --- a/pype/tools/launcher/actions.py +++ b/pype/tools/launcher/actions.py @@ -8,7 +8,6 @@ class ProjectManagerAction(api.Action): name = "projectmanager" label = "Project Manager" icon = "gear" - group = "Test" order = 999 # at the end def is_compatible(self, session): @@ -28,8 +27,7 @@ class LoaderAction(api.Action): name = "loader" label = "Loader" icon = "cloud-download" - order = 998 # at the end - group = "Test" + order = 998 def is_compatible(self, session): return "AVALON_PROJECT" in session @@ -38,7 +36,7 @@ class LoaderAction(api.Action): return lib.launch( executable="python", args=[ - "-u", "-m", "avalon.tools.cbloader", session['AVALON_PROJECT'] + "-u", "-m", "avalon.tools.loader", session['AVALON_PROJECT'] ] ) @@ -72,8 +70,10 @@ def register_config_actions(): module_name = os.environ["AVALON_CONFIG"] config = importlib.import_module(module_name) if not hasattr(config, "register_launcher_actions"): - print("Current configuration `%s` has no 'register_launcher_actions'" - % config.__name__) + print( + "Current configuration `%s` has no 'register_launcher_actions'" + % config.__name__ + ) return config.register_launcher_actions() diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index 6f554110dc..b6a7d4dab2 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -1,10 +1,10 @@ -import sys import copy -from avalon.vendor.Qt import QtWidgets, QtCore, QtGui +from Qt import QtWidgets, QtCore, QtGui from avalon import style from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from pype.api import resources from avalon.tools import lib as tools_lib from avalon.tools.widgets import AssetWidget @@ -16,10 +16,6 @@ from .widgets import ( from .flickcharm import FlickCharm -module = sys.modules[__name__] -module.window = None - - class IconListView(QtWidgets.QListView): """Styled ListView that allows to toggle between icon and list mode. @@ -244,17 +240,21 @@ class AssetsPanel(QtWidgets.QWidget): return session -class Window(QtWidgets.QDialog): +class LauncherWindow(QtWidgets.QDialog): """Launcher interface""" def __init__(self, parent=None): - super(Window, self).__init__(parent) + super(LauncherWindow, self).__init__(parent) self.dbcon = DbConnector() self.setWindowTitle("Launcher") self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) + + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) + self.setStyleSheet(style.load_stylesheet()) # Allow minimize self.setWindowFlags( @@ -287,8 +287,10 @@ class Window(QtWidgets.QDialog): # Vertically split Pages and Actions body = QtWidgets.QSplitter() body.setContentsMargins(0, 0, 0, 0) - body.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + body.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) body.setOrientation(QtCore.Qt.Vertical) body.addWidget(page_slider) body.addWidget(actions_bar) @@ -462,221 +464,3 @@ class Window(QtWidgets.QDialog): # requires a forced refresh first self.asset_panel.on_asset_changed() self.asset_panel.assets_widget.select_task(task_name) - - -class Application(QtWidgets.QApplication): - def __init__(self, *args): - super(Application, self).__init__(*args) - - # Set app icon - icon_path = tools_lib.resource("icons", "png", "avalon-logo-16.png") - icon = QtGui.QIcon(icon_path) - - self.setWindowIcon(icon) - - # Toggles - self.toggles = {"autoHide": False} - - # Timers - keep_visible = QtCore.QTimer(self) - keep_visible.setInterval(100) - keep_visible.setSingleShot(True) - - timers = {"keepVisible": keep_visible} - - tray = QtWidgets.QSystemTrayIcon(icon) - tray.setToolTip("Avalon Launcher") - - # Signals - tray.activated.connect(self.on_tray_activated) - self.aboutToQuit.connect(self.on_quit) - - menu = self.build_menu() - tray.setContextMenu(menu) - tray.show() - - tray.showMessage("Avalon", "Launcher started.") - - # Don't close the app when we close the log window. - # self.setQuitOnLastWindowClosed(False) - - self.focusChanged.connect(self.on_focus_changed) - - window = Window() - window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) - - self.timers = timers - self._tray = tray - self._window = window - - # geometry = self.calculate_window_geometry(window) - # window.setGeometry(geometry) - - def show(self): - """Show the primary GUI - - This also activates the window and deals with platform-differences. - - """ - - self._window.show() - self._window.raise_() - self._window.activateWindow() - - self.timers["keepVisible"].start() - - def on_tray_activated(self, reason): - if self._window.isVisible(): - self._window.hide() - - elif reason == QtWidgets.QSystemTrayIcon.Trigger: - self.show() - - def on_focus_changed(self, old, new): - """Respond to window losing focus""" - window = new - keep_visible = self.timers["keepVisible"].isActive() - self._window.hide() if (self.toggles["autoHide"] and - not window and - not keep_visible) else None - - def on_autohide_changed(self, auto_hide): - """Respond to changes to auto-hide - - Auto-hide is changed in the UI and determines whether or not - the UI hides upon losing focus. - - """ - - self.toggles["autoHide"] = auto_hide - self.echo("Hiding when losing focus" if auto_hide else "Stays visible") - - def on_quit(self): - """Respond to the application quitting""" - self._tray.hide() - - def build_menu(self): - """Build the right-mouse context menu for the tray icon""" - menu = QtWidgets.QMenu() - - icon = qtawesome.icon("fa.eye", color=style.colors.default) - open = QtWidgets.QAction(icon, "Open", self) - open.triggered.connect(self.show) - - def toggle(): - self.on_autohide_changed(not self.toggles['autoHide']) - - keep_open = QtWidgets.QAction("Keep open", self) - keep_open.setCheckable(True) - keep_open.setChecked(not self.toggles['autoHide']) - keep_open.triggered.connect(toggle) - - quit = QtWidgets.QAction("Quit", self) - quit.triggered.connect(self.quit) - - menu.setStyleSheet(""" - QMenu { - padding: 0px; - margin: 0px; - } - """) - - for action in [open, keep_open, quit]: - menu.addAction(action) - - return menu - - def calculate_window_geometry(self, window): - """Respond to status changes - - On creation, align window with where the tray icon is - located. For example, if the tray icon is in the upper - right corner of the screen, then this is where the - window is supposed to appear. - - Arguments: - status (int): Provided by Qt, the status flag of - loading the input file. - - """ - - tray_x = self._tray.geometry().x() - tray_y = self._tray.geometry().y() - - width = window.width() - width = max(width, window.minimumWidth()) - - height = window.height() - height = max(height, window.sizeHint().height()) - - desktop_geometry = QtWidgets.QDesktopWidget().availableGeometry() - screen_geometry = window.geometry() - - screen_width = screen_geometry.width() - screen_height = screen_geometry.height() - - # Calculate width and height of system tray - systray_width = screen_geometry.width() - desktop_geometry.width() - systray_height = screen_geometry.height() - desktop_geometry.height() - - padding = 10 - - x = screen_width - width - y = screen_height - height - - if tray_x < (screen_width / 2): - x = 0 + systray_width + padding - else: - x -= systray_width + padding - - if tray_y < (screen_height / 2): - y = 0 + systray_height + padding - else: - y -= systray_height + padding - - return QtCore.QRect(x, y, width, height) - - -def show(root=None, debug=False, parent=None): - """Display Loader GUI - - Arguments: - debug (bool, optional): Run loader in debug-mode, - defaults to False - parent (QtCore.QObject, optional): When provided parent the interface - to this QObject. - - """ - - app = Application(sys.argv) - app.setStyleSheet(style.load_stylesheet()) - - # Show the window on launch - app.show() - - app.exec_() - - -def cli(args): - import argparse - parser = argparse.ArgumentParser() - #parser.add_argument("project") - - args = parser.parse_args(args) - #project = args.project - - import launcher.actions as actions - print("Registering default actions..") - actions.register_default_actions() - print("Registering config actions..") - actions.register_config_actions() - print("Registering environment actions..") - actions.register_environment_actions() - io.install() - - #io.Session["AVALON_PROJECT"] = project - - import traceback - sys.excepthook = lambda typ, val, tb: traceback.print_last() - - show() diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index 027ae96e84..d307e146d5 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -95,6 +95,7 @@ def get_action_icon(action): ) except Exception: + ICON_CACHE[icon_name] = NOT_FOUND print("Can't load icon \"{}\"".format(icon_name)) return icon From eb16141a0e3c07583dd4efd4fc0c0d113548e725 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 02:22:01 +0200 Subject: [PATCH 080/158] modified avalon_apps module --- pype/modules/avalon_apps/avalon_app.py | 33 ++++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py index d103a84d90..393e1fe755 100644 --- a/pype/modules/avalon_apps/avalon_app.py +++ b/pype/modules/avalon_apps/avalon_app.py @@ -1,10 +1,7 @@ -import os -import argparse -from Qt import QtGui, QtWidgets +from Qt import QtWidgets from avalon.tools import libraryloader from pype.api import Logger -from avalon import io -from launcher import launcher_widget, lib as launcher_lib +from pype.tools.launcher import LauncherWindow, actions class AvalonApps: @@ -12,7 +9,12 @@ class AvalonApps: self.log = Logger().get_logger(__name__) self.main_parent = main_parent self.parent = parent - self.app_launcher = None + + self.app_launcher = LauncherWindow() + + # actions.register_default_actions() + actions.register_config_actions() + actions.register_environment_actions() def process_modules(self, modules): if "RestApiServer" in modules: @@ -32,23 +34,18 @@ class AvalonApps: self.log.warning('Parent menu is not set') return - icon = QtGui.QIcon(launcher_lib.resource("icon", "main.png")) - aShowLauncher = QtWidgets.QAction(icon, "&Launcher", parent_menu) - aLibraryLoader = QtWidgets.QAction("Library", parent_menu) + action_launcher = QtWidgets.QAction("Launcher", parent_menu) + action_library_loader = QtWidgets.QAction("Library", parent_menu) - aShowLauncher.triggered.connect(self.show_launcher) - aLibraryLoader.triggered.connect(self.show_library_loader) + action_launcher.triggered.connect(self.show_launcher) + action_library_loader.triggered.connect(self.show_library_loader) - parent_menu.addAction(aShowLauncher) - parent_menu.addAction(aLibraryLoader) + parent_menu.addAction(action_launcher) + parent_menu.addAction(action_library_loader) def show_launcher(self): # if app_launcher don't exist create it/otherwise only show main window - if self.app_launcher is None: - io.install() - APP_PATH = launcher_lib.resource("qml", "main.qml") - self.app_launcher = launcher_widget.Launcher(APP_PATH) - self.app_launcher.window.show() + self.app_launcher.show() def show_library_loader(self): libraryloader.show( From 03c3b4213ca0f155d4cd32589a1ed4687feada99 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 02:30:28 +0200 Subject: [PATCH 081/158] modified library loader action label --- pype/modules/avalon_apps/avalon_app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py index 393e1fe755..34fbc5c5ae 100644 --- a/pype/modules/avalon_apps/avalon_app.py +++ b/pype/modules/avalon_apps/avalon_app.py @@ -35,7 +35,9 @@ class AvalonApps: return action_launcher = QtWidgets.QAction("Launcher", parent_menu) - action_library_loader = QtWidgets.QAction("Library", parent_menu) + action_library_loader = QtWidgets.QAction( + "Library loader", parent_menu + ) action_launcher.triggered.connect(self.show_launcher) action_library_loader.triggered.connect(self.show_library_loader) From 68df56b31b79535b9f51e05aa776dbeb9478cd01 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 02:33:32 +0200 Subject: [PATCH 082/158] do not use group delegate yet --- pype/tools/launcher/widgets.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 4fc7d166cb..391f9f90f7 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -92,8 +92,9 @@ class ActionBar(QtWidgets.QWidget): model = ActionModel(self.dbcon, self) view.setModel(model) - delegate = ActionDelegate(model.GROUP_ROLE, self) - view.setItemDelegate(delegate) + # TODO better group delegate + # delegate = ActionDelegate(model.GROUP_ROLE, self) + # view.setItemDelegate(delegate) layout.addWidget(view) From 7c6849ce6a649aec763c63a89c05f3ae8218dc00 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 02:39:55 +0200 Subject: [PATCH 083/158] small tweaks --- pype/tools/launcher/app.py | 1 + pype/tools/launcher/delegates.py | 4 ++-- pype/tools/launcher/widgets.py | 2 +- pype/tools/tray/__main__.py | 10 +++++++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/app.py index b6a7d4dab2..df2a47eed8 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/app.py @@ -16,6 +16,7 @@ from .widgets import ( from .flickcharm import FlickCharm + class IconListView(QtWidgets.QListView): """Styled ListView that allows to toggle between icon and list mode. diff --git a/pype/tools/launcher/delegates.py b/pype/tools/launcher/delegates.py index 750301cec4..8e1ec2004e 100644 --- a/pype/tools/launcher/delegates.py +++ b/pype/tools/launcher/delegates.py @@ -3,8 +3,8 @@ from Qt import QtCore, QtWidgets, QtGui class ActionDelegate(QtWidgets.QStyledItemDelegate): extender_lines = 2 - extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100))#, 160)) - extender_fg = QtGui.QColor(255, 255, 255)#, 160) + extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100, 160)) + extender_fg = QtGui.QColor(255, 255, 255, 160) def __init__(self, group_role, *args, **kwargs): super(ActionDelegate, self).__init__(*args, **kwargs) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 391f9f90f7..21546e286e 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -2,7 +2,7 @@ import copy from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome -from .delegates import ActionDelegate +# from .delegates import ActionDelegate from .models import TaskModel, ActionModel, ProjectModel from .flickcharm import FlickCharm diff --git a/pype/tools/tray/__main__.py b/pype/tools/tray/__main__.py index d0006c0afe..94d5461dc4 100644 --- a/pype/tools/tray/__main__.py +++ b/pype/tools/tray/__main__.py @@ -1,4 +1,12 @@ +import os import sys import pype_tray -sys.exit(pype_tray.PypeTrayApplication().exec_()) +app = pype_tray.PypeTrayApplication() +if os.name == "nt": + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + u"pype_tray" + ) + +sys.exit(app.exec_()) From 26142007452d7c0d213797af729587da39ef1354 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 03:01:32 +0200 Subject: [PATCH 084/158] app.py renamed to window.py --- pype/tools/launcher/__init__.py | 2 +- pype/tools/launcher/{app.py => window.py} | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) rename pype/tools/launcher/{app.py => window.py} (97%) diff --git a/pype/tools/launcher/__init__.py b/pype/tools/launcher/__init__.py index 8f7bf5a769..109d642e86 100644 --- a/pype/tools/launcher/__init__.py +++ b/pype/tools/launcher/__init__.py @@ -1,4 +1,4 @@ -from .app import LauncherWindow +from .window import LauncherWindow from . import actions __all__ = [ diff --git a/pype/tools/launcher/app.py b/pype/tools/launcher/window.py similarity index 97% rename from pype/tools/launcher/app.py rename to pype/tools/launcher/window.py index df2a47eed8..b53b5b415c 100644 --- a/pype/tools/launcher/app.py +++ b/pype/tools/launcher/window.py @@ -1,4 +1,5 @@ import copy +import logging from Qt import QtWidgets, QtCore, QtGui from avalon import style @@ -247,6 +248,9 @@ class LauncherWindow(QtWidgets.QDialog): def __init__(self, parent=None): super(LauncherWindow, self).__init__(parent) + self.log = logging.getLogger( + ".".join([__name__, self.__class__.__name__]) + ) self.dbcon = DbConnector() self.setWindowTitle("Launcher") @@ -365,7 +369,7 @@ class LauncherWindow(QtWidgets.QDialog): def echo(self, message): self.message_label.setText(str(message)) QtCore.QTimer.singleShot(5000, lambda: self.message_label.setText("")) - print(message) + self.log.debug(message) def on_project_changed(self): project_name = self.asset_panel.project_bar.get_current_project() @@ -399,7 +403,7 @@ class LauncherWindow(QtWidgets.QDialog): self.actions_bar.model.refresh() def on_action_clicked(self, action): - self.echo("Running action: %s" % action.name) + self.echo("Running action: {}".format(action.name)) self.run_action(action) def on_history_action(self, history_data): @@ -440,7 +444,11 @@ class LauncherWindow(QtWidgets.QDialog): self.action_history.add_action(action, session) # Process the Action - action().process(session) + try: + action().process(session) + except Exception as exc: + self.log.warning("Action launch failed.", exc_info=True) + self.echo("Failed: {}".format(str(exc))) def set_session(self, session): project_name = session.get("AVALON_PROJECT") From f011223f22ec987ab84db6a582a21775989c598e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 15 Aug 2020 03:05:14 +0200 Subject: [PATCH 085/158] hound fix --- pype/tools/launcher/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index 61e240c2eb..fee09e4f4b 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -76,9 +76,9 @@ class TaskModel(QtGui.QStandardItemModel): if not asset_docs: return - task_names = collections.Counter() + task_names = set() for asset_doc in asset_docs: - asset_tasks = asset_doc.get("data", {}).get("tasks", []) + asset_tasks = asset_doc.get("data", {}).get("tasks") or set() task_names.update(asset_tasks) self.beginResetModel() @@ -89,7 +89,7 @@ class TaskModel(QtGui.QStandardItemModel): self.appendRow(item) else: - for task_name, count in sorted(task_names.items()): + for task_name in sorted(task_names): icon = self._icons.get(task_name, self.default_icon) item = QtGui.QStandardItem(icon, task_name) self.appendRow(item) From 0b48d447295384c1abc7a5744561fe1cc79ce4f1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 17 Aug 2020 08:54:35 +0100 Subject: [PATCH 086/158] Make keepImages a class attribute --- pype/plugins/maya/create/create_review.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/maya/create/create_review.py b/pype/plugins/maya/create/create_review.py index c488a7559c..d76b78c580 100644 --- a/pype/plugins/maya/create/create_review.py +++ b/pype/plugins/maya/create/create_review.py @@ -11,6 +11,7 @@ class CreateReview(avalon.maya.Creator): family = "review" icon = "video-camera" defaults = ['Main'] + keepImages = False def __init__(self, *args, **kwargs): super(CreateReview, self).__init__(*args, **kwargs) @@ -21,6 +22,6 @@ class CreateReview(avalon.maya.Creator): for key, value in animation_data.items(): data[key] = value - data["keepImages"] = False + data["keepImages"] = self.keepImages self.data = data From 3a096e5b638ccf47199eb30d7c390f6a5c06d432 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 10:10:18 +0200 Subject: [PATCH 087/158] fixed actions history --- pype/tools/launcher/widgets.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 21546e286e..c3a908c9dd 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -3,6 +3,7 @@ from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome # from .delegates import ActionDelegate +from . import lib from .models import TaskModel, ActionModel, ProjectModel from .flickcharm import FlickCharm @@ -264,9 +265,7 @@ class ActionHistory(QtWidgets.QPushButton): m = "{{action:{0}}} | {{breadcrumb}}".format(largest_action_label) label = m.format(action=action.label, breadcrumb=breadcrumb) - icon_name = action.icon - color = action.color or "white" - icon = qtawesome.icon("fa.%s" % icon_name, color=color) + icon = lib.get_action_icon(action) item = QtWidgets.QListWidgetItem(icon, label) item.setData(action_session_role, (action, session)) From 00523f0c500412721c50907ef35deacc3c7c7569 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:02:23 +0200 Subject: [PATCH 088/158] added label_variant to actions --- pype/tools/launcher/lib.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index d307e146d5..25270fcbfe 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -48,15 +48,16 @@ def get_application_actions(project): icon = app_definition.get("icon", app.get("icon", "folder-o")) color = app_definition.get("color", app.get("color", None)) order = app_definition.get("order", app.get("order", 0)) - label = app.get("label") or app_definition.get("label") or app["name"] - group = app.get("group") or app_definition.get("group") - + label = app_definition.get("label") or app.get("label") or app_name + label_variant = app_definition.get("label_variant") + group = app_definition.get("group") or app.get("group") action = type( "app_{}".format(app_name), (ApplicationAction,), { "name": app_name, "label": label, + "label_variant": label_variant, "group": group, "icon": icon, "color": color, From 15f97fbad1ef10657be83f00d2efea7996bce567 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:02:56 +0200 Subject: [PATCH 089/158] variantss are showing in proper way --- pype/tools/launcher/lib.py | 11 +++++++ pype/tools/launcher/models.py | 54 +++++++++++++++++++++++++++------- pype/tools/launcher/widgets.py | 51 ++++++++++++++++++++++++++++---- 3 files changed, 100 insertions(+), 16 deletions(-) diff --git a/pype/tools/launcher/lib.py b/pype/tools/launcher/lib.py index 25270fcbfe..a6d6ff6865 100644 --- a/pype/tools/launcher/lib.py +++ b/pype/tools/launcher/lib.py @@ -100,3 +100,14 @@ def get_action_icon(action): print("Can't load icon \"{}\"".format(icon_name)) return icon + + +def get_action_label(action): + label = getattr(action, "label", None) + if not label: + return action.name + + label_variant = getattr(action, "label_variant", None) + if not label_variant: + return label + return " ".join([label, label_variant]) diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index fee09e4f4b..f76e26afde 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -155,25 +155,57 @@ class ActionModel(QtGui.QStandardItemModel): self.beginResetModel() single_actions = [] + varianted_actions = collections.defaultdict(list) grouped_actions = collections.defaultdict(list) for action in actions: + # Groups group_name = getattr(action, "group", None) - if not group_name: - single_actions.append(action) - else: + + # Lable variants + label = getattr(action, "label", None) + label_variant = getattr(action, "label_variant", None) + if label_variant and not label: + print(( + "Invalid action \"{}\" has set `label_variant` to \"{}\"" + ", but doesn't have set `label` attribute" + ).format(action.name, label_variant)) + action.label_variant = None + label_variant = None + + if group_name: grouped_actions[group_name].append(action) - for group_name, actions in tuple(grouped_actions.items()): - if len(actions) == 1: - grouped_actions.pop(group_name) - single_actions.append(actions[0]) + elif label_variant: + varianted_actions[label].append(action) + else: + single_actions.append(action) items_by_order = collections.defaultdict(list) + for label, actions in tuple(varianted_actions.items()): + if len(actions) == 1: + varianted_actions.pop(label) + single_actions.append(actions[0]) + continue + + icon = None + order = None + for action in actions: + if icon is None: + _icon = lib.get_action_icon(action) + if _icon: + icon = _icon + + if order is None or action.order < order: + order = action.order + + item = QtGui.QStandardItem(icon, action.label) + item.setData(actions, self.ACTION_ROLE) + item.setData(True, self.GROUP_ROLE) + items_by_order[order].append(item) + for action in single_actions: icon = self.get_icon(action) - item = QtGui.QStandardItem( - icon, str(action.label or action.name) - ) + item = QtGui.QStandardItem(icon, lib.get_action_label(action)) item.setData(action, self.ACTION_ROLE) items_by_order[action.order].append(item) @@ -185,7 +217,7 @@ class ActionModel(QtGui.QStandardItemModel): order = action.order if icon is None: - _icon = self.get_icon(action) + _icon = lib.get_action_icon(action) if _icon: icon = _icon diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index c3a908c9dd..774c3de5ee 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -1,4 +1,5 @@ import copy +import collections from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome @@ -123,13 +124,53 @@ class ActionBar(QtWidgets.QWidget): self.action_clicked.emit(action) return - menu = QtWidgets.QMenu(self) actions = index.data(self.model.ACTION_ROLE) - actions_mapping = {} + by_variant_label = collections.defaultdict(list) + orders = [] for action in actions: - menu_action = QtWidgets.QAction(action.label or action.name) - menu.addAction(menu_action) - actions_mapping[menu_action] = action + # Lable variants + label = getattr(action, "label", None) + label_variant = getattr(action, "label_variant", None) + if label_variant and not label: + label_variant = None + + if not label_variant: + orders.append(action) + continue + + if label not in orders: + orders.append(label) + by_variant_label[label].append(action) + + menu = QtWidgets.QMenu(self) + actions_mapping = {} + + for action_item in orders: + actions = by_variant_label.get(action_item) + if not actions: + action = action_item + elif len(actions) == 1: + action = actions[0] + else: + action = None + + if action: + menu_action = QtWidgets.QAction( + lib.get_action_label(action) + ) + menu.addAction(menu_action) + actions_mapping[menu_action] = action + continue + + sub_menu = QtWidgets.QMenu(label, menu) + for action in actions: + menu_action = QtWidgets.QAction( + lib.get_action_label(action) + ) + sub_menu.addAction(menu_action) + actions_mapping[menu_action] = action + + menu.addMenu(sub_menu) result = menu.exec_(QtGui.QCursor.pos()) if result: From 8b4f209da42fe4e20d8c5cccfec4b4ba5c09f7bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:03:19 +0200 Subject: [PATCH 090/158] added group and variant to ApplicationAction --- pype/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/lib.py b/pype/lib.py index 46dd2b781d..00bd72a164 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1601,6 +1601,8 @@ class ApplicationAction(avalon.api.Action): """ config = None + group = None + variant = None required_session_keys = ( "AVALON_PROJECT", "AVALON_ASSET", From 0bda8c0d2faf86d0b5c47abb1f407491979b312e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:03:40 +0200 Subject: [PATCH 091/158] use non perfect group delegate to make clear what's grouped --- pype/tools/launcher/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 774c3de5ee..82435e8681 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -3,7 +3,7 @@ import collections from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome -# from .delegates import ActionDelegate +from .delegates import ActionDelegate from . import lib from .models import TaskModel, ActionModel, ProjectModel from .flickcharm import FlickCharm @@ -95,8 +95,8 @@ class ActionBar(QtWidgets.QWidget): view.setModel(model) # TODO better group delegate - # delegate = ActionDelegate(model.GROUP_ROLE, self) - # view.setItemDelegate(delegate) + delegate = ActionDelegate(model.GROUP_ROLE, self) + view.setItemDelegate(delegate) layout.addWidget(view) From 33ea814c890a9c3c71d1f18945786f3ab7856e4e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:23:59 +0200 Subject: [PATCH 092/158] variants are working now --- pype/tools/launcher/delegates.py | 10 ++-- pype/tools/launcher/models.py | 3 +- pype/tools/launcher/widgets.py | 87 ++++++++++++++++++-------------- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/pype/tools/launcher/delegates.py b/pype/tools/launcher/delegates.py index 8e1ec2004e..95ccde6445 100644 --- a/pype/tools/launcher/delegates.py +++ b/pype/tools/launcher/delegates.py @@ -6,13 +6,17 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate): extender_bg_brush = QtGui.QBrush(QtGui.QColor(100, 100, 100, 160)) extender_fg = QtGui.QColor(255, 255, 255, 160) - def __init__(self, group_role, *args, **kwargs): + def __init__(self, group_roles, *args, **kwargs): super(ActionDelegate, self).__init__(*args, **kwargs) - self.group_role = group_role + self.group_roles = group_roles def paint(self, painter, option, index): super(ActionDelegate, self).paint(painter, option, index) - is_group = index.data(self.group_role) + is_group = False + for group_role in self.group_roles: + is_group = index.data(group_role) + if is_group: + break if not is_group: return diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index f76e26afde..3fb201702e 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -109,6 +109,7 @@ class TaskModel(QtGui.QStandardItemModel): class ActionModel(QtGui.QStandardItemModel): ACTION_ROLE = QtCore.Qt.UserRole GROUP_ROLE = QtCore.Qt.UserRole + 1 + VARIANT_GROUP_ROLE = QtCore.Qt.UserRole + 2 def __init__(self, dbcon, parent=None): super(ActionModel, self).__init__(parent=parent) @@ -200,7 +201,7 @@ class ActionModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem(icon, action.label) item.setData(actions, self.ACTION_ROLE) - item.setData(True, self.GROUP_ROLE) + item.setData(True, self.VARIANT_GROUP_ROLE) items_by_order[order].append(item) for action in single_actions: diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 82435e8681..7ab0a3f8ea 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -95,7 +95,10 @@ class ActionBar(QtWidgets.QWidget): view.setModel(model) # TODO better group delegate - delegate = ActionDelegate(model.GROUP_ROLE, self) + delegate = ActionDelegate( + [model.GROUP_ROLE, model.VARIANT_GROUP_ROLE], + self + ) view.setItemDelegate(delegate) layout.addWidget(view) @@ -119,58 +122,68 @@ class ActionBar(QtWidgets.QWidget): return is_group = index.data(self.model.GROUP_ROLE) - if not is_group: + is_variant_group = index.data(self.model.VARIANT_GROUP_ROLE) + if not is_group and not is_variant_group: action = index.data(self.model.ACTION_ROLE) self.action_clicked.emit(action) return actions = index.data(self.model.ACTION_ROLE) - by_variant_label = collections.defaultdict(list) - orders = [] - for action in actions: - # Lable variants - label = getattr(action, "label", None) - label_variant = getattr(action, "label_variant", None) - if label_variant and not label: - label_variant = None - - if not label_variant: - orders.append(action) - continue - - if label not in orders: - orders.append(label) - by_variant_label[label].append(action) menu = QtWidgets.QMenu(self) actions_mapping = {} - for action_item in orders: - actions = by_variant_label.get(action_item) - if not actions: - action = action_item - elif len(actions) == 1: - action = actions[0] - else: - action = None - - if action: + if is_variant_group: + for action in actions: menu_action = QtWidgets.QAction( lib.get_action_label(action) ) menu.addAction(menu_action) actions_mapping[menu_action] = action - continue - - sub_menu = QtWidgets.QMenu(label, menu) + else: + by_variant_label = collections.defaultdict(list) + orders = [] for action in actions: - menu_action = QtWidgets.QAction( - lib.get_action_label(action) - ) - sub_menu.addAction(menu_action) - actions_mapping[menu_action] = action + # Lable variants + label = getattr(action, "label", None) + label_variant = getattr(action, "label_variant", None) + if label_variant and not label: + label_variant = None - menu.addMenu(sub_menu) + if not label_variant: + orders.append(action) + continue + + if label not in orders: + orders.append(label) + by_variant_label[label].append(action) + + for action_item in orders: + actions = by_variant_label.get(action_item) + if not actions: + action = action_item + elif len(actions) == 1: + action = actions[0] + else: + action = None + + if action: + menu_action = QtWidgets.QAction( + lib.get_action_label(action) + ) + menu.addAction(menu_action) + actions_mapping[menu_action] = action + continue + + sub_menu = QtWidgets.QMenu(label, menu) + for action in actions: + menu_action = QtWidgets.QAction( + lib.get_action_label(action) + ) + sub_menu.addAction(menu_action) + actions_mapping[menu_action] = action + + menu.addMenu(sub_menu) result = menu.exec_(QtGui.QCursor.pos()) if result: From 3b18458cac54e7c99b5e484f1f9508b767085ccb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:39:22 +0200 Subject: [PATCH 093/158] fixed action history --- pype/tools/launcher/window.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/pype/tools/launcher/window.py b/pype/tools/launcher/window.py index b53b5b415c..70a8b0fc2e 100644 --- a/pype/tools/launcher/window.py +++ b/pype/tools/launcher/window.py @@ -317,18 +317,6 @@ class LauncherWindow(QtWidgets.QDialog): self.asset_panel = asset_panel self.actions_bar = actions_bar self.action_history = action_history - - self.data = { - "pages": { - "project": project_panel, - "asset": asset_panel - }, - "model": { - "actions": actions_bar, - "action_history": action_history - }, - } - self.page_slider = page_slider self._page = 0 @@ -458,10 +446,14 @@ class LauncherWindow(QtWidgets.QDialog): if project_name: # Force the "in project" view. - self.pages.slide_view(1, direction="right") - index = self.asset_panel.project_bar.view.findText(project_name) + self.page_slider.slide_view(1, direction="right") + index = self.asset_panel.project_bar.project_combobox.findText( + project_name + ) if index >= 0: - self.asset_panel.project_bar.view.setCurrentIndex(index) + self.asset_panel.project_bar.project_combobox.setCurrentIndex( + index + ) if silo: self.asset_panel.assets_widget.set_silo(silo) @@ -472,4 +464,4 @@ class LauncherWindow(QtWidgets.QDialog): if task_name: # requires a forced refresh first self.asset_panel.on_asset_changed() - self.asset_panel.assets_widget.select_task(task_name) + self.asset_panel.tasks_widget.select_task(task_name) From b2cbfe98d934be40f219377000feb3d30b583ed3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Aug 2020 12:42:05 +0200 Subject: [PATCH 094/158] reverse logic of action history --- pype/tools/launcher/window.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/launcher/window.py b/pype/tools/launcher/window.py index 70a8b0fc2e..13b4abee6e 100644 --- a/pype/tools/launcher/window.py +++ b/pype/tools/launcher/window.py @@ -401,11 +401,11 @@ class LauncherWindow(QtWidgets.QDialog): is_control_down = QtCore.Qt.ControlModifier & modifiers if is_control_down: - # User is holding control, rerun the action - self.run_action(action, session=session) - else: # Revert to that "session" location self.set_session(session) + else: + # User is holding control, rerun the action + self.run_action(action, session=session) def get_current_session(self): if self._page == 1: From f52fe3beefcfba1bd0ef744d943aa044a4b83e96 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 17 Aug 2020 15:31:44 +0100 Subject: [PATCH 095/158] Add "isolate" to reviewable subset --- pype/plugins/maya/publish/collect_review.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/maya/publish/collect_review.py b/pype/plugins/maya/publish/collect_review.py index 063a854bd1..e16b6ae9f9 100644 --- a/pype/plugins/maya/publish/collect_review.py +++ b/pype/plugins/maya/publish/collect_review.py @@ -63,6 +63,7 @@ class CollectReview(pyblish.api.InstancePlugin): data['handles'] = instance.data.get('handles', None) data['step'] = instance.data['step'] data['fps'] = instance.data['fps'] + data["isolate"] = instance.data["isolate"] cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) instance.context[i].data.update(data) From 71bfa3774a944a65201bf779bc5335d61b6c57ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 17 Aug 2020 16:50:29 +0200 Subject: [PATCH 096/158] submit each assembly job separately --- .../global/publish/submit_publish_job.py | 5 ++-- .../maya/publish/submit_maya_deadline.py | 30 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index d106175eb6..1a3d0df1b3 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -897,8 +897,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # # Batch name reflect original scene name - if instance.data.get("assemblySubmissionJob"): - render_job["Props"]["Batch"] = instance.data.get("jobBatchName") + if instance.data.get("assemblySubmissionJobs"): + render_job["Props"]["Batch"] = instance.data.get( + "jobBatchName") else: render_job["Props"]["Batch"] = os.path.splitext( os.path.basename(context.data.get("currentFile")))[0] diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index d6d4bd2910..afa5496455 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -638,6 +638,9 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "Path is unreachable: `{}`".format( os.path.dirname(config_file))) + # add config file as job auxFile + assembly_payloads[hash]["AuxFiles"] = [config_file] + with open(config_file, "w") as cf: print("TileCount={}".format(tiles_count), file=cf) print("ImageFileName={}".format(file), file=cf) @@ -658,20 +661,23 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): for k, v in tiles.items(): print("{}={}".format(k, v), file=cf) - self.log.debug(json.dumps(assembly_payloads, - indent=4, sort_keys=True)) - self.log.info( - "Submitting assembly job(s) [{}] ...".format(len(assembly_payloads))) # noqa: E501 - url = "{}/api/jobs".format(self._deadline_url) - response = self._requests_post(url, json={ - "Jobs": list(assembly_payloads.values()), - "AuxFiles": [] - }) - if not response.ok: - raise Exception(response) + job_idx = 1 + instance.data["assemblySubmissionJobs"] = [] + for k, ass_job in assembly_payloads.items(): + self.log.info("submitting assembly job {} of {}".format( + job_idx, len(assembly_payloads) + )) + self.log.debug(json.dumps(ass_job, indent=4, sort_keys=True)) + response = self._requests_post(url, json=ass_job) + if not response.ok: + raise Exception(response.text) + + instance.data["assemblySubmissionJobs"].append(ass_job) + job_idx += 1 - instance.data["assemblySubmissionJob"] = assembly_payloads instance.data["jobBatchName"] = payload["JobInfo"]["BatchName"] + self.log.info("Setting batch name on instance: {}".format( + instance.data["jobBatchName"])) else: # Submit job to farm -------------------------------------------- self.log.info("Submitting ...") From 9d917f5fa42a9075089bb5154e8a2c6a6a102d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 17 Aug 2020 17:20:36 +0200 Subject: [PATCH 097/158] delete unused tile validator --- .../validate_deadline_tile_submission.py | 69 ------------------- 1 file changed, 69 deletions(-) delete mode 100644 pype/plugins/maya/publish/validate_deadline_tile_submission.py diff --git a/pype/plugins/maya/publish/validate_deadline_tile_submission.py b/pype/plugins/maya/publish/validate_deadline_tile_submission.py deleted file mode 100644 index b0b995de3e..0000000000 --- a/pype/plugins/maya/publish/validate_deadline_tile_submission.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validate settings from Deadline Submitter. - -This is useful mainly for tile rendering, where jobs on farm are created by -submitter script from Maya. - -Unfortunately Deadline doesn't expose frame number for tiles job so that -cannot be validated, even if it is important setting. Also we cannot -determine if 'Region Rendering' (tile rendering) is enabled or not because -of the same thing. - -""" -import os - -from maya import mel -from maya import cmds - -import pyblish.api -from pype.hosts.maya import lib - - -class ValidateDeadlineTileSubmission(pyblish.api.InstancePlugin): - """Validate Deadline Submission settings are OK for tile rendering.""" - - label = "Validate Deadline Tile Submission" - order = pyblish.api.ValidatorOrder - hosts = ["maya"] - families = ["renderlayer"] - if not os.environ.get("DEADLINE_REST_URL"): - active = False - - def process(self, instance): - """Entry point.""" - # try if Deadline submitter was loaded - if mel.eval("exists SubmitJobToDeadline") == 0: - # if not, try to load it manually - try: - mel.eval("source DeadlineMayaClient;") - except RuntimeError: - raise AssertionError("Deadline Maya client cannot be loaded") - mel.eval("DeadlineMayaClient();") - assert mel.eval("exists SubmitJobToDeadline") == 1, ( - "Deadline Submission script cannot be initialized.") - if instance.data.get("tileRendering"): - job_name = cmds.getAttr("defaultRenderGlobals.deadlineJobName") - scene_name = os.path.splitext(os.path.basename( - instance.context.data.get("currentFile")))[0] - if job_name != scene_name: - self.log.warning(("Job submitted through Deadline submitter " - "has different name then current scene " - "{} / {}").format(job_name, scene_name)) - if cmds.getAttr("defaultRenderGlobals.deadlineTileSingleJob") == 1: - layer = instance.data['setMembers'] - anim_override = lib.get_attr_in_layer( - "defaultRenderGlobals.animation", layer=layer) - assert anim_override, ( - "Animation must be enabled in " - "Render Settings even when rendering single frame." - ) - - start_frame = cmds.getAttr("defaultRenderGlobals.startFrame") - end_frame = cmds.getAttr("defaultRenderGlobals.endFrame") - assert start_frame == end_frame, ( - "Start frame and end frame are not equals. When " - "'Submit All Tles As A Single Job' is selected, only " - "single frame is expected to be rendered. It must match " - "the one specified in Deadline Submitter under " - "'Region Rendering'" - ) From 1f55e22c1313cfc5122186b22b90d5d3befd651e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 17 Aug 2020 17:24:15 +0200 Subject: [PATCH 098/158] fix hound warning --- pype/plugins/maya/publish/submit_maya_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index afa5496455..cf17de3445 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -663,7 +663,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): job_idx = 1 instance.data["assemblySubmissionJobs"] = [] - for k, ass_job in assembly_payloads.items(): + for _k, ass_job in assembly_payloads.items(): self.log.info("submitting assembly job {} of {}".format( job_idx, len(assembly_payloads) )) From c65ab39d330d58009b73c2fecadeb4ce442e8853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 17 Aug 2020 21:46:48 +0200 Subject: [PATCH 099/158] fix prefix path for tiles --- pype/plugins/maya/publish/submit_maya_deadline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index cf17de3445..34e4432aa5 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -130,8 +130,8 @@ def _format_tiles(filename, index, tiles_x, tiles_y, width, height, prefix): os.path.basename(filename) ) out["JobInfo"][out_tile_index] = new_filename - out["PluginInfo"]["RegionPrefix{}".format(str(tile))] = tile_prefix.join( # noqa: E501 - prefix.rsplit("/", 1)) + out["PluginInfo"]["RegionPrefix{}".format(str(tile))] = \ + "/{}".format(tile_prefix).join(prefix.rsplit("/", 1)) out["PluginInfo"]["RegionTop{}".format(tile)] = int(height) - (tile_y * h_space) # noqa: E501 out["PluginInfo"]["RegionBottom{}".format(tile)] = int(height) - ((tile_y - 1) * h_space) - 1 # noqa: E501 @@ -252,7 +252,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): optional = True use_published = True - tile_assembler_plugin = "DraftTileAssembler" + tile_assembler_plugin = "PypeTileAssembler" def process(self, instance): """Plugin entry point.""" From 5e552598bf291d8b8da295b3b110cccaaff20732 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 17 Aug 2020 22:04:54 +0100 Subject: [PATCH 100/158] Copy bit rate of input video to match quality. --- pype/scripts/otio_burnin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py index 16e24757dd..718943855c 100644 --- a/pype/scripts/otio_burnin.py +++ b/pype/scripts/otio_burnin.py @@ -524,6 +524,10 @@ def burnins_from_data( profile_name = profile_name.replace(" ", "_").lower() ffmpeg_args.append("-profile:v {}".format(profile_name)) + bit_rate = burnin._streams[0].get("bit_rate") + if bit_rate: + ffmpeg_args.append("--b:v {}".format(bit_rate)) + pix_fmt = burnin._streams[0].get("pix_fmt") if pix_fmt: ffmpeg_args.append("-pix_fmt {}".format(pix_fmt)) From 63ae53a8d2ebd0e4da081a5a13e9a1e4590cce9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 18 Aug 2020 14:49:30 +0200 Subject: [PATCH 101/158] fix job naming for multiple frames --- pype/plugins/maya/publish/submit_maya_deadline.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 34e4432aa5..ca796d0a1c 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -566,13 +566,16 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): file_index = 1 for file in files: frame = re.search(R_FRAME_NUMBER, file).group("frame") - new_payload = copy.copy(payload) + new_payload = copy.deepcopy(payload) new_payload["JobInfo"]["Name"] = \ "{} (Frame {} - {} tiles)".format( - new_payload["JobInfo"]["Name"], + payload["JobInfo"]["Name"], frame, instance.data.get("tilesX") * instance.data.get("tilesY") # noqa: E501 ) + self.log.info( + "... preparing job {}".format( + new_payload["JobInfo"]["Name"])) new_payload["JobInfo"]["TileJobFrame"] = frame tiles_data = _format_tiles( @@ -592,7 +595,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): frame_payloads.append(new_payload) - new_assembly_payload = copy.copy(assembly_payload) + new_assembly_payload = copy.deepcopy(assembly_payload) new_assembly_payload["JobInfo"]["OutputFilename0"] = re.sub( REPL_FRAME_NUMBER, "\\1{}\\3".format("#" * len(frame)), file) From 42a19156f49fdfb184c6b2686b7feb24c387317a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 18 Aug 2020 15:30:32 +0200 Subject: [PATCH 102/158] fix assembly job names --- pype/plugins/maya/publish/submit_maya_deadline.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index ca796d0a1c..eeb6472850 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -596,6 +596,10 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): frame_payloads.append(new_payload) new_assembly_payload = copy.deepcopy(assembly_payload) + new_assembly_payload["JobInfo"]["Name"] = \ + "{} (Frame {})".format( + assembly_payload["JobInfo"]["Name"], + frame) new_assembly_payload["JobInfo"]["OutputFilename0"] = re.sub( REPL_FRAME_NUMBER, "\\1{}\\3".format("#" * len(frame)), file) From 866bc150085f3eeb82f6042f9866f130de95b400 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Aug 2020 15:53:38 +0200 Subject: [PATCH 103/158] reraise launcher window if is already shown but in back --- pype/modules/avalon_apps/avalon_app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py index 34fbc5c5ae..7ed651f82b 100644 --- a/pype/modules/avalon_apps/avalon_app.py +++ b/pype/modules/avalon_apps/avalon_app.py @@ -48,6 +48,8 @@ class AvalonApps: def show_launcher(self): # if app_launcher don't exist create it/otherwise only show main window self.app_launcher.show() + self.app_launcher.raise_() + self.app_launcher.activateWindow() def show_library_loader(self): libraryloader.show( From 8f9fb484ce48d7a8ac967a711648d9a3cf7adab2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 18 Aug 2020 17:33:58 +0200 Subject: [PATCH 104/158] added missing argument to launch_application in ftrack app handler --- pype/modules/ftrack/lib/ftrack_app_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index 4847464973..23776aced7 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -149,7 +149,9 @@ class AppAction(BaseAction): asset_name = entity["parent"]["name"] project_name = entity["project"]["full_name"] try: - pypelib.launch_application(project_name, asset_name, task_name) + pypelib.launch_application( + project_name, asset_name, task_name, self.identifier + ) except pypelib.ApplicationLaunchFailed as exc: self.log.error(str(exc)) From a200be829b7beb35b21fbe9c10b5466ae0a9750e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 19 Aug 2020 16:10:41 +0200 Subject: [PATCH 105/158] tile support for separate AOVs and better dep handling --- .../global/publish/submit_publish_job.py | 26 ++++---------- .../maya/publish/submit_maya_deadline.py | 35 ++++++++++++++----- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 1a3d0df1b3..d2f38c328b 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -275,26 +275,14 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # Mandatory for Deadline, may be empty "AuxFiles": [], } - """ - In this part we will add file dependencies instead of job dependencies. - This way we don't need to take care of tile assembly job, getting its - id or name. We expect it to produce specific file with specific name - and we are just waiting for them. - """ + + # add assembly jobs as dependencies if instance.data.get("tileRendering"): - self.log.info("Adding tile assembly results as dependencies...") - asset_index = 0 - for inst in instances: - for represenation in inst.get("representations", []): - if isinstance(represenation["files"], (list, tuple)): - for file in represenation["files"]: - dependency = os.path.join(output_dir, file) - payload["JobInfo"]["AssetDependency{}".format(asset_index)] = dependency # noqa: E501 - else: - dependency = os.path.join( - output_dir, represenation["files"]) - payload["JobInfo"]["AssetDependency{}".format(asset_index)] = dependency # noqa: E501 - asset_index += 1 + self.log.info("Adding tile assembly jobs as dependencies...") + job_index = 0 + for assembly_id in instance.data.get("assemblySubmissionJobs"): + payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 + job_index += 1 else: payload["JobInfo"]["JobDependency0"] = job["_id"] diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index eeb6472850..5baea9d82b 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -24,6 +24,7 @@ import copy import re import hashlib from datetime import datetime +import itertools import clique import requests @@ -548,7 +549,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): assembly_payload["JobInfo"].update(output_filenames) frame_payloads = [] - assembly_payloads = {} + assembly_payloads = [] R_FRAME_NUMBER = re.compile(r".+\.(?P[0-9]+)\..+") # noqa: N806, E501 REPL_FRAME_NUMBER = re.compile(r"(.+\.)([0-9]+)(\..+)") # noqa: N806, E501 @@ -557,11 +558,19 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # we have aovs and we need to iterate over them # get files from `beauty` files = exp[0].get("beauty") + # assembly files are used for assembly jobs as we need to put + # together all AOVs + assembly_files = list( + itertools.chain.from_iterable( + [f for _, f in exp[0].items()])) if not files: # if beauty doesn't exists, use first aov we found files = exp[0].get(list(exp[0].keys())[0]) else: files = exp + assembly_files = files + + frame_jobs = {} file_index = 1 for file in files: @@ -590,11 +599,16 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): new_payload["PluginInfo"].update(tiles_data["PluginInfo"]) job_hash = hashlib.sha256("{}_{}".format(file_index, file)) + frame_jobs[frame] = job_hash.hexdigest() new_payload["JobInfo"]["ExtraInfo0"] = job_hash.hexdigest() new_payload["JobInfo"]["ExtraInfo1"] = file frame_payloads.append(new_payload) + file_index += 1 + file_index = 1 + for file in assembly_files: + frame = re.search(R_FRAME_NUMBER, file).group("frame") new_assembly_payload = copy.deepcopy(assembly_payload) new_assembly_payload["JobInfo"]["Name"] = \ "{} (Frame {})".format( @@ -604,9 +618,9 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): REPL_FRAME_NUMBER, "\\1{}\\3".format("#" * len(frame)), file) - new_assembly_payload["JobInfo"]["ExtraInfo0"] = job_hash.hexdigest() # noqa: E501 + new_assembly_payload["JobInfo"]["ExtraInfo0"] = frame_jobs[frame] # noqa: E501 new_assembly_payload["JobInfo"]["ExtraInfo1"] = file - assembly_payloads[job_hash.hexdigest()] = new_assembly_payload + assembly_payloads.append(new_assembly_payload) file_index += 1 self.log.info( @@ -622,9 +636,13 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): job_id = response.json()["_id"] hash = response.json()["Props"]["Ex0"] - file = response.json()["Props"]["Ex1"] - assembly_payloads[hash]["JobInfo"]["JobDependency0"] = job_id + for assembly_job in assembly_payloads: + if assembly_job["JobInfo"]["ExtraInfo0"] == hash: + assembly_job["JobInfo"]["JobDependency0"] = job_id + + for assembly_job in assembly_payloads: + file = assembly_job["JobInfo"]["ExtraInfo1"] # write assembly job config files now = datetime.now() @@ -646,7 +664,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): os.path.dirname(config_file))) # add config file as job auxFile - assembly_payloads[hash]["AuxFiles"] = [config_file] + assembly_job["AuxFiles"] = [config_file] with open(config_file, "w") as cf: print("TileCount={}".format(tiles_count), file=cf) @@ -670,7 +688,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): job_idx = 1 instance.data["assemblySubmissionJobs"] = [] - for _k, ass_job in assembly_payloads.items(): + for ass_job in assembly_payloads: self.log.info("submitting assembly job {} of {}".format( job_idx, len(assembly_payloads) )) @@ -679,7 +697,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): if not response.ok: raise Exception(response.text) - instance.data["assemblySubmissionJobs"].append(ass_job) + instance.data["assemblySubmissionJobs"].append( + response.json()["_id"]) job_idx += 1 instance.data["jobBatchName"] = payload["JobInfo"]["BatchName"] From a2c8e8a088b8964ed9e9c5873388ca462dcaef94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 19 Aug 2020 16:31:41 +0200 Subject: [PATCH 106/158] assembly job priority --- pype/plugins/maya/publish/submit_maya_deadline.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 5baea9d82b..d9ee7f9646 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -547,6 +547,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): } } assembly_payload["JobInfo"].update(output_filenames) + assembly_payload["JobInfo"]["Priority"] = self._instance.data.get( + "priority", 50) frame_payloads = [] assembly_payloads = [] From 4ef3d225e8711329c7c03b2b282ad6c57b11ece4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 09:57:34 +0200 Subject: [PATCH 107/158] disable editing of items in tasks and actions --- pype/tools/launcher/widgets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 7ab0a3f8ea..894dde3926 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -85,6 +85,7 @@ class ActionBar(QtWidgets.QWidget): view.setViewMode(QtWidgets.QListView.IconMode) view.setResizeMode(QtWidgets.QListView.Adjust) view.setSelectionMode(QtWidgets.QListView.NoSelection) + view.setEditTriggers(QtWidgets.QListView.NoEditTriggers) view.setWrapping(True) view.setGridSize(QtCore.QSize(70, 75)) view.setIconSize(QtCore.QSize(30, 30)) @@ -206,6 +207,7 @@ class TasksWidget(QtWidgets.QWidget): view = QtWidgets.QTreeView(self) view.setIndentation(0) + view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) model = TaskModel(self.dbcon) view.setModel(model) From 7868d8e0165cdf8e98ae61df1fb636b16ce5129a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 20 Aug 2020 13:22:17 +0200 Subject: [PATCH 108/158] allow thumbnails from single frame renders --- pype/plugins/global/publish/extract_jpeg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index ae74370b06..2ec97759db 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -48,7 +48,9 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): continue if not isinstance(repre['files'], (list, tuple)): - continue + input_file = repre['files'] + else: + input_file = repre['files'][0] stagingdir = os.path.normpath(repre.get("stagingDir")) input_file = repre['files'][0] From 5df17ed2939be3d780477527cd758630adeb0319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 20 Aug 2020 13:34:37 +0200 Subject: [PATCH 109/158] delete forgotten line --- pype/plugins/global/publish/extract_jpeg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 2ec97759db..333c2ec852 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -53,7 +53,6 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): input_file = repre['files'][0] stagingdir = os.path.normpath(repre.get("stagingDir")) - input_file = repre['files'][0] # input_file = ( # collections[0].format('{head}{padding}{tail}') % start From 3a69b9956d6d9ca82a99edb77c8ad52fa6b11084 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 15:15:10 +0200 Subject: [PATCH 110/158] moved resources to widgets folder --- .../{ => widgets}/resources/__init__.py | 0 .../{ => widgets}/resources/edit.svg | 0 .../{ => widgets}/resources/file.png | Bin .../{ => widgets}/resources/files.png | Bin .../{ => widgets}/resources/houdini.png | Bin .../{ => widgets}/resources/image_file.png | Bin .../{ => widgets}/resources/image_files.png | Bin .../{ => widgets}/resources/information.svg | 0 .../{ => widgets}/resources/maya.png | Bin .../{ => widgets}/resources/menu.png | Bin .../{ => widgets}/resources/menu_disabled.png | Bin .../{ => widgets}/resources/menu_hover.png | Bin .../{ => widgets}/resources/menu_pressed.png | Bin .../{ => widgets}/resources/menu_pressed_hover.png | Bin .../{ => widgets}/resources/nuke.png | Bin .../{ => widgets}/resources/premiere.png | Bin .../{ => widgets}/resources/trash.png | Bin .../{ => widgets}/resources/trash_disabled.png | Bin .../{ => widgets}/resources/trash_hover.png | Bin .../{ => widgets}/resources/trash_pressed.png | Bin .../{ => widgets}/resources/trash_pressed_hover.png | Bin .../{ => widgets}/resources/video_file.png | Bin 22 files changed, 0 insertions(+), 0 deletions(-) rename pype/tools/standalonepublish/{ => widgets}/resources/__init__.py (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/edit.svg (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/file.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/files.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/houdini.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/image_file.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/image_files.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/information.svg (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/maya.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/menu.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/menu_disabled.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/menu_hover.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/menu_pressed.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/menu_pressed_hover.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/nuke.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/premiere.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/trash.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/trash_disabled.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/trash_hover.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/trash_pressed.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/trash_pressed_hover.png (100%) rename pype/tools/standalonepublish/{ => widgets}/resources/video_file.png (100%) diff --git a/pype/tools/standalonepublish/resources/__init__.py b/pype/tools/standalonepublish/widgets/resources/__init__.py similarity index 100% rename from pype/tools/standalonepublish/resources/__init__.py rename to pype/tools/standalonepublish/widgets/resources/__init__.py diff --git a/pype/tools/standalonepublish/resources/edit.svg b/pype/tools/standalonepublish/widgets/resources/edit.svg similarity index 100% rename from pype/tools/standalonepublish/resources/edit.svg rename to pype/tools/standalonepublish/widgets/resources/edit.svg diff --git a/pype/tools/standalonepublish/resources/file.png b/pype/tools/standalonepublish/widgets/resources/file.png similarity index 100% rename from pype/tools/standalonepublish/resources/file.png rename to pype/tools/standalonepublish/widgets/resources/file.png diff --git a/pype/tools/standalonepublish/resources/files.png b/pype/tools/standalonepublish/widgets/resources/files.png similarity index 100% rename from pype/tools/standalonepublish/resources/files.png rename to pype/tools/standalonepublish/widgets/resources/files.png diff --git a/pype/tools/standalonepublish/resources/houdini.png b/pype/tools/standalonepublish/widgets/resources/houdini.png similarity index 100% rename from pype/tools/standalonepublish/resources/houdini.png rename to pype/tools/standalonepublish/widgets/resources/houdini.png diff --git a/pype/tools/standalonepublish/resources/image_file.png b/pype/tools/standalonepublish/widgets/resources/image_file.png similarity index 100% rename from pype/tools/standalonepublish/resources/image_file.png rename to pype/tools/standalonepublish/widgets/resources/image_file.png diff --git a/pype/tools/standalonepublish/resources/image_files.png b/pype/tools/standalonepublish/widgets/resources/image_files.png similarity index 100% rename from pype/tools/standalonepublish/resources/image_files.png rename to pype/tools/standalonepublish/widgets/resources/image_files.png diff --git a/pype/tools/standalonepublish/resources/information.svg b/pype/tools/standalonepublish/widgets/resources/information.svg similarity index 100% rename from pype/tools/standalonepublish/resources/information.svg rename to pype/tools/standalonepublish/widgets/resources/information.svg diff --git a/pype/tools/standalonepublish/resources/maya.png b/pype/tools/standalonepublish/widgets/resources/maya.png similarity index 100% rename from pype/tools/standalonepublish/resources/maya.png rename to pype/tools/standalonepublish/widgets/resources/maya.png diff --git a/pype/tools/standalonepublish/resources/menu.png b/pype/tools/standalonepublish/widgets/resources/menu.png similarity index 100% rename from pype/tools/standalonepublish/resources/menu.png rename to pype/tools/standalonepublish/widgets/resources/menu.png diff --git a/pype/tools/standalonepublish/resources/menu_disabled.png b/pype/tools/standalonepublish/widgets/resources/menu_disabled.png similarity index 100% rename from pype/tools/standalonepublish/resources/menu_disabled.png rename to pype/tools/standalonepublish/widgets/resources/menu_disabled.png diff --git a/pype/tools/standalonepublish/resources/menu_hover.png b/pype/tools/standalonepublish/widgets/resources/menu_hover.png similarity index 100% rename from pype/tools/standalonepublish/resources/menu_hover.png rename to pype/tools/standalonepublish/widgets/resources/menu_hover.png diff --git a/pype/tools/standalonepublish/resources/menu_pressed.png b/pype/tools/standalonepublish/widgets/resources/menu_pressed.png similarity index 100% rename from pype/tools/standalonepublish/resources/menu_pressed.png rename to pype/tools/standalonepublish/widgets/resources/menu_pressed.png diff --git a/pype/tools/standalonepublish/resources/menu_pressed_hover.png b/pype/tools/standalonepublish/widgets/resources/menu_pressed_hover.png similarity index 100% rename from pype/tools/standalonepublish/resources/menu_pressed_hover.png rename to pype/tools/standalonepublish/widgets/resources/menu_pressed_hover.png diff --git a/pype/tools/standalonepublish/resources/nuke.png b/pype/tools/standalonepublish/widgets/resources/nuke.png similarity index 100% rename from pype/tools/standalonepublish/resources/nuke.png rename to pype/tools/standalonepublish/widgets/resources/nuke.png diff --git a/pype/tools/standalonepublish/resources/premiere.png b/pype/tools/standalonepublish/widgets/resources/premiere.png similarity index 100% rename from pype/tools/standalonepublish/resources/premiere.png rename to pype/tools/standalonepublish/widgets/resources/premiere.png diff --git a/pype/tools/standalonepublish/resources/trash.png b/pype/tools/standalonepublish/widgets/resources/trash.png similarity index 100% rename from pype/tools/standalonepublish/resources/trash.png rename to pype/tools/standalonepublish/widgets/resources/trash.png diff --git a/pype/tools/standalonepublish/resources/trash_disabled.png b/pype/tools/standalonepublish/widgets/resources/trash_disabled.png similarity index 100% rename from pype/tools/standalonepublish/resources/trash_disabled.png rename to pype/tools/standalonepublish/widgets/resources/trash_disabled.png diff --git a/pype/tools/standalonepublish/resources/trash_hover.png b/pype/tools/standalonepublish/widgets/resources/trash_hover.png similarity index 100% rename from pype/tools/standalonepublish/resources/trash_hover.png rename to pype/tools/standalonepublish/widgets/resources/trash_hover.png diff --git a/pype/tools/standalonepublish/resources/trash_pressed.png b/pype/tools/standalonepublish/widgets/resources/trash_pressed.png similarity index 100% rename from pype/tools/standalonepublish/resources/trash_pressed.png rename to pype/tools/standalonepublish/widgets/resources/trash_pressed.png diff --git a/pype/tools/standalonepublish/resources/trash_pressed_hover.png b/pype/tools/standalonepublish/widgets/resources/trash_pressed_hover.png similarity index 100% rename from pype/tools/standalonepublish/resources/trash_pressed_hover.png rename to pype/tools/standalonepublish/widgets/resources/trash_pressed_hover.png diff --git a/pype/tools/standalonepublish/resources/video_file.png b/pype/tools/standalonepublish/widgets/resources/video_file.png similarity index 100% rename from pype/tools/standalonepublish/resources/video_file.png rename to pype/tools/standalonepublish/widgets/resources/video_file.png From 1e101c1fbc408e7fe77809dca3385b14e16fdea6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 15:15:28 +0200 Subject: [PATCH 111/158] PngFactory do not create Qt object on init --- .../widgets/widget_component_item.py | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/pype/tools/standalonepublish/widgets/widget_component_item.py b/pype/tools/standalonepublish/widgets/widget_component_item.py index dd838075e3..3850d68b96 100644 --- a/pype/tools/standalonepublish/widgets/widget_component_item.py +++ b/pype/tools/standalonepublish/widgets/widget_component_item.py @@ -1,6 +1,6 @@ import os from Qt import QtCore, QtGui, QtWidgets -from pype.resources import get_resource +from .resources import get_resource from avalon import style @@ -353,27 +353,37 @@ class LightingButton(QtWidgets.QPushButton): class PngFactory: - png_names = { - "trash": { - "normal": QtGui.QIcon(get_resource("trash.png")), - "hover": QtGui.QIcon(get_resource("trash_hover.png")), - "pressed": QtGui.QIcon(get_resource("trash_pressed.png")), - "pressed_hover": QtGui.QIcon( - get_resource("trash_pressed_hover.png") - ), - "disabled": QtGui.QIcon(get_resource("trash_disabled.png")) - }, + png_names = None - "menu": { - "normal": QtGui.QIcon(get_resource("menu.png")), - "hover": QtGui.QIcon(get_resource("menu_hover.png")), - "pressed": QtGui.QIcon(get_resource("menu_pressed.png")), - "pressed_hover": QtGui.QIcon( - get_resource("menu_pressed_hover.png") - ), - "disabled": QtGui.QIcon(get_resource("menu_disabled.png")) + @classmethod + def init(cls): + cls.png_names = { + "trash": { + "normal": QtGui.QIcon(get_resource("trash.png")), + "hover": QtGui.QIcon(get_resource("trash_hover.png")), + "pressed": QtGui.QIcon(get_resource("trash_pressed.png")), + "pressed_hover": QtGui.QIcon( + get_resource("trash_pressed_hover.png") + ), + "disabled": QtGui.QIcon(get_resource("trash_disabled.png")) + }, + + "menu": { + "normal": QtGui.QIcon(get_resource("menu.png")), + "hover": QtGui.QIcon(get_resource("menu_hover.png")), + "pressed": QtGui.QIcon(get_resource("menu_pressed.png")), + "pressed_hover": QtGui.QIcon( + get_resource("menu_pressed_hover.png") + ), + "disabled": QtGui.QIcon(get_resource("menu_disabled.png")) + } } - } + + @classmethod + def get_png(cls, name): + if cls.png_names is None: + cls.init() + return cls.png_names.get(name) class PngButton(QtWidgets.QPushButton): @@ -406,7 +416,7 @@ class PngButton(QtWidgets.QPushButton): png_dict = {} if name: - png_dict = PngFactory.png_names.get(name) or {} + png_dict = PngFactory.get_png(name) or {} if not png_dict: print(( "WARNING: There is not set icon with name \"{}\"" From 4c6e5fefb415eab87ee52fd3b0bceb416f0643bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 16:05:03 +0200 Subject: [PATCH 112/158] implemented action for pushing frameStart and frameEnd values to task specific custom attributes --- .../action_push_frame_values_to_task.py | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 pype/modules/ftrack/actions/action_push_frame_values_to_task.py diff --git a/pype/modules/ftrack/actions/action_push_frame_values_to_task.py b/pype/modules/ftrack/actions/action_push_frame_values_to_task.py new file mode 100644 index 0000000000..3037695452 --- /dev/null +++ b/pype/modules/ftrack/actions/action_push_frame_values_to_task.py @@ -0,0 +1,245 @@ +import json +import collections +import ftrack_api +from pype.modules.ftrack.lib import BaseAction, statics_icon + + +class PushFrameValuesToTaskAction(BaseAction): + """Action for testing purpose or as base for new actions.""" + + identifier = "admin.push_frame_values_to_task" + label = "Pype Admin" + variant = "- Push Frame values to Task" + role_list = ["Pypeclub", "Administrator", "Project Manager"] + icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") + + entities_query = ( + "select id, name, parent_id, link" + " from TypedContext where project_id is \"{}\"" + ) + cust_attrs_query = ( + "select id, key, object_type_id, is_hierarchical, default" + " from CustomAttributeConfiguration" + " where key in ({})" + ) + cust_attr_value_query = ( + "select value, entity_id from CustomAttributeValue" + " where entity_id in ({}) and configuration_id in ({})" + ) + custom_attribute_keys = ["frameStart", "frameEnd"] + + def discover(self, session, entities, event): + return True + + def launch(self, session, entities, event): + task_attrs_by_key, hier_attrs = self.frame_attributes(session) + missing_keys = [ + key + for key in self.custom_attribute_keys + if key not in task_attrs_by_key + ] + if missing_keys: + if len(missing_keys) == 1: + sub_msg = " \"{}\"".format(missing_keys[0]) + else: + sub_msg = "s {}".format(", ".join([ + "\"{}\"".format(key) + for key in missing_keys + ])) + + msg = "Missing Task's custom attribute{}.".format(sub_msg) + self.log.warning(msg) + return { + "success": False, + "message": msg + } + + self.log.debug("{}: Creating job".format(self.label)) + + user_entity = session.query( + "User where id is {}".format(event["source"]["user"]["id"]) + ).one() + job = session.create("Job", { + "user": user_entity, + "status": "running", + "data": json.dumps({ + "description": "Propagation of Frame attribute values to task." + }) + }) + session.commit() + + try: + project_entity = self.get_project_from_entity(entities[0]) + result = self.propagate_values( + session, + tuple(task_attrs_by_key.values()), + hier_attrs, + project_entity + ) + job["status"] = "done" + session.commit() + + return result + + except Exception: + session.rollback() + job["status"] = "failed" + session.commit() + + msg = "Pushing Custom attribute values to task Failed" + self.log.warning(msg, exc_info=True) + return { + "success": False, + "message": msg + } + + finally: + if job["status"] == "running": + job["status"] = "failed" + session.commit() + + def frame_attributes(self, session): + task_object_type = session.query( + "ObjectType where name is \"Task\"" + ).one() + + attr_names = self.custom_attribute_keys + if isinstance(attr_names, str): + attr_names = [attr_names] + + joined_keys = ",".join([ + "\"{}\"".format(attr_name) + for attr_name in attr_names + ]) + + attribute_entities = session.query( + self.cust_attrs_query.format(joined_keys) + ).all() + + hier_attrs = [] + task_attrs = {} + for attr in attribute_entities: + attr_key = attr["key"] + if attr["is_hierarchical"]: + hier_attrs.append(attr) + elif attr["object_type_id"] == task_object_type["id"]: + task_attrs[attr_key] = attr + return task_attrs, hier_attrs + + def join_keys(self, items): + return ",".join(["\"{}\"".format(item) for item in items]) + + def propagate_values( + self, session, task_attrs, hier_attrs, project_entity + ): + self.log.debug("Querying project's entities \"{}\".".format( + project_entity["full_name"] + )) + entities = session.query( + self.entities_query.format(project_entity["id"]) + ).all() + + self.log.debug("Filtering Task entities.") + task_entities_by_parent_id = collections.defaultdict(list) + for entity in entities: + if entity.entity_type.lower() == "task": + task_entities_by_parent_id[entity["parent_id"]].append(entity) + + self.log.debug("Getting Custom attribute values from tasks' parents.") + hier_values_by_entity_id = self.get_hier_values( + session, + hier_attrs, + list(task_entities_by_parent_id.keys()) + ) + + self.log.debug("Setting parents' values to task.") + self.set_task_attr_values( + session, + task_entities_by_parent_id, + hier_values_by_entity_id, + task_attrs + ) + + return True + + def get_hier_values(self, session, hier_attrs, focus_entity_ids): + joined_entity_ids = self.join_keys(focus_entity_ids) + hier_attr_ids = self.join_keys( + tuple(hier_attr["id"] for hier_attr in hier_attrs) + ) + hier_attrs_key_by_id = { + hier_attr["id"]: hier_attr["key"] + for hier_attr in hier_attrs + } + call_expr = [{ + "action": "query", + "expression": self.cust_attr_value_query.format( + joined_entity_ids, hier_attr_ids + ) + }] + if hasattr(session, "call"): + [values] = session.call(call_expr) + else: + [values] = session._call(call_expr) + + values_per_entity_id = {} + for item in values["data"]: + entity_id = item["entity_id"] + key = hier_attrs_key_by_id[item["configuration_id"]] + + if entity_id not in values_per_entity_id: + values_per_entity_id[entity_id] = {} + value = item["value"] + if value is not None: + values_per_entity_id[entity_id][key] = value + + output = {} + for entity_id in focus_entity_ids: + value = values_per_entity_id.get(entity_id) + if value: + output[entity_id] = value + + return output + + def set_task_attr_values( + self, + session, + task_entities_by_parent_id, + hier_values_by_entity_id, + task_attrs + ): + task_attr_ids_by_key = { + attr["key"]: attr["id"] + for attr in task_attrs + } + + total_parents = len(hier_values_by_entity_id) + idx = 1 + for parent_id, values in hier_values_by_entity_id.items(): + self.log.info(( + "[{}/{}] {} Processing values to children. Values: {}" + ).format(idx, total_parents, parent_id, values)) + + task_entities = task_entities_by_parent_id[parent_id] + for key, value in values.items(): + for task_entity in task_entities: + _entity_key = collections.OrderedDict({ + "configuration_id": task_attr_ids_by_key[key], + "entity_id": task_entity["id"] + }) + + session.recorded_operations.push( + ftrack_api.operation.UpdateEntityOperation( + "ContextCustomAttributeValue", + _entity_key, + "value", + ftrack_api.symbol.NOT_SET, + value + ) + ) + session.commit() + idx += 1 + + +def register(session, plugins_presets={}): + PushFrameValuesToTask(session, plugins_presets).register() From 1431930e6af89df656cced3572598907d13525ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 16:05:13 +0200 Subject: [PATCH 113/158] implemented event handler for pushing frameStart and frameEnd values to task specific custom attributes --- .../events/event_push_frame_values_to_task.py | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 pype/modules/ftrack/events/event_push_frame_values_to_task.py diff --git a/pype/modules/ftrack/events/event_push_frame_values_to_task.py b/pype/modules/ftrack/events/event_push_frame_values_to_task.py new file mode 100644 index 0000000000..dd5c5911ec --- /dev/null +++ b/pype/modules/ftrack/events/event_push_frame_values_to_task.py @@ -0,0 +1,148 @@ +import collections +import ftrack_api +from pype.modules.ftrack import BaseEvent + + +class PushFrameValuesToTaskEvent(BaseEvent): + """Action for testing purpose or as base for new actions.""" + cust_attrs_query = ( + "select id, key, object_type_id, is_hierarchical, default" + " from CustomAttributeConfiguration" + " where key in ({}) and object_type_id = {}" + ) + + # Ignore event handler by default + ignore_me = True + + interest_attributes = ["frameStart", "frameEnd"] + _cached_task_object_id = None + + @classmethod + def task_object_id(cls, session): + if cls._cached_task_object_id is None: + task_object_type = session.query( + "ObjectType where name is \"Task\"" + ).one() + cls._cached_task_object_id = task_object_type["id"] + return cls._cached_task_object_id + + def extract_interesting_data(self, session, event): + # Filter if event contain relevant data + entities_info = event["data"].get("entities") + if not entities_info: + return + + interesting_data = {} + for entity_info in entities_info: + # Care only about tasks + if entity_info.get("entityType") != "task": + continue + + # Care only about changes of status + changes = entity_info.get("changes") or {} + if not changes: + continue + + # Care only about changes if specific keys + entity_changes = {} + for key in self.interest_attributes: + if key in changes: + entity_changes[key] = changes[key]["new"] + + if not entity_changes: + continue + + # Do not care about "Task" entity_type + task_object_id = self.task_object_id(session) + if entity_info.get("objectTypeId") == task_object_id: + continue + + interesting_data[entity_info["entityId"]] = entity_changes + return interesting_data + + def join_keys(self, keys): + return ",".join(["\"{}\"".format(key) for key in keys]) + + def get_task_entities(self, session, entities_info): + return session.query( + "Task where parent_id in ({})".format( + self.join_keys(entities_info.keys()) + ) + ).all() + + def task_attrs(self, session): + return session.query(self.cust_attrs_query.format( + self.join_keys(self.interest_attributes), + self.task_object_id(session) + )).all() + + def launch(self, session, event): + interesting_data = self.extract_interesting_data(session, event) + if not interesting_data: + return + + task_entities = self.get_task_entities(session, interesting_data) + if not task_entities: + return + + task_attrs = self.task_attrs(session) + if not task_attrs: + self.log.warning(( + "There is not created Custom Attributes {}" + " for \"Task\" entity type." + ).format(self.join_keys(self.interest_attributes))) + return + + task_attr_id_by_key = { + attr["key"]: attr["id"] + for attr in task_attrs + } + task_entities_by_parent_id = collections.defaultdict(list) + for task_entity in task_entities: + task_entities_by_parent_id[task_entity["parent_id"]].append( + task_entity + ) + + for parent_id, values in interesting_data.items(): + task_entities = task_entities_by_parent_id[parent_id] + for key, value in values.items(): + changed_ids = [] + for task_entity in task_entities: + task_id = task_entity["id"] + changed_ids.append(task_id) + + entity_key = collections.OrderedDict({ + "configuration_id": task_attr_id_by_key[key], + "entity_id": task_id + }) + if value is None: + op = ftrack_api.operation.DeleteEntityOperation( + "CustomAttributeValue", + entity_key + ) + else: + op = ftrack_api.operation.UpdateEntityOperation( + "ContextCustomAttributeValue", + entity_key, + "value", + ftrack_api.symbol.NOT_SET, + value + ) + + session.recorded_operations.push(op) + self.log.info(( + "Changing Custom Attribute \"{}\" to value" + " \"{}\" on entities: {}" + ).format(key, value, self.join_keys(changed_ids))) + try: + session.commit() + except Exception: + session.rollback() + self.log.warning( + "Changing of values failed.", + exc_info=True + ) + + +def register(session, plugins_presets): + PushFrameValuesToTaskEvent(session, plugins_presets).register() From 516fafbfec822e8b5f2957bdd108f157412a5963 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 16:39:16 +0200 Subject: [PATCH 114/158] moved action to server --- .../{actions => events}/action_push_frame_values_to_task.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pype/modules/ftrack/{actions => events}/action_push_frame_values_to_task.py (100%) diff --git a/pype/modules/ftrack/actions/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py similarity index 100% rename from pype/modules/ftrack/actions/action_push_frame_values_to_task.py rename to pype/modules/ftrack/events/action_push_frame_values_to_task.py From 8ae527a154c24d0b0e5634546d759f421f0c0426 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 16:39:47 +0200 Subject: [PATCH 115/158] action converted to server action --- .../action_push_frame_values_to_task.py | 64 ++++++++++++++----- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py index 3037695452..bd036411ac 100644 --- a/pype/modules/ftrack/events/action_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py @@ -1,7 +1,7 @@ import json import collections import ftrack_api -from pype.modules.ftrack.lib import BaseAction, statics_icon +from pype.modules.ftrack.lib import BaseAction class PushFrameValuesToTaskAction(BaseAction): @@ -10,8 +10,6 @@ class PushFrameValuesToTaskAction(BaseAction): identifier = "admin.push_frame_values_to_task" label = "Pype Admin" variant = "- Push Frame values to Task" - role_list = ["Pypeclub", "Administrator", "Project Manager"] - icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") entities_query = ( "select id, name, parent_id, link" @@ -26,12 +24,56 @@ class PushFrameValuesToTaskAction(BaseAction): "select value, entity_id from CustomAttributeValue" " where entity_id in ({}) and configuration_id in ({})" ) - custom_attribute_keys = ["frameStart", "frameEnd"] + custom_attribute_keys = {"frameStart", "frameEnd"} + discover_role_list = {"Pypeclub", "Administrator", "Project Manager"} + + def register(self): + modified_role_names = set() + for role_name in self.discover_role_list: + modified_role_names.add(role_name.lower()) + self.discover_role_list = modified_role_names + + self.session.event_hub.subscribe( + "topic=ftrack.action.discover", + self._discover, + priority=self.priority + ) + + launch_subscription = ( + "topic=ftrack.action.launch and data.actionIdentifier={0}" + ).format(self.identifier) + self.session.event_hub.subscribe(launch_subscription, self._launch) def discover(self, session, entities, event): - return True + """ Validation """ + # Check if selection is valid + valid_selection = False + for ent in event["data"]["selection"]: + # Ignore entities that are not tasks or projects + if ent["entityType"].lower() in ["show", "task"]: + valid_selection = True + break + + if not valid_selection: + return False + + # Get user and check his roles + user_id = event.get("source", {}).get("user", {}).get("id") + if not user_id: + return False + + user = session.query("User where id is \"{}\"".format(user_id)).first() + if not user: + return False + + for role in user["user_security_roles"]: + lowered_role = role["security_role"]["name"].lower() + if lowered_role in self.discover_role_list: + return True + return False def launch(self, session, entities, event): + # TODO this can be threaded task_attrs_by_key, hier_attrs = self.frame_attributes(session) missing_keys = [ key @@ -103,15 +145,7 @@ class PushFrameValuesToTaskAction(BaseAction): "ObjectType where name is \"Task\"" ).one() - attr_names = self.custom_attribute_keys - if isinstance(attr_names, str): - attr_names = [attr_names] - - joined_keys = ",".join([ - "\"{}\"".format(attr_name) - for attr_name in attr_names - ]) - + joined_keys = self.join_keys(self.custom_attribute_keys) attribute_entities = session.query( self.cust_attrs_query.format(joined_keys) ).all() @@ -242,4 +276,4 @@ class PushFrameValuesToTaskAction(BaseAction): def register(session, plugins_presets={}): - PushFrameValuesToTask(session, plugins_presets).register() + PushFrameValuesToTaskAction(session, plugins_presets).register() From 63c2cf6a41ca8d66cfffca914d6933c28d32b920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 20 Aug 2020 16:39:57 +0200 Subject: [PATCH 116/158] support for different tile order in vray --- pype/plugins/maya/publish/submit_maya_deadline.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index d9ee7f9646..747d2727b7 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -65,7 +65,9 @@ payload_skeleton = { } -def _format_tiles(filename, index, tiles_x, tiles_y, width, height, prefix): +def _format_tiles( + filename, index, tiles_x, tiles_y, + width, height, prefix, origin="blc"): """Generate tile entries for Deadline tile job. Returns two dictionaries - one that can be directly used in Deadline @@ -142,7 +144,11 @@ def _format_tiles(filename, index, tiles_x, tiles_y, width, height, prefix): cfg["Tile{}".format(tile)] = new_filename cfg["Tile{}Tile".format(tile)] = new_filename cfg["Tile{}X".format(tile)] = (tile_x - 1) * w_space - cfg["Tile{}Y".format(tile)] = (tile_y - 1) * h_space + if origin == "blc": + cfg["Tile{}Y".format(tile)] = (tile_y - 1) * h_space + else: + cfg["Tile{}Y".format(tile)] = int(height) - ((tile_y - 1) * h_space) # noqa: E501 + cfg["Tile{}Width".format(tile)] = tile_x * w_space cfg["Tile{}Height".format(tile)] = tile_y * h_space @@ -549,6 +555,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): assembly_payload["JobInfo"].update(output_filenames) assembly_payload["JobInfo"]["Priority"] = self._instance.data.get( "priority", 50) + assembly_payload["JobInfo"]["UserName"] = deadline_user frame_payloads = [] assembly_payloads = [] @@ -620,6 +627,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): REPL_FRAME_NUMBER, "\\1{}\\3".format("#" * len(frame)), file) + new_assembly_payload["PluginInfo"]["Renderer"] = self._instance.data["renderer"] # noqa: E501 new_assembly_payload["JobInfo"]["ExtraInfo0"] = frame_jobs[frame] # noqa: E501 new_assembly_payload["JobInfo"]["ExtraInfo1"] = file assembly_payloads.append(new_assembly_payload) From a37da37bd15c6e5aca9c37ea56a57eb163359f11 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 16:42:56 +0200 Subject: [PATCH 117/158] commit all changes at once --- .../modules/ftrack/events/action_push_frame_values_to_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py index bd036411ac..4f0c7ffeb7 100644 --- a/pype/modules/ftrack/events/action_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py @@ -253,6 +253,7 @@ class PushFrameValuesToTaskAction(BaseAction): self.log.info(( "[{}/{}] {} Processing values to children. Values: {}" ).format(idx, total_parents, parent_id, values)) + idx += 1 task_entities = task_entities_by_parent_id[parent_id] for key, value in values.items(): @@ -271,8 +272,7 @@ class PushFrameValuesToTaskAction(BaseAction): value ) ) - session.commit() - idx += 1 + session.commit() def register(session, plugins_presets={}): From 97b42d8703150038feb6833832c9da0d39df40ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 16:46:35 +0200 Subject: [PATCH 118/158] ignore action by default --- pype/modules/ftrack/events/action_push_frame_values_to_task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py index 4f0c7ffeb7..5b7da8bebb 100644 --- a/pype/modules/ftrack/events/action_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py @@ -7,6 +7,9 @@ from pype.modules.ftrack.lib import BaseAction class PushFrameValuesToTaskAction(BaseAction): """Action for testing purpose or as base for new actions.""" + # Ignore event handler by default + ignore_me = True + identifier = "admin.push_frame_values_to_task" label = "Pype Admin" variant = "- Push Frame values to Task" From 07b34dec926f0135a03327d4c52aa2d2837e5199 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 17:45:59 +0200 Subject: [PATCH 119/158] show only on project --- pype/modules/ftrack/events/action_push_frame_values_to_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py index 5b7da8bebb..e6276d84ac 100644 --- a/pype/modules/ftrack/events/action_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py @@ -53,7 +53,7 @@ class PushFrameValuesToTaskAction(BaseAction): valid_selection = False for ent in event["data"]["selection"]: # Ignore entities that are not tasks or projects - if ent["entityType"].lower() in ["show", "task"]: + if ent["entityType"].lower() == "show": valid_selection = True break From fb6de46cd6c186934cd878c6e055dadf86c89d83 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 19:05:56 +0200 Subject: [PATCH 120/158] pushing is also pushing to item itself --- .../action_push_frame_values_to_task.py | 279 ++++++++++++++---- 1 file changed, 221 insertions(+), 58 deletions(-) diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py index e6276d84ac..d88f4a1016 100644 --- a/pype/modules/ftrack/events/action_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py @@ -15,8 +15,8 @@ class PushFrameValuesToTaskAction(BaseAction): variant = "- Push Frame values to Task" entities_query = ( - "select id, name, parent_id, link" - " from TypedContext where project_id is \"{}\"" + "select id, name, parent_id, link from TypedContext" + " where project_id is \"{}\" and object_type_id in ({})" ) cust_attrs_query = ( "select id, key, object_type_id, is_hierarchical, default" @@ -27,7 +27,13 @@ class PushFrameValuesToTaskAction(BaseAction): "select value, entity_id from CustomAttributeValue" " where entity_id in ({}) and configuration_id in ({})" ) - custom_attribute_keys = {"frameStart", "frameEnd"} + + pushing_entity_types = {"Shot"} + hierarchical_custom_attribute_keys = {"frameStart", "frameEnd"} + custom_attribute_mapping = { + "frameStart": "fstart", + "frameEnd": "fend" + } discover_role_list = {"Pypeclub", "Administrator", "Project Manager"} def register(self): @@ -76,29 +82,6 @@ class PushFrameValuesToTaskAction(BaseAction): return False def launch(self, session, entities, event): - # TODO this can be threaded - task_attrs_by_key, hier_attrs = self.frame_attributes(session) - missing_keys = [ - key - for key in self.custom_attribute_keys - if key not in task_attrs_by_key - ] - if missing_keys: - if len(missing_keys) == 1: - sub_msg = " \"{}\"".format(missing_keys[0]) - else: - sub_msg = "s {}".format(", ".join([ - "\"{}\"".format(key) - for key in missing_keys - ])) - - msg = "Missing Task's custom attribute{}.".format(sub_msg) - self.log.warning(msg) - return { - "success": False, - "message": msg - } - self.log.debug("{}: Creating job".format(self.label)) user_entity = session.query( @@ -115,12 +98,7 @@ class PushFrameValuesToTaskAction(BaseAction): try: project_entity = self.get_project_from_entity(entities[0]) - result = self.propagate_values( - session, - tuple(task_attrs_by_key.values()), - hier_attrs, - project_entity - ) + result = self.propagate_values(session, project_entity, event) job["status"] = "done" session.commit() @@ -143,12 +121,20 @@ class PushFrameValuesToTaskAction(BaseAction): job["status"] = "failed" session.commit() - def frame_attributes(self, session): + def task_attributes(self, session): task_object_type = session.query( "ObjectType where name is \"Task\"" ).one() - joined_keys = self.join_keys(self.custom_attribute_keys) + hier_attr_names = list( + self.custom_attribute_mapping.keys() + ) + entity_type_specific_names = list( + self.custom_attribute_mapping.values() + ) + joined_keys = self.join_keys( + hier_attr_names + entity_type_specific_names + ) attribute_entities = session.query( self.cust_attrs_query.format(joined_keys) ).all() @@ -158,47 +144,139 @@ class PushFrameValuesToTaskAction(BaseAction): for attr in attribute_entities: attr_key = attr["key"] if attr["is_hierarchical"]: - hier_attrs.append(attr) + if attr_key in hier_attr_names: + hier_attrs.append(attr) elif attr["object_type_id"] == task_object_type["id"]: - task_attrs[attr_key] = attr + if attr_key in entity_type_specific_names: + task_attrs[attr_key] = attr["id"] return task_attrs, hier_attrs def join_keys(self, items): return ",".join(["\"{}\"".format(item) for item in items]) - def propagate_values( - self, session, task_attrs, hier_attrs, project_entity - ): + def propagate_values(self, session, project_entity, event): self.log.debug("Querying project's entities \"{}\".".format( project_entity["full_name"] )) - entities = session.query( - self.entities_query.format(project_entity["id"]) - ).all() + pushing_entity_types = tuple( + ent_type.lower() + for ent_type in self.pushing_entity_types + ) + destination_object_types = [] + all_object_types = session.query("ObjectType").all() + for object_type in all_object_types: + lowered_name = object_type["name"].lower() + if ( + lowered_name == "task" + or lowered_name in pushing_entity_types + ): + destination_object_types.append(object_type) + + destination_object_type_ids = tuple( + obj_type["id"] + for obj_type in destination_object_types + ) + entities = session.query(self.entities_query.format( + project_entity["id"], + self.join_keys(destination_object_type_ids) + )).all() + + entities_by_id = { + entity["id"]: entity + for entity in entities + } self.log.debug("Filtering Task entities.") task_entities_by_parent_id = collections.defaultdict(list) + non_task_entities = [] + non_task_entity_ids = [] for entity in entities: - if entity.entity_type.lower() == "task": - task_entities_by_parent_id[entity["parent_id"]].append(entity) + if entity.entity_type.lower() != "task": + non_task_entities.append(entity) + non_task_entity_ids.append(entity["id"]) + continue + + parent_id = entity["parent_id"] + if parent_id in entities_by_id: + task_entities_by_parent_id[parent_id].append(entity) + + task_attr_id_by_keys, hier_attrs = self.task_attributes(session) self.log.debug("Getting Custom attribute values from tasks' parents.") hier_values_by_entity_id = self.get_hier_values( session, hier_attrs, - list(task_entities_by_parent_id.keys()) + non_task_entity_ids ) self.log.debug("Setting parents' values to task.") - self.set_task_attr_values( + task_missing_keys = self.set_task_attr_values( session, task_entities_by_parent_id, hier_values_by_entity_id, - task_attrs + task_attr_id_by_keys ) + self.log.debug("Setting values to entities themselves.") + missing_keys_by_object_name = self.push_values_to_entities( + session, + non_task_entities, + hier_values_by_entity_id + ) + if task_missing_keys: + missing_keys_by_object_name["Task"] = task_missing_keys + if missing_keys_by_object_name: + self.report(missing_keys_by_object_name, event) return True + def report(self, missing_keys_by_object_name, event): + splitter = {"type": "label", "value": "---"} + + title = "Push Custom Attribute values report:" + + items = [] + items.append({ + "type": "label", + "value": "# Pushing values was not complete" + }) + items.append({ + "type": "label", + "value": ( + "

It was due to missing custom" + " attribute configurations for specific entity type/s." + " These configurations are not created automatically.

" + ) + }) + + log_message_items = [] + log_message_item_template = ( + "Entity type \"{}\" does not have created Custom Attribute/s: {}" + ) + for object_name, missing_attr_names in ( + missing_keys_by_object_name.items() + ): + log_message_items.append(log_message_item_template.format( + object_name, self.join_keys(missing_attr_names) + )) + + items.append(splitter) + items.append({ + "type": "label", + "value": "## Entity type: {}".format(object_name) + }) + + items.append({ + "type": "label", + "value": "

{}

".format("
".join(missing_attr_names)) + }) + + self.log.warning(( + "Couldn't finish pushing attribute values because" + " few entity types miss Custom attribute configurations:\n{}" + ).format("\n".join(log_message_items))) + + self.show_interface(items, title, event) + def get_hier_values(self, session, hier_attrs, focus_entity_ids): joined_entity_ids = self.join_keys(focus_entity_ids) hier_attr_ids = self.join_keys( @@ -243,26 +321,28 @@ class PushFrameValuesToTaskAction(BaseAction): session, task_entities_by_parent_id, hier_values_by_entity_id, - task_attrs + task_attr_id_by_keys ): - task_attr_ids_by_key = { - attr["key"]: attr["id"] - for attr in task_attrs - } + missing_keys = set() total_parents = len(hier_values_by_entity_id) - idx = 1 + idx = 0 for parent_id, values in hier_values_by_entity_id.items(): - self.log.info(( - "[{}/{}] {} Processing values to children. Values: {}" - ).format(idx, total_parents, parent_id, values)) idx += 1 + self.log.info(( + "[{}/{}] {} Processing values to task. Values: {}" + ).format(idx, total_parents, parent_id, values)) task_entities = task_entities_by_parent_id[parent_id] - for key, value in values.items(): + for hier_key, value in values.items(): + key = self.custom_attribute_mapping[hier_key] + if key not in task_attr_id_by_keys: + missing_keys.add(key) + continue + for task_entity in task_entities: _entity_key = collections.OrderedDict({ - "configuration_id": task_attr_ids_by_key[key], + "configuration_id": task_attr_id_by_keys[key], "entity_id": task_entity["id"] }) @@ -277,6 +357,89 @@ class PushFrameValuesToTaskAction(BaseAction): ) session.commit() + return missing_keys + + def push_values_to_entities( + self, + session, + non_task_entities, + hier_values_by_entity_id + ): + object_types = session.query( + "ObjectType where name in ({})".format( + self.join_keys(self.pushing_entity_types) + ) + ).all() + object_type_names_by_id = { + object_type["id"]: object_type["name"] + for object_type in object_types + } + joined_keys = self.join_keys( + self.custom_attribute_mapping.values() + ) + attribute_entities = session.query( + self.cust_attrs_query.format(joined_keys) + ).all() + + attrs_by_obj_id = {} + for attr in attribute_entities: + if attr["is_hierarchical"]: + continue + + obj_id = attr["object_type_id"] + if obj_id not in object_type_names_by_id: + continue + + if obj_id not in attrs_by_obj_id: + attrs_by_obj_id[obj_id] = {} + + attr_key = attr["key"] + attrs_by_obj_id[obj_id][attr_key] = attr["id"] + + entities_by_obj_id = collections.defaultdict(list) + for entity in non_task_entities: + entities_by_obj_id[entity["object_type_id"]].append(entity) + + missing_keys_by_object_id = collections.defaultdict(set) + for obj_type_id, attr_keys in attrs_by_obj_id.items(): + entities = entities_by_obj_id.get(obj_type_id) + if not entities: + continue + + for entity in entities: + values = hier_values_by_entity_id.get(entity["id"]) + if not values: + continue + + for hier_key, value in values.items(): + key = self.custom_attribute_mapping[hier_key] + if key not in attr_keys: + missing_keys_by_object_id[obj_type_id].add(key) + continue + + _entity_key = collections.OrderedDict({ + "configuration_id": attr_keys[key], + "entity_id": entity["id"] + }) + + session.recorded_operations.push( + ftrack_api.operation.UpdateEntityOperation( + "ContextCustomAttributeValue", + _entity_key, + "value", + ftrack_api.symbol.NOT_SET, + value + ) + ) + session.commit() + + missing_keys_by_object_name = {} + for obj_id, missing_keys in missing_keys_by_object_id.items(): + obj_name = object_type_names_by_id[obj_id] + missing_keys_by_object_name[obj_name] = missing_keys + + return missing_keys_by_object_name + def register(session, plugins_presets={}): PushFrameValuesToTaskAction(session, plugins_presets).register() From 044434b35205f10e0d7d415d7d32ecefdbd65257 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 19:32:36 +0200 Subject: [PATCH 121/158] event handle the same way as action --- .../events/event_push_frame_values_to_task.py | 145 ++++++++++++++---- 1 file changed, 114 insertions(+), 31 deletions(-) diff --git a/pype/modules/ftrack/events/event_push_frame_values_to_task.py b/pype/modules/ftrack/events/event_push_frame_values_to_task.py index dd5c5911ec..dd24110c1b 100644 --- a/pype/modules/ftrack/events/event_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/event_push_frame_values_to_task.py @@ -4,18 +4,27 @@ from pype.modules.ftrack import BaseEvent class PushFrameValuesToTaskEvent(BaseEvent): - """Action for testing purpose or as base for new actions.""" - cust_attrs_query = ( - "select id, key, object_type_id, is_hierarchical, default" - " from CustomAttributeConfiguration" - " where key in ({}) and object_type_id = {}" - ) - # Ignore event handler by default ignore_me = True - interest_attributes = ["frameStart", "frameEnd"] + cust_attrs_query = ( + "select id, key, object_type_id, is_hierarchical, default" + " from CustomAttributeConfiguration" + " where key in ({}) and object_type_id in ({})" + ) + + interest_entity_types = {"Shot"} + interest_attributes = {"frameStart", "frameEnd"} + interest_attr_mapping = { + "frameStart": "fstart", + "frameEnd": "fend" + } _cached_task_object_id = None + _cached_interest_object_ids = None + + @staticmethod + def join_keys(keys): + return ",".join(["\"{}\"".format(key) for key in keys]) @classmethod def task_object_id(cls, session): @@ -26,6 +35,20 @@ class PushFrameValuesToTaskEvent(BaseEvent): cls._cached_task_object_id = task_object_type["id"] return cls._cached_task_object_id + @classmethod + def interest_object_ids(cls, session): + if cls._cached_interest_object_ids is None: + object_types = session.query( + "ObjectType where name in ({})".format( + cls.join_keys(cls.interest_entity_types) + ) + ).all() + cls._cached_interest_object_ids = tuple( + object_type["id"] + for object_type in object_types + ) + return cls._cached_interest_object_ids + def extract_interesting_data(self, session, event): # Filter if event contain relevant data entities_info = event["data"].get("entities") @@ -60,60 +83,107 @@ class PushFrameValuesToTaskEvent(BaseEvent): interesting_data[entity_info["entityId"]] = entity_changes return interesting_data - def join_keys(self, keys): - return ",".join(["\"{}\"".format(key) for key in keys]) - - def get_task_entities(self, session, entities_info): - return session.query( - "Task where parent_id in ({})".format( - self.join_keys(entities_info.keys()) + def get_entities(self, session, interesting_data): + entities = session.query( + "TypedContext where id in ({})".format( + self.join_keys(interesting_data.keys()) ) ).all() - def task_attrs(self, session): - return session.query(self.cust_attrs_query.format( - self.join_keys(self.interest_attributes), - self.task_object_id(session) + output = [] + interest_object_ids = self.interest_object_ids(session) + for entity in entities: + if entity["object_type_id"] in interest_object_ids: + output.append(entity) + return output + + def get_task_entities(self, session, interesting_data): + return session.query( + "Task where parent_id in ({})".format( + self.join_keys(interesting_data.keys()) + ) + ).all() + + def attrs_configurations(self, session): + object_ids = list(self.interest_object_ids(session)) + object_ids.append(self.task_object_id(session)) + + attrs = session.query(self.cust_attrs_query.format( + self.join_keys(self.interest_attr_mapping.values()), + self.join_keys(object_ids) )).all() + output = {} + for attr in attrs: + obj_id = attr["object_type_id"] + if obj_id not in output: + output[obj_id] = {} + output[obj_id][attr["key"]] = attr["id"] + return output + def launch(self, session, event): interesting_data = self.extract_interesting_data(session, event) if not interesting_data: return + entities = self.get_entities(session, interesting_data) + if not entities: + return + + entities_by_id = { + entity["id"]: entity + for entity in entities + } + for entity_id in tuple(interesting_data.keys()): + if entity_id not in entities_by_id: + interesting_data.pop(entity_id) + task_entities = self.get_task_entities(session, interesting_data) if not task_entities: return - task_attrs = self.task_attrs(session) - if not task_attrs: + attrs_by_obj_id = self.attrs_configurations(session) + if not attrs_by_obj_id: self.log.warning(( "There is not created Custom Attributes {}" " for \"Task\" entity type." ).format(self.join_keys(self.interest_attributes))) return - task_attr_id_by_key = { - attr["key"]: attr["id"] - for attr in task_attrs - } task_entities_by_parent_id = collections.defaultdict(list) for task_entity in task_entities: task_entities_by_parent_id[task_entity["parent_id"]].append( task_entity ) + missing_keys_by_object_name = collections.defaultdict(set) for parent_id, values in interesting_data.items(): - task_entities = task_entities_by_parent_id[parent_id] + entities = task_entities_by_parent_id.get(parent_id) or [] + entities.append(entities_by_id[parent_id]) + for key, value in values.items(): changed_ids = [] - for task_entity in task_entities: - task_id = task_entity["id"] - changed_ids.append(task_id) + for entity in entities: + entity_attrs_mapping = ( + attrs_by_obj_id.get(entity["object_type_id"]) + ) + if not entity_attrs_mapping: + missing_keys_by_object_name[entity.entity_key].add( + key + ) + continue + configuration_id = entity_attrs_mapping.get(key) + if not configuration_id: + missing_keys_by_object_name[entity.entity_key].add( + key + ) + continue + + changed_ids.append(entity["id"]) entity_key = collections.OrderedDict({ - "configuration_id": task_attr_id_by_key[key], - "entity_id": task_id + "configuration_id": configuration_id, + "entity_id": entity["id"] }) if value is None: op = ftrack_api.operation.DeleteEntityOperation( @@ -142,6 +212,19 @@ class PushFrameValuesToTaskEvent(BaseEvent): "Changing of values failed.", exc_info=True ) + if not missing_keys_by_object_name: + return + + msg_items = [] + for object_name, missing_keys in missing_keys_by_object_name.items(): + msg_items.append( + "{}: ({})".format(object_name, self.join_keys(missing_keys)) + ) + + self.log.warning(( + "Missing Custom Attribute configuration" + " per specific object types: {}" + ).format(", ".join(msg_items))) def register(session, plugins_presets): From a628260c82d6c787960df501560fea8999acbc16 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 19:33:42 +0200 Subject: [PATCH 122/158] moved code in better order --- .../events/event_push_frame_values_to_task.py | 144 +++++++++--------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/pype/modules/ftrack/events/event_push_frame_values_to_task.py b/pype/modules/ftrack/events/event_push_frame_values_to_task.py index dd24110c1b..d4056c2ae5 100644 --- a/pype/modules/ftrack/events/event_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/event_push_frame_values_to_task.py @@ -49,78 +49,6 @@ class PushFrameValuesToTaskEvent(BaseEvent): ) return cls._cached_interest_object_ids - def extract_interesting_data(self, session, event): - # Filter if event contain relevant data - entities_info = event["data"].get("entities") - if not entities_info: - return - - interesting_data = {} - for entity_info in entities_info: - # Care only about tasks - if entity_info.get("entityType") != "task": - continue - - # Care only about changes of status - changes = entity_info.get("changes") or {} - if not changes: - continue - - # Care only about changes if specific keys - entity_changes = {} - for key in self.interest_attributes: - if key in changes: - entity_changes[key] = changes[key]["new"] - - if not entity_changes: - continue - - # Do not care about "Task" entity_type - task_object_id = self.task_object_id(session) - if entity_info.get("objectTypeId") == task_object_id: - continue - - interesting_data[entity_info["entityId"]] = entity_changes - return interesting_data - - def get_entities(self, session, interesting_data): - entities = session.query( - "TypedContext where id in ({})".format( - self.join_keys(interesting_data.keys()) - ) - ).all() - - output = [] - interest_object_ids = self.interest_object_ids(session) - for entity in entities: - if entity["object_type_id"] in interest_object_ids: - output.append(entity) - return output - - def get_task_entities(self, session, interesting_data): - return session.query( - "Task where parent_id in ({})".format( - self.join_keys(interesting_data.keys()) - ) - ).all() - - def attrs_configurations(self, session): - object_ids = list(self.interest_object_ids(session)) - object_ids.append(self.task_object_id(session)) - - attrs = session.query(self.cust_attrs_query.format( - self.join_keys(self.interest_attr_mapping.values()), - self.join_keys(object_ids) - )).all() - - output = {} - for attr in attrs: - obj_id = attr["object_type_id"] - if obj_id not in output: - output[obj_id] = {} - output[obj_id][attr["key"]] = attr["id"] - return output - def launch(self, session, event): interesting_data = self.extract_interesting_data(session, event) if not interesting_data: @@ -226,6 +154,78 @@ class PushFrameValuesToTaskEvent(BaseEvent): " per specific object types: {}" ).format(", ".join(msg_items))) + def extract_interesting_data(self, session, event): + # Filter if event contain relevant data + entities_info = event["data"].get("entities") + if not entities_info: + return + + interesting_data = {} + for entity_info in entities_info: + # Care only about tasks + if entity_info.get("entityType") != "task": + continue + + # Care only about changes of status + changes = entity_info.get("changes") or {} + if not changes: + continue + + # Care only about changes if specific keys + entity_changes = {} + for key in self.interest_attributes: + if key in changes: + entity_changes[key] = changes[key]["new"] + + if not entity_changes: + continue + + # Do not care about "Task" entity_type + task_object_id = self.task_object_id(session) + if entity_info.get("objectTypeId") == task_object_id: + continue + + interesting_data[entity_info["entityId"]] = entity_changes + return interesting_data + + def get_entities(self, session, interesting_data): + entities = session.query( + "TypedContext where id in ({})".format( + self.join_keys(interesting_data.keys()) + ) + ).all() + + output = [] + interest_object_ids = self.interest_object_ids(session) + for entity in entities: + if entity["object_type_id"] in interest_object_ids: + output.append(entity) + return output + + def get_task_entities(self, session, interesting_data): + return session.query( + "Task where parent_id in ({})".format( + self.join_keys(interesting_data.keys()) + ) + ).all() + + def attrs_configurations(self, session): + object_ids = list(self.interest_object_ids(session)) + object_ids.append(self.task_object_id(session)) + + attrs = session.query(self.cust_attrs_query.format( + self.join_keys(self.interest_attr_mapping.values()), + self.join_keys(object_ids) + )).all() + + output = {} + for attr in attrs: + obj_id = attr["object_type_id"] + if obj_id not in output: + output[obj_id] = {} + output[obj_id][attr["key"]] = attr["id"] + return output + def register(session, plugins_presets): PushFrameValuesToTaskEvent(session, plugins_presets).register() From 293ceb8e0bffda6dd4f1975633c915f5457ccb6f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 19:33:53 +0200 Subject: [PATCH 123/158] fixed few minor bugs --- .../ftrack/events/event_push_frame_values_to_task.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/events/event_push_frame_values_to_task.py b/pype/modules/ftrack/events/event_push_frame_values_to_task.py index d4056c2ae5..32993ef938 100644 --- a/pype/modules/ftrack/events/event_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/event_push_frame_values_to_task.py @@ -67,8 +67,6 @@ class PushFrameValuesToTaskEvent(BaseEvent): interesting_data.pop(entity_id) task_entities = self.get_task_entities(session, interesting_data) - if not task_entities: - return attrs_by_obj_id = self.attrs_configurations(session) if not attrs_by_obj_id: @@ -89,21 +87,22 @@ class PushFrameValuesToTaskEvent(BaseEvent): entities = task_entities_by_parent_id.get(parent_id) or [] entities.append(entities_by_id[parent_id]) - for key, value in values.items(): + for hier_key, value in values.items(): changed_ids = [] for entity in entities: + key = self.interest_attr_mapping[hier_key] entity_attrs_mapping = ( attrs_by_obj_id.get(entity["object_type_id"]) ) if not entity_attrs_mapping: - missing_keys_by_object_name[entity.entity_key].add( + missing_keys_by_object_name[entity.entity_type].add( key ) continue configuration_id = entity_attrs_mapping.get(key) if not configuration_id: - missing_keys_by_object_name[entity.entity_key].add( + missing_keys_by_object_name[entity.entity_type].add( key ) continue From 009a02f104019c64e94a7cacc4b7c8748c56d018 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 Aug 2020 19:39:49 +0200 Subject: [PATCH 124/158] removed unnecessary logs --- .../ftrack/events/action_push_frame_values_to_task.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py index d88f4a1016..dec34a58cb 100644 --- a/pype/modules/ftrack/events/action_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py @@ -326,13 +326,7 @@ class PushFrameValuesToTaskAction(BaseAction): missing_keys = set() total_parents = len(hier_values_by_entity_id) - idx = 0 for parent_id, values in hier_values_by_entity_id.items(): - idx += 1 - self.log.info(( - "[{}/{}] {} Processing values to task. Values: {}" - ).format(idx, total_parents, parent_id, values)) - task_entities = task_entities_by_parent_id[parent_id] for hier_key, value in values.items(): key = self.custom_attribute_mapping[hier_key] From 576beb744687294bd12f48ce64d7a9ac8d1361bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 20 Aug 2020 20:34:50 +0200 Subject: [PATCH 125/158] removed unused variable --- pype/modules/ftrack/events/action_push_frame_values_to_task.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pype/modules/ftrack/events/action_push_frame_values_to_task.py b/pype/modules/ftrack/events/action_push_frame_values_to_task.py index dec34a58cb..a55c1e46a6 100644 --- a/pype/modules/ftrack/events/action_push_frame_values_to_task.py +++ b/pype/modules/ftrack/events/action_push_frame_values_to_task.py @@ -324,8 +324,6 @@ class PushFrameValuesToTaskAction(BaseAction): task_attr_id_by_keys ): missing_keys = set() - - total_parents = len(hier_values_by_entity_id) for parent_id, values in hier_values_by_entity_id.items(): task_entities = task_entities_by_parent_id[parent_id] for hier_key, value in values.items(): From 2a08d0e44f038a65debe9ec6a96a184c8bfb80d0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 21 Aug 2020 11:13:59 +0200 Subject: [PATCH 126/158] taks are update not overriden during extract hierarchy plugin --- pype/plugins/global/publish/extract_hierarchy_avalon.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/global/publish/extract_hierarchy_avalon.py b/pype/plugins/global/publish/extract_hierarchy_avalon.py index ab8226f6ef..7cea825541 100644 --- a/pype/plugins/global/publish/extract_hierarchy_avalon.py +++ b/pype/plugins/global/publish/extract_hierarchy_avalon.py @@ -78,6 +78,12 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): if entity: # Do not override data, only update cur_entity_data = entity.get("data") or {} + new_tasks = data.pop("tasks", []) + if "tasks" in cur_entity_data and new_tasks: + for task_name in new_tasks: + if task_name not in cur_entity_data["tasks"]: + cur_entity_data["tasks"].append(task_name) + cur_entity_data.update(data) data = cur_entity_data else: From 693d8e75dc37a5a3f072dcee4fe1494a2448b12f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 21 Aug 2020 11:50:01 +0200 Subject: [PATCH 127/158] use default icon if icon for variants was not found --- pype/tools/launcher/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index 3fb201702e..b2743d221c 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -199,6 +199,9 @@ class ActionModel(QtGui.QStandardItemModel): if order is None or action.order < order: order = action.order + if icon is None: + icon = self.default_icon + item = QtGui.QStandardItem(icon, action.label) item.setData(actions, self.ACTION_ROLE) item.setData(True, self.VARIANT_GROUP_ROLE) From d830ad8b00811bea3fd2937d7c82a8f9b03db5fe Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 21 Aug 2020 15:13:36 +0200 Subject: [PATCH 128/158] deal with tasks in edit --- pype/plugins/global/publish/extract_hierarchy_avalon.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/plugins/global/publish/extract_hierarchy_avalon.py b/pype/plugins/global/publish/extract_hierarchy_avalon.py index ab8226f6ef..1d8191f2e3 100644 --- a/pype/plugins/global/publish/extract_hierarchy_avalon.py +++ b/pype/plugins/global/publish/extract_hierarchy_avalon.py @@ -78,6 +78,11 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): if entity: # Do not override data, only update cur_entity_data = entity.get("data") or {} + new_tasks = data.pop("tasks", []) + if "tasks" in cur_entity_data and new_tasks: + for task_name in new_tasks: + if task_name not in cur_entity_data["tasks"]: + cur_entity_data["tasks"].append(task_name) cur_entity_data.update(data) data = cur_entity_data else: From 7584d1ef72e0a03364ff112ec1475be6825cfdbd Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 21 Aug 2020 15:29:49 +0200 Subject: [PATCH 129/158] load audio and refence in harmony --- pype/plugins/harmony/load/load_audio.py | 2 +- pype/plugins/harmony/load/load_imagesequence.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/harmony/load/load_audio.py b/pype/plugins/harmony/load/load_audio.py index 694fda3247..600791e61a 100644 --- a/pype/plugins/harmony/load/load_audio.py +++ b/pype/plugins/harmony/load/load_audio.py @@ -31,7 +31,7 @@ func class ImportAudioLoader(api.Loader): """Import audio.""" - families = ["shot"] + families = ["shot", "audio"] representations = ["wav"] label = "Import Audio" diff --git a/pype/plugins/harmony/load/load_imagesequence.py b/pype/plugins/harmony/load/load_imagesequence.py index 774782b092..c5f50a7d23 100644 --- a/pype/plugins/harmony/load/load_imagesequence.py +++ b/pype/plugins/harmony/load/load_imagesequence.py @@ -230,7 +230,7 @@ class ImageSequenceLoader(api.Loader): """Load images Stores the imported asset in a container named after the asset. """ - families = ["shot", "render", "image", "plate"] + families = ["shot", "render", "image", "plate", "reference"] representations = ["jpeg", "png", "jpg"] def load(self, context, name=None, namespace=None, data=None): From ad7b7c6e220ccb0993e46f3e9036ee97ac531010 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Aug 2020 16:19:59 +0200 Subject: [PATCH 130/158] fix(global): not default True value --- pype/plugins/global/publish/extract_jpeg.py | 2 +- pype/plugins/global/publish/extract_review.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index dd5f79b6ac..f6f41d1397 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -27,7 +27,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): return # Skip review when requested. - if not instance.data.get("review"): + if not instance.data.get("review", True): return # get representation and loop them diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 3a5bd3464a..0bae1b2ddc 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -51,7 +51,7 @@ class ExtractReview(pyblish.api.InstancePlugin): def process(self, instance): # Skip review when requested. - if not instance.data.get("review"): + if not instance.data.get("review", True): return # ffmpeg doesn't support multipart exrs From 0d711c3b7f8c28cd92a3674eeff880766c7f8054 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 Aug 2020 16:25:48 +0100 Subject: [PATCH 131/158] Get linked assets from "inputs". --- pype/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 7cf4e2f1a5..601c85f521 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -746,8 +746,9 @@ class PypeHook: def get_linked_assets(asset_entity): """Return linked assets for `asset_entity`.""" - # TODO implement - return [] + inputs = asset_entity["data"].get("inputs", []) + inputs = [io.find_one({"_id": x}) for x in inputs] + return inputs def map_subsets_by_family(subsets): From 5f0ea1378c5d9c5fc34fdcc91f7fd03698f77584 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 23 Aug 2020 19:16:50 +0200 Subject: [PATCH 132/158] indexes should be removed properly now for artist view --- pype/tools/pyblish_pype/model.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index fdcdffd33f..3c9d4806ac 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -870,13 +870,18 @@ class ArtistProxy(QtCore.QAbstractProxyModel): self.rowsInserted.emit(self.parent(), new_from, new_to + 1) def _remove_rows(self, parent_row, from_row, to_row): - removed_rows = [] increment_num = self.mapping_from[parent_row][from_row] + + to_end_index = len(self.mapping_from[parent_row]) - 1 + for _idx in range(0, parent_row): + to_end_index += len(self.mapping_from[_idx]) + + removed_rows = 0 _emit_last = None for row_num in reversed(range(from_row, to_row + 1)): row = self.mapping_from[parent_row].pop(row_num) _emit_last = row - removed_rows.append(row) + removed_rows += 1 _emit_first = int(increment_num) mapping_from_len = len(self.mapping_from) @@ -896,11 +901,8 @@ class ArtistProxy(QtCore.QAbstractProxyModel): self.mapping_from[idx_i][idx_j] = increment_num increment_num += 1 - first_to_row = None - for row in removed_rows: - if first_to_row is None: - first_to_row = row - self.mapping_to.pop(row) + for idx in range(removed_rows): + self.mapping_to.pop(to_end_index - idx) return (_emit_first, _emit_last) From b648b3c72e1199a3f282f8030d9d5c47cfb35681 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 25 Aug 2020 08:35:09 +0100 Subject: [PATCH 133/158] Add "preview" to image plane representations --- pype/plugins/maya/load/load_image_plane.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/plugins/maya/load/load_image_plane.py b/pype/plugins/maya/load/load_image_plane.py index 08f7c99156..17a6866f80 100644 --- a/pype/plugins/maya/load/load_image_plane.py +++ b/pype/plugins/maya/load/load_image_plane.py @@ -12,7 +12,7 @@ class ImagePlaneLoader(api.Loader): families = ["plate", "render"] label = "Create imagePlane on selected camera." - representations = ["mov", "exr"] + representations = ["mov", "exr", "preview"] icon = "image" color = "orange" @@ -83,7 +83,8 @@ class ImagePlaneLoader(api.Loader): image_plane_shape.frameOut.set(end_frame) image_plane_shape.useFrameExtension.set(1) - if context["representation"]["name"] == "mov": + movie_representations = ["mov", "preview"] + if context["representation"]["name"] in movie_representations: # Need to get "type" by string, because its a method as well. pc.Attribute(image_plane_shape + ".type").set(2) From e1a203125833d96844be994ee993724e8b532c50 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 24 Aug 2020 20:27:03 +0100 Subject: [PATCH 134/158] Handle original file missing and destination file existing. --- pype/plugins/global/publish/integrate_new.py | 36 ++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index a3c2ffe52b..24f5b7bddc 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -6,6 +6,8 @@ import copy import clique import errno import six +import re +import shutil from pymongo import DeleteOne, InsertOne import pyblish.api @@ -952,21 +954,35 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): """ if integrated_file_sizes: for file_url, _file_size in integrated_file_sizes.items(): + if not os.path.exists(file_url): + self.log.debug( + "File {} was not found.".format(file_url) + ) + continue + try: if mode == 'remove': - self.log.debug("Removing file ...{}".format(file_url)) + self.log.debug("Removing file {}".format(file_url)) os.remove(file_url) if mode == 'finalize': - self.log.debug("Renaming file ...{}".format(file_url)) - import re - os.rename(file_url, - re.sub('\.{}$'.format(self.TMP_FILE_EXT), - '', - file_url) - ) + new_name = re.sub( + r'\.{}$'.format(self.TMP_FILE_EXT), + '', + file_url + ) - except FileNotFoundError: - pass # file not there, nothing to delete + if os.path.exists(new_name): + self.log.debug( + "Overwriting file {} to {}".format( + file_url, new_name + ) + ) + shutil.copy(file_url, new_name) + else: + self.log.debug( + "Renaming file {} to {}".format(file_url, new_name) + ) + os.rename(file_url, new_name) except OSError: self.log.error("Cannot {} file {}".format(mode, file_url), exc_info=True) From 432d715d2fe3fd8254d0e02bb5d3f910c4a7ecfb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 24 Aug 2020 20:28:58 +0100 Subject: [PATCH 135/158] Houd --- pype/plugins/global/publish/integrate_new.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 24f5b7bddc..f92968e554 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -980,7 +980,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): shutil.copy(file_url, new_name) else: self.log.debug( - "Renaming file {} to {}".format(file_url, new_name) + "Renaming file {} to {}".format( + file_url, new_name + ) ) os.rename(file_url, new_name) except OSError: From cdf32eb15051b75b17ed2187daefabc0a5fff43a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 26 Aug 2020 10:16:20 +0100 Subject: [PATCH 136/158] Containerize audio loading. --- pype/plugins/maya/load/load_audio.py | 49 +++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/pype/plugins/maya/load/load_audio.py b/pype/plugins/maya/load/load_audio.py index e1860d0ca6..ca38082ed0 100644 --- a/pype/plugins/maya/load/load_audio.py +++ b/pype/plugins/maya/load/load_audio.py @@ -1,6 +1,9 @@ from maya import cmds, mel +import pymel.core as pc from avalon import api +from avalon.maya.pipeline import containerise +from avalon.maya import lib class AudioLoader(api.Loader): @@ -24,4 +27,48 @@ class AudioLoader(api.Loader): displaySound=True ) - return [sound_node] + asset = context["asset"]["name"] + namespace = namespace or lib.unique_namespace( + asset + "_", + prefix="_" if asset[0].isdigit() else "", + suffix="_", + ) + + return containerise( + name=name, + namespace=namespace, + nodes=[sound_node], + context=context, + loader=self.__class__.__name__ + ) + + def update(self, container, representation): + audio_node = None + for node in pc.PyNode(container["objectName"]).members(): + if node.nodeType() == "audio": + audio_node = node + + assert audio_node is not None, "Audio node not found." + + path = api.get_representation_path(representation) + audio_node.filename.set(path) + cmds.setAttr( + container["objectName"] + ".representation", + str(representation["_id"]), + type="string" + ) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + members = cmds.sets(container['objectName'], query=True) + cmds.lockNode(members, lock=False) + cmds.delete([container['objectName']] + members) + + # Clean up the namespace + try: + cmds.namespace(removeNamespace=container['namespace'], + deleteNamespaceContent=True) + except RuntimeError: + pass From a3d82fc92c5ac27f348077685d36b7acb3517cc3 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 26 Aug 2020 11:03:00 +0100 Subject: [PATCH 137/158] Enable previews for Ftrack review. --- pype/plugins/nukestudio/publish/collect_reviews.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/nukestudio/publish/collect_reviews.py b/pype/plugins/nukestudio/publish/collect_reviews.py index aa8c60767c..c158dee876 100644 --- a/pype/plugins/nukestudio/publish/collect_reviews.py +++ b/pype/plugins/nukestudio/publish/collect_reviews.py @@ -99,7 +99,7 @@ class CollectReviews(api.InstancePlugin): "step": 1, "fps": rev_inst.data.get("fps"), "name": "preview", - "tags": ["preview"], + "tags": ["preview", "ftrackreview"], "ext": ext } From 76d65ca6d3dcb8772800a3013005dec25c93fe00 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 26 Aug 2020 11:06:49 +0100 Subject: [PATCH 138/158] Thumbnail parent Assetversion was not found when its not linked to a task. assetversion["task"] returns None instead of raising exception. --- .../ftrack/actions/action_thumbnail_to_parent.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/actions/action_thumbnail_to_parent.py b/pype/modules/ftrack/actions/action_thumbnail_to_parent.py index 8710fa9dcf..fb473f9aa5 100644 --- a/pype/modules/ftrack/actions/action_thumbnail_to_parent.py +++ b/pype/modules/ftrack/actions/action_thumbnail_to_parent.py @@ -41,9 +41,9 @@ class ThumbToParent(BaseAction): parent = None thumbid = None if entity.entity_type.lower() == 'assetversion': - try: - parent = entity['task'] - except Exception: + parent = entity['task'] + + if parent is None: par_ent = entity['link'][-2] parent = session.get(par_ent['type'], par_ent['id']) else: @@ -51,7 +51,7 @@ class ThumbToParent(BaseAction): parent = entity['parent'] except Exception as e: msg = ( - "Durin Action 'Thumb to Parent'" + "During Action 'Thumb to Parent'" " went something wrong" ) self.log.error(msg) @@ -62,7 +62,10 @@ class ThumbToParent(BaseAction): parent['thumbnail_id'] = thumbid status = 'done' else: - status = 'failed' + raise Exception( + "Parent or thumbnail id not found. Parent: {}. " + "Thumbnail id: {}".format(parent, thumbid) + ) # inform the user that the job is done job['status'] = status or 'done' From 76e0b5a7ae1037f24816fe1094e88b52997570ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 26 Aug 2020 12:44:20 +0200 Subject: [PATCH 139/158] allow show icon in bar instead of python icon --- pype/tools/tray/pype_tray.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index 9537b62581..a4cf4eabfe 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -537,6 +537,14 @@ class PypeTrayApplication(QtWidgets.QApplication): super(self.__class__, self).__init__(sys.argv) # Allows to close widgets without exiting app self.setQuitOnLastWindowClosed(False) + + # Allow show icon istead of python icon in task bar (Windows) + if os.name == "nt": + import ctypes + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + u"pype_tray" + ) + # Sets up splash splash_widget = self.set_splash() From bda8cb88017a7c61b225d3bcc4187323d3c27e7f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 26 Aug 2020 12:44:32 +0200 Subject: [PATCH 140/158] login thread is not qthread based --- pype/modules/ftrack/tray/login_tools.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pype/modules/ftrack/tray/login_tools.py b/pype/modules/ftrack/tray/login_tools.py index 02982294f2..e7d22fbc19 100644 --- a/pype/modules/ftrack/tray/login_tools.py +++ b/pype/modules/ftrack/tray/login_tools.py @@ -2,7 +2,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from urllib import parse import webbrowser import functools -from Qt import QtCore +import threading from pype.api import resources @@ -55,20 +55,17 @@ class LoginServerHandler(BaseHTTPRequestHandler): ) -class LoginServerThread(QtCore.QThread): +class LoginServerThread(threading.Thread): '''Login server thread.''' - # Login signal. - loginSignal = QtCore.Signal(object, object, object) - - def start(self, url): - '''Start thread.''' + def __init__(self, url, callback): self.url = url - super(LoginServerThread, self).start() + self.callback = callback + super(LoginServerThread, self).__init__() def _handle_login(self, api_user, api_key): '''Login to server with *api_user* and *api_key*.''' - self.loginSignal.emit(self.url, api_user, api_key) + self.callback(api_user, api_key) def run(self): '''Listen for events.''' From e0e4b4eb9f1050ed6778af3b5860447c7c78b738 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 26 Aug 2020 12:44:50 +0200 Subject: [PATCH 141/158] login dialog was rewriten from base --- pype/modules/ftrack/tray/login_dialog.py | 446 ++++++++++++----------- 1 file changed, 224 insertions(+), 222 deletions(-) diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py index e0614513a3..9ffd21fd30 100644 --- a/pype/modules/ftrack/tray/login_dialog.py +++ b/pype/modules/ftrack/tray/login_dialog.py @@ -7,309 +7,311 @@ from pype.api import resources from Qt import QtCore, QtGui, QtWidgets -class Login_Dialog_ui(QtWidgets.QWidget): - +class CredentialsDialog(QtWidgets.QDialog): SIZE_W = 300 SIZE_H = 230 - loginSignal = QtCore.Signal(object, object, object) - _login_server_thread = None - inputs = [] - buttons = [] - labels = [] + login_changed = QtCore.Signal() + logout_signal = QtCore.Signal() - def __init__(self, parent=None, is_event=False): + def __init__(self, parent=None): + super(CredentialsDialog, self).__init__(parent) - super(Login_Dialog_ui, self).__init__() + self.setWindowTitle("Pype - Ftrack Login") - self.parent = parent - self.is_event = is_event + self._login_server_thread = None + self._is_logged = False + self._in_advance_mode = False - if hasattr(parent, 'icon'): - self.setWindowIcon(self.parent.icon) - elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'): - self.setWindowIcon(self.parent.parent.icon) - else: - icon = QtGui.QIcon(resources.pype_icon_filepath()) - self.setWindowIcon(icon) + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) self.setWindowFlags( QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint ) - self.loginSignal.connect(self.loginWithCredentials) - self._translate = QtCore.QCoreApplication.translate - - self.font = QtGui.QFont() - self.font.setFamily("DejaVu Sans Condensed") - self.font.setPointSize(9) - self.font.setBold(True) - self.font.setWeight(50) - self.font.setKerning(True) - - self.resize(self.SIZE_W, self.SIZE_H) self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) - self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) + self.setMaximumSize(QtCore.QSize(self.SIZE_W + 100, self.SIZE_H + 100)) self.setStyleSheet(style.load_stylesheet()) - self.setLayout(self._main()) - self.setWindowTitle('Pype - Ftrack Login') + self.ui_init() - def _main(self): - self.main = QtWidgets.QVBoxLayout() - self.main.setObjectName("main") - - self.form = QtWidgets.QFormLayout() - self.form.setContentsMargins(10, 15, 10, 5) - self.form.setObjectName("form") - - self.ftsite_label = QtWidgets.QLabel("FTrack URL:") - self.ftsite_label.setFont(self.font) - self.ftsite_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.ftsite_label.setTextFormat(QtCore.Qt.RichText) - self.ftsite_label.setObjectName("user_label") + def ui_init(self): + self.ftsite_label = QtWidgets.QLabel("Ftrack URL:") + self.user_label = QtWidgets.QLabel("Username:") + self.api_label = QtWidgets.QLabel("API Key:") self.ftsite_input = QtWidgets.QLineEdit() - self.ftsite_input.setEnabled(True) - self.ftsite_input.setFrame(True) - self.ftsite_input.setEnabled(False) self.ftsite_input.setReadOnly(True) - self.ftsite_input.setObjectName("ftsite_input") - - self.user_label = QtWidgets.QLabel("Username:") - self.user_label.setFont(self.font) - self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.user_label.setTextFormat(QtCore.Qt.RichText) - self.user_label.setObjectName("user_label") + self.ftsite_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) self.user_input = QtWidgets.QLineEdit() - self.user_input.setEnabled(True) - self.user_input.setFrame(True) - self.user_input.setObjectName("user_input") - self.user_input.setPlaceholderText( - self._translate("main", "user.name") - ) + self.user_input.setPlaceholderText("user.name") self.user_input.textChanged.connect(self._user_changed) - self.api_label = QtWidgets.QLabel("API Key:") - self.api_label.setFont(self.font) - self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.api_label.setTextFormat(QtCore.Qt.RichText) - self.api_label.setObjectName("api_label") - self.api_input = QtWidgets.QLineEdit() - self.api_input.setEnabled(True) - self.api_input.setFrame(True) - self.api_input.setObjectName("api_input") - self.api_input.setPlaceholderText(self._translate( - "main", "e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - )) + self.api_input.setPlaceholderText( + "e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + ) self.api_input.textChanged.connect(self._api_changed) + input_layout = QtWidgets.QFormLayout() + input_layout.setContentsMargins(10, 15, 10, 5) + + input_layout.addRow(self.ftsite_label, self.ftsite_input) + input_layout.addRow(self.user_label, self.user_input) + input_layout.addRow(self.api_label, self.api_input) + + self.btn_advanced = QtWidgets.QPushButton("Advanced") + self.btn_advanced.clicked.connect(self._on_advanced_clicked) + + self.btn_simple = QtWidgets.QPushButton("Simple") + self.btn_simple.clicked.connect(self._on_simple_clicked) + + self.btn_login = QtWidgets.QPushButton("Login") + self.btn_login.setToolTip( + "Set Username and API Key with entered values" + ) + self.btn_login.clicked.connect(self._on_login_clicked) + + self.btn_ftrack_login = QtWidgets.QPushButton("Ftrack login") + self.btn_ftrack_login.setToolTip("Open browser for Login to Ftrack") + self.btn_ftrack_login.clicked.connect(self._on_ftrack_login_clicked) + + self.btn_logout = QtWidgets.QPushButton("Logout") + self.btn_logout.clicked.connect(self._on_logout_clicked) + + self.btn_close = QtWidgets.QPushButton("Close") + self.btn_close.setToolTip("Close this window") + self.btn_close.clicked.connect(self._close_widget) + + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addWidget(self.btn_advanced) + btns_layout.addWidget(self.btn_simple) + btns_layout.addStretch(1) + btns_layout.addWidget(self.btn_ftrack_login) + btns_layout.addWidget(self.btn_login) + btns_layout.addWidget(self.btn_logout) + btns_layout.addWidget(self.btn_close) + + self.note_label = QtWidgets.QLabel(( + "NOTE: Click on \"{}\" button to log with your default browser" + " or click on \"{}\" button to enter API key manually." + ).format(self.btn_ftrack_login.text(), self.btn_advanced.text())) + + self.note_label.setWordWrap(True) + self.note_label.hide() + self.error_label = QtWidgets.QLabel("") - self.error_label.setFont(self.font) - self.error_label.setTextFormat(QtCore.Qt.RichText) - self.error_label.setObjectName("error_label") self.error_label.setWordWrap(True) self.error_label.hide() - self.form.addRow(self.ftsite_label, self.ftsite_input) - self.form.addRow(self.user_label, self.user_input) - self.form.addRow(self.api_label, self.api_input) - self.form.addRow(self.error_label) + label_layout = QtWidgets.QVBoxLayout() + label_layout.setContentsMargins(10, 5, 10, 5) + label_layout.addWidget(self.note_label) + label_layout.addWidget(self.error_label) - self.btnGroup = QtWidgets.QHBoxLayout() - self.btnGroup.addStretch(1) - self.btnGroup.setObjectName("btnGroup") + main = QtWidgets.QVBoxLayout(self) + main.addLayout(input_layout) + main.addLayout(label_layout) + main.addStretch(1) + main.addLayout(btns_layout) - self.btnEnter = QtWidgets.QPushButton("Login") - self.btnEnter.setToolTip( - 'Set Username and API Key with entered values' - ) - self.btnEnter.clicked.connect(self.enter_credentials) + self.fill_ftrack_url() - self.btnClose = QtWidgets.QPushButton("Close") - self.btnClose.setToolTip('Close this window') - self.btnClose.clicked.connect(self._close_widget) + self.set_is_logged(self._is_logged) - self.btnFtrack = QtWidgets.QPushButton("Ftrack") - self.btnFtrack.setToolTip('Open browser for Login to Ftrack') - self.btnFtrack.clicked.connect(self.open_ftrack) + self.setLayout(main) - self.btnGroup.addWidget(self.btnFtrack) - self.btnGroup.addWidget(self.btnEnter) - self.btnGroup.addWidget(self.btnClose) + def fill_ftrack_url(self): + url = os.getenv("FTRACK_SERVER") + checked_url = self.check_url(url) - self.main.addLayout(self.form) - self.main.addLayout(self.btnGroup) + if checked_url is None: + checked_url = "" + self.btn_login.setEnabled(False) + self.btn_ftrack_login.setEnabled(False) - self.inputs.append(self.api_input) - self.inputs.append(self.user_input) - self.inputs.append(self.ftsite_input) + self.api_input.setEnabled(False) + self.user_input.setEnabled(False) + self.ftsite_input.setEnabled(False) - self.enter_site() - return self.main + self.ftsite_input.setText(checked_url) - def enter_site(self): - try: - url = os.getenv('FTRACK_SERVER') - newurl = self.checkUrl(url) + def set_advanced_mode(self, is_advanced): + self._in_advance_mode = is_advanced - if newurl is None: - self.btnEnter.setEnabled(False) - self.btnFtrack.setEnabled(False) - for input in self.inputs: - input.setEnabled(False) - newurl = url + self.error_label.setVisible(False) - self.ftsite_input.setText(newurl) + is_logged = self._is_logged - except Exception: - self.setError("FTRACK_SERVER is not set in templates") - self.btnEnter.setEnabled(False) - self.btnFtrack.setEnabled(False) - for input in self.inputs: - input.setEnabled(False) + self.note_label.setVisible(not is_logged and not is_advanced) + self.btn_ftrack_login.setVisible(not is_logged and not is_advanced) + self.btn_advanced.setVisible(not is_logged and not is_advanced) - def setError(self, msg): + self.btn_login.setVisible(not is_logged and is_advanced) + self.btn_simple.setVisible(not is_logged and is_advanced) + + self.user_label.setVisible(is_logged or is_advanced) + self.user_input.setVisible(is_logged or is_advanced) + self.api_label.setVisible(is_logged or is_advanced) + self.api_input.setVisible(is_logged or is_advanced) + if is_advanced: + self.user_input.setFocus() + else: + self.btn_ftrack_login.setFocus() + + def set_is_logged(self, is_logged): + self._is_logged = is_logged + + self.user_input.setReadOnly(is_logged) + self.api_input.setReadOnly(is_logged) + self.user_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + self.api_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + + self.btn_logout.setVisible(is_logged) + + self.set_advanced_mode(self._in_advance_mode) + + def set_error(self, msg): self.error_label.setText(msg) self.error_label.show() + def _on_logout_clicked(self): + self.user_input.setText("") + self.api_input.setText("") + self.set_is_logged(False) + self.logout_signal.emit() + + def _on_simple_clicked(self): + self.set_advanced_mode(False) + + def _on_advanced_clicked(self): + self.set_advanced_mode(True) + def _user_changed(self): - self.user_input.setStyleSheet("") + self._not_invalid_input(self.user_input) def _api_changed(self): - self.api_input.setStyleSheet("") + self._not_invalid_input(self.api_input) - def _invalid_input(self, entity): - entity.setStyleSheet("border: 1px solid red;") + def _not_invalid_input(self, input_widget): + input_widget.setStyleSheet("") - def enter_credentials(self): + def _invalid_input(self, input_widget): + input_widget.setStyleSheet("border: 1px solid red;") + + def _on_login_clicked(self): username = self.user_input.text().strip() - apiKey = self.api_input.text().strip() - msg = "You didn't enter " + api_key = self.api_input.text().strip() missing = [] if username == "": missing.append("Username") self._invalid_input(self.user_input) - if apiKey == "": + if api_key == "": missing.append("API Key") self._invalid_input(self.api_input) if len(missing) > 0: - self.setError("{0} {1}".format(msg, " and ".join(missing))) + self.set_error("You didn't enter {}".format(" and ".join(missing))) return - verification = credentials.check_credentials(username, apiKey) - - if verification: - credentials.save_credentials(username, apiKey, self.is_event) - credentials.set_env(username, apiKey) - if self.parent is not None: - self.parent.loginChange() - self._close_widget() - else: + if not self.login_with_credentials(username, api_key): self._invalid_input(self.user_input) self._invalid_input(self.api_input) - self.setError( + self.set_error( "We're unable to sign in to Ftrack with these credentials" ) - def open_ftrack(self): - url = self.ftsite_input.text() - self.loginWithCredentials(url, None, None) - - def checkUrl(self, url): - url = url.strip('/ ') - + def _on_ftrack_login_clicked(self): + url = self.check_url(self.ftsite_input.text()) if not url: - self.setError("There is no URL set in Templates") - return - - if 'http' not in url: - if url.endswith('ftrackapp.com'): - url = 'https://' + url - else: - url = 'https://{0}.ftrackapp.com'.format(url) - try: - result = requests.get( - url, - # Old python API will not work with redirect. - allow_redirects=False - ) - except requests.exceptions.RequestException: - self.setError( - 'The server URL set in Templates could not be reached.' - ) - return - - if ( - result.status_code != 200 or 'FTRACK_VERSION' not in result.headers - ): - self.setError( - 'The server URL set in Templates is not a valid ftrack server.' - ) - return - return url - - def loginWithCredentials(self, url, username, apiKey): - url = url.strip('/ ') - - if not url: - self.setError( - 'You need to specify a valid server URL, ' - 'for example https://server-name.ftrackapp.com' - ) - return - - if 'http' not in url: - if url.endswith('ftrackapp.com'): - url = 'https://' + url - else: - url = 'https://{0}.ftrackapp.com'.format(url) - try: - result = requests.get( - url, - # Old python API will not work with redirect. - allow_redirects=False - ) - except requests.exceptions.RequestException: - self.setError( - 'The server URL you provided could not be reached.' - ) - return - - if ( - result.status_code != 200 or 'FTRACK_VERSION' not in result.headers - ): - self.setError( - 'The server URL you provided is not a valid ftrack server.' - ) return # If there is an existing server thread running we need to stop it. if self._login_server_thread: - self._login_server_thread.quit() + self._login_server_thread.stop() + self._login_server_thread.join() self._login_server_thread = None # If credentials are not properly set, try to get them using a http # server. - if not username or not apiKey: - self._login_server_thread = login_tools.LoginServerThread() - self._login_server_thread.loginSignal.connect(self.loginSignal) - self._login_server_thread.start(url) + self._login_server_thread = login_tools.LoginServerThread( + url, self._result_of_ftrack_thread + ) + self._login_server_thread.start() + + def _result_of_ftrack_thread(self, username, api_key): + if not self.login_with_credentials(username, api_key): + self._invalid_input(self.api_input) + self.set_error(( + "Somthing happened with Ftrack login." + " Try enter Username and API key manually." + )) + else: + self.set_is_logged(True) + + def login_with_credentials(self, username, api_key): + verification = credentials.check_credentials(username, api_key) + if verification: + credentials.save_credentials(username, api_key, False) + credentials.set_env(username, api_key) + self.set_credentials(username, api_key) + self.login_changed.emit() + return verification + + def set_credentials(self, username, api_key, is_logged=True): + self.user_input.setText(username) + self.api_input.setText(api_key) + + self.error_label.hide() + + self._not_invalid_input(self.ftsite_input) + self._not_invalid_input(self.user_input) + self._not_invalid_input(self.api_input) + + if is_logged is not None: + self.set_is_logged(is_logged) + + def check_url(self, url): + if url is not None: + url = url.strip("/ ") + + if not url: + self.set_error(( + "You need to specify a valid server URL, " + "for example https://server-name.ftrackapp.com" + )) return - verification = credentials.check_credentials(username, apiKey) + if "http" not in url: + if url.endswith("ftrackapp.com"): + url = "https://" + url + else: + url = "https://{}.ftrackapp.com".format(url) + try: + result = requests.get( + url, + # Old python API will not work with redirect. + allow_redirects=False + ) + except requests.exceptions.RequestException: + self.set_error( + "Specified URL could not be reached." + ) + return - if verification is True: - credentials.save_credentials(username, apiKey, self.is_event) - credentials.set_env(username, apiKey) - if self.parent is not None: - self.parent.loginChange() - self._close_widget() + if ( + result.status_code != 200 + or "FTRACK_VERSION" not in result.headers + ): + self.set_error( + "Specified URL does not lead to a valid Ftrack server." + ) + return + return url def closeEvent(self, event): event.ignore() From 15cb7393297f3ba30a50e2d610ddfd2cd7a1efe1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 26 Aug 2020 12:45:16 +0200 Subject: [PATCH 142/158] ftrack module modified to be able handle new ftrack login dialog --- pype/modules/ftrack/tray/ftrack_module.py | 110 +++++++++++++--------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/pype/modules/ftrack/tray/ftrack_module.py b/pype/modules/ftrack/tray/ftrack_module.py index 674e8cbd4f..99f382b11e 100644 --- a/pype/modules/ftrack/tray/ftrack_module.py +++ b/pype/modules/ftrack/tray/ftrack_module.py @@ -2,7 +2,7 @@ import os import time import datetime import threading -from Qt import QtCore, QtWidgets +from Qt import QtCore, QtWidgets, QtGui import ftrack_api from ..ftrack_server.lib import check_ftrack_url @@ -10,7 +10,7 @@ from ..ftrack_server import socket_thread from ..lib import credentials from . import login_dialog -from pype.api import Logger +from pype.api import Logger, resources log = Logger().get_logger("FtrackModule", "ftrack") @@ -19,7 +19,7 @@ log = Logger().get_logger("FtrackModule", "ftrack") class FtrackModule: def __init__(self, main_parent=None, parent=None): self.parent = parent - self.widget_login = login_dialog.Login_Dialog_ui(self) + self.thread_action_server = None self.thread_socket_server = None self.thread_timer = None @@ -29,8 +29,22 @@ class FtrackModule: self.bool_action_thread_running = False self.bool_timer_event = False + self.widget_login = login_dialog.CredentialsDialog() + self.widget_login.login_changed.connect(self.on_login_change) + self.widget_login.logout_signal.connect(self.on_logout) + + self.action_credentials = None + self.icon_logged = QtGui.QIcon( + resources.get_resource("icons", "circle_green.png") + ) + self.icon_not_logged = QtGui.QIcon( + resources.get_resource("icons", "circle_orange.png") + ) + def show_login_widget(self): self.widget_login.show() + self.widget_login.activateWindow() + self.widget_login.raise_() def validate(self): validation = False @@ -39,9 +53,10 @@ class FtrackModule: ft_api_key = cred.get("api_key") validation = credentials.check_credentials(ft_user, ft_api_key) if validation: + self.widget_login.set_credentials(ft_user, ft_api_key) credentials.set_env(ft_user, ft_api_key) log.info("Connected to Ftrack successfully") - self.loginChange() + self.on_login_change() return validation @@ -60,15 +75,24 @@ class FtrackModule: return validation # Necessary - login_dialog works with this method after logging in - def loginChange(self): + def on_login_change(self): self.bool_logged = True + + self.action_credentials.setIcon(self.icon_logged) + self.action_credentials.setToolTip( + "Logged as user \"{}\"".format(self.widget_login.user_input.text()) + ) + self.set_menu_visibility() self.start_action_server() - def logout(self): + def on_logout(self): credentials.clear_credentials() self.stop_action_server() + self.action_credentials.setIcon(self.icon_not_logged) + self.action_credentials.setToolTip("Logged out") + log.info("Logged out of Ftrack") self.bool_logged = False self.set_menu_visibility() @@ -218,43 +242,45 @@ class FtrackModule: # Definition of Tray menu def tray_menu(self, parent_menu): # Menu for Tray App - self.menu = QtWidgets.QMenu('Ftrack', parent_menu) - self.menu.setProperty('submenu', 'on') - - # Actions - server - self.smActionS = self.menu.addMenu("Action server") - - self.aRunActionS = QtWidgets.QAction( - "Run action server", self.smActionS - ) - self.aResetActionS = QtWidgets.QAction( - "Reset action server", self.smActionS - ) - self.aStopActionS = QtWidgets.QAction( - "Stop action server", self.smActionS - ) - - self.aRunActionS.triggered.connect(self.start_action_server) - self.aResetActionS.triggered.connect(self.reset_action_server) - self.aStopActionS.triggered.connect(self.stop_action_server) - - self.smActionS.addAction(self.aRunActionS) - self.smActionS.addAction(self.aResetActionS) - self.smActionS.addAction(self.aStopActionS) + tray_menu = QtWidgets.QMenu("Ftrack", parent_menu) # Actions - basic - self.aLogin = QtWidgets.QAction("Login", self.menu) - self.aLogin.triggered.connect(self.validate) - self.aLogout = QtWidgets.QAction("Logout", self.menu) - self.aLogout.triggered.connect(self.logout) + action_credentials = QtWidgets.QAction("Credentials", tray_menu) + action_credentials.triggered.connect(self.show_login_widget) + if self.bool_logged: + icon = self.icon_logged + else: + icon = self.icon_not_logged + action_credentials.setIcon(icon) + tray_menu.addAction(action_credentials) + self.action_credentials = action_credentials - self.menu.addAction(self.aLogin) - self.menu.addAction(self.aLogout) + # Actions - server + tray_server_menu = tray_menu.addMenu("Action server") + self.action_server_run = QtWidgets.QAction( + "Run action server", tray_server_menu + ) + self.action_server_reset = QtWidgets.QAction( + "Reset action server", tray_server_menu + ) + self.action_server_stop = QtWidgets.QAction( + "Stop action server", tray_server_menu + ) + + self.action_server_run.triggered.connect(self.start_action_server) + self.action_server_reset.triggered.connect(self.reset_action_server) + self.action_server_stop.triggered.connect(self.stop_action_server) + + tray_server_menu.addAction(self.action_server_run) + tray_server_menu.addAction(self.action_server_reset) + tray_server_menu.addAction(self.action_server_stop) + + self.tray_server_menu = tray_server_menu self.bool_logged = False self.set_menu_visibility() - parent_menu.addMenu(self.menu) + parent_menu.addMenu(tray_menu) def tray_start(self): self.validate() @@ -264,19 +290,15 @@ class FtrackModule: # Definition of visibility of each menu actions def set_menu_visibility(self): - - self.smActionS.menuAction().setVisible(self.bool_logged) - self.aLogin.setVisible(not self.bool_logged) - self.aLogout.setVisible(self.bool_logged) - + self.tray_server_menu.menuAction().setVisible(self.bool_logged) if self.bool_logged is False: if self.bool_timer_event is True: self.stop_timer_thread() return - self.aRunActionS.setVisible(not self.bool_action_server_running) - self.aResetActionS.setVisible(self.bool_action_thread_running) - self.aStopActionS.setVisible(self.bool_action_server_running) + self.action_server_run.setVisible(not self.bool_action_server_running) + self.action_server_reset.setVisible(self.bool_action_thread_running) + self.action_server_stop.setVisible(self.bool_action_server_running) if self.bool_timer_event is False: self.start_timer_thread() From c49ad39965dfdf9d8cce447128ae5f399635d6c5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 26 Aug 2020 12:46:21 +0200 Subject: [PATCH 143/158] more secure icon changes --- pype/modules/ftrack/tray/ftrack_module.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pype/modules/ftrack/tray/ftrack_module.py b/pype/modules/ftrack/tray/ftrack_module.py index 99f382b11e..0b011c5b33 100644 --- a/pype/modules/ftrack/tray/ftrack_module.py +++ b/pype/modules/ftrack/tray/ftrack_module.py @@ -78,10 +78,13 @@ class FtrackModule: def on_login_change(self): self.bool_logged = True - self.action_credentials.setIcon(self.icon_logged) - self.action_credentials.setToolTip( - "Logged as user \"{}\"".format(self.widget_login.user_input.text()) - ) + if self.action_credentials: + self.action_credentials.setIcon(self.icon_logged) + self.action_credentials.setToolTip( + "Logged as user \"{}\"".format( + self.widget_login.user_input.text() + ) + ) self.set_menu_visibility() self.start_action_server() @@ -90,8 +93,9 @@ class FtrackModule: credentials.clear_credentials() self.stop_action_server() - self.action_credentials.setIcon(self.icon_not_logged) - self.action_credentials.setToolTip("Logged out") + if self.action_credentials: + self.action_credentials.setIcon(self.icon_not_logged) + self.action_credentials.setToolTip("Logged out") log.info("Logged out of Ftrack") self.bool_logged = False From ca05176ce5d1becbf3972be6d87cf0b856bb5369 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 26 Aug 2020 12:54:40 +0200 Subject: [PATCH 144/158] thread does not have stop method --- pype/modules/ftrack/tray/login_dialog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py index 9ffd21fd30..b142f31891 100644 --- a/pype/modules/ftrack/tray/login_dialog.py +++ b/pype/modules/ftrack/tray/login_dialog.py @@ -232,7 +232,6 @@ class CredentialsDialog(QtWidgets.QDialog): # If there is an existing server thread running we need to stop it. if self._login_server_thread: - self._login_server_thread.stop() self._login_server_thread.join() self._login_server_thread = None From b98db04a8671abff9fd1f21f4f7cb47b224892c4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 26 Aug 2020 13:01:03 +0200 Subject: [PATCH 145/158] close widget when successfully logged in --- pype/modules/ftrack/tray/login_dialog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py index b142f31891..b703c4cd14 100644 --- a/pype/modules/ftrack/tray/login_dialog.py +++ b/pype/modules/ftrack/tray/login_dialog.py @@ -224,6 +224,8 @@ class CredentialsDialog(QtWidgets.QDialog): self.set_error( "We're unable to sign in to Ftrack with these credentials" ) + else: + self._close_widget() def _on_ftrack_login_clicked(self): url = self.check_url(self.ftsite_input.text()) @@ -251,6 +253,7 @@ class CredentialsDialog(QtWidgets.QDialog): )) else: self.set_is_logged(True) + self._close_widget() def login_with_credentials(self, username, api_key): verification = credentials.check_credentials(username, api_key) From 2f05d7cb197aa7c91ec0ac37585cbf36f882b373 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 26 Aug 2020 13:08:37 +0200 Subject: [PATCH 146/158] fixed closing widget --- pype/modules/ftrack/tray/login_dialog.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py index b703c4cd14..7730ee1609 100644 --- a/pype/modules/ftrack/tray/login_dialog.py +++ b/pype/modules/ftrack/tray/login_dialog.py @@ -35,6 +35,8 @@ class CredentialsDialog(QtWidgets.QDialog): self.setMaximumSize(QtCore.QSize(self.SIZE_W + 100, self.SIZE_H + 100)) self.setStyleSheet(style.load_stylesheet()) + self.login_changed.connect(self._on_login) + self.ui_init() def ui_init(self): @@ -202,6 +204,10 @@ class CredentialsDialog(QtWidgets.QDialog): def _invalid_input(self, input_widget): input_widget.setStyleSheet("border: 1px solid red;") + def _on_login(self): + self.set_is_logged(True) + self._close_widget() + def _on_login_clicked(self): username = self.user_input.text().strip() api_key = self.api_input.text().strip() @@ -224,8 +230,6 @@ class CredentialsDialog(QtWidgets.QDialog): self.set_error( "We're unable to sign in to Ftrack with these credentials" ) - else: - self._close_widget() def _on_ftrack_login_clicked(self): url = self.check_url(self.ftsite_input.text()) @@ -251,9 +255,6 @@ class CredentialsDialog(QtWidgets.QDialog): "Somthing happened with Ftrack login." " Try enter Username and API key manually." )) - else: - self.set_is_logged(True) - self._close_widget() def login_with_credentials(self, username, api_key): verification = credentials.check_credentials(username, api_key) From 850ab0a820698d21af891f7a6fc1f9e4a10ca5f6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 27 Aug 2020 11:01:46 +0100 Subject: [PATCH 147/158] Fix collect reviews - The code logic resulted in the last track review in the context being the review file for all shots. Comparing shot naming as well to isolate to correct clip instance. - Remove "- review" from label cause its already in subset. --- pype/plugins/nukestudio/publish/collect_reviews.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pype/plugins/nukestudio/publish/collect_reviews.py b/pype/plugins/nukestudio/publish/collect_reviews.py index c158dee876..3167c66170 100644 --- a/pype/plugins/nukestudio/publish/collect_reviews.py +++ b/pype/plugins/nukestudio/publish/collect_reviews.py @@ -63,10 +63,14 @@ class CollectReviews(api.InstancePlugin): self.log.debug("Track item on plateMain") rev_inst = None for inst in instance.context[:]: - if inst.data["track"] in track: - rev_inst = inst - self.log.debug("Instance review: {}".format( - rev_inst.data["name"])) + if inst.data["track"] != track: + continue + + if inst.data["item"].name() != instance.data["item"].name(): + continue + + rev_inst = inst + break if rev_inst is None: raise RuntimeError(( @@ -82,7 +86,7 @@ class CollectReviews(api.InstancePlugin): ext = os.path.splitext(file)[-1][1:] # change label - instance.data["label"] = "{0} - {1} - ({2}) - review".format( + instance.data["label"] = "{0} - {1} - ({2})".format( instance.data['asset'], instance.data["subset"], ext ) From 7cdacafb8368b9aee8dd24e8647f1d5f8bde983f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 27 Aug 2020 12:54:53 +0200 Subject: [PATCH 148/158] Moved PR from 2.x/develop --- .../modules/ftrack/actions/action_delivery.py | 364 +++++++++++++----- 1 file changed, 257 insertions(+), 107 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index 7dbb7c65e8..86c3d604ee 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -1,5 +1,6 @@ import os import copy +import json import shutil import collections @@ -9,7 +10,7 @@ from bson.objectid import ObjectId from avalon import pipeline from avalon.vendor import filelink -from pype.api import Anatomy +from pype.api import Anatomy, config from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey from pype.modules.ftrack.lib.io_nonsingleton import DbConnector @@ -41,36 +42,22 @@ class Delivery(BaseAction): items = [] item_splitter = {"type": "label", "value": "---"} - # Prepare component names for processing - components = None - project = None - for entity in entities: - if project is None: - project_id = None - for ent_info in entity["link"]: - if ent_info["type"].lower() == "project": - project_id = ent_info["id"] - break + project_entity = self.get_project_from_entity(entities[0]) + project_name = project_entity["full_name"] + self.db_con.install() + self.db_con.Session["AVALON_PROJECT"] = project_name + project_doc = self.db_con.find_one({"type": "project"}) + if not project_doc: + return { + "success": False, + "message": ( + "Didn't found project \"{}\" in avalon." + ).format(project_name) + } - if project_id is None: - project = entity["asset"]["parent"]["project"] - else: - project = session.query(( - "select id, full_name from Project where id is \"{}\"" - ).format(project_id)).one() + repre_names = self._get_repre_names(entities) + self.db_con.uninstall() - _components = set( - [component["name"] for component in entity["components"]] - ) - if components is None: - components = _components - continue - - components = components.intersection(_components) - if not components: - break - - project_name = project["full_name"] items.append({ "type": "hidden", "name": "__project_name__", @@ -93,7 +80,7 @@ class Delivery(BaseAction): skipped = False # Add message if there are any common components - if not components or not new_anatomies: + if not repre_names or not new_anatomies: skipped = True items.append({ "type": "label", @@ -106,7 +93,7 @@ class Delivery(BaseAction): "value": skipped }) - if not components: + if not repre_names: if len(entities) == 1: items.append({ "type": "label", @@ -143,12 +130,12 @@ class Delivery(BaseAction): "type": "label" }) - for component in components: + for repre_name in repre_names: items.append({ "type": "boolean", "value": False, - "label": component, - "name": component + "label": repre_name, + "name": repre_name }) items.append(item_splitter) @@ -198,27 +185,231 @@ class Delivery(BaseAction): "title": title } + def _get_repre_names(self, entities): + version_ids = self._get_interest_version_ids(entities) + repre_docs = self.db_con.find({ + "type": "representation", + "parent": {"$in": version_ids} + }) + return list(sorted(repre_docs.distinct("name"))) + + def _get_interest_version_ids(self, entities): + parent_ent_by_id = {} + subset_names = set() + version_nums = set() + for entity in entities: + asset = entity["asset"] + parent = asset["parent"] + parent_ent_by_id[parent["id"]] = parent + + subset_name = asset["name"] + subset_names.add(subset_name) + + version = entity["version"] + version_nums.add(version) + + asset_docs_by_ftrack_id = self._get_asset_docs(parent_ent_by_id) + subset_docs = self._get_subset_docs( + asset_docs_by_ftrack_id, subset_names, entities + ) + version_docs = self._get_version_docs( + asset_docs_by_ftrack_id, subset_docs, version_nums, entities + ) + + return [version_doc["_id"] for version_doc in version_docs] + + def _get_version_docs( + self, asset_docs_by_ftrack_id, subset_docs, version_nums, entities + ): + subset_docs_by_id = { + subset_doc["_id"]: subset_doc + for subset_doc in subset_docs + } + version_docs = list(self.db_con.find({ + "type": "version", + "parent": {"$in": list(subset_docs_by_id.keys())}, + "name": {"$in": list(version_nums)} + })) + version_docs_by_parent_id = collections.defaultdict(dict) + for version_doc in version_docs: + subset_doc = subset_docs_by_id[version_doc["parent"]] + + asset_id = subset_doc["parent"] + subset_name = subset_doc["name"] + version = version_doc["name"] + if version_docs_by_parent_id[asset_id].get(subset_name) is None: + version_docs_by_parent_id[asset_id][subset_name] = {} + + version_docs_by_parent_id[asset_id][subset_name][version] = ( + version_doc + ) + + filtered_versions = [] + for entity in entities: + asset = entity["asset"] + + parent = asset["parent"] + asset_doc = asset_docs_by_ftrack_id[parent["id"]] + + subsets_by_name = version_docs_by_parent_id.get(asset_doc["_id"]) + if not subsets_by_name: + continue + + subset_name = asset["name"] + version_docs_by_version = subsets_by_name.get(subset_name) + if not version_docs_by_version: + continue + + version = entity["version"] + version_doc = version_docs_by_version.get(version) + if version_doc: + filtered_versions.append(version_doc) + return filtered_versions + + def _get_subset_docs( + self, asset_docs_by_ftrack_id, subset_names, entities + ): + asset_doc_ids = list() + for asset_doc in asset_docs_by_ftrack_id.values(): + asset_doc_ids.append(asset_doc["_id"]) + + subset_docs = list(self.db_con.find({ + "type": "subset", + "parent": {"$in": asset_doc_ids}, + "name": {"$in": list(subset_names)} + })) + subset_docs_by_parent_id = collections.defaultdict(dict) + for subset_doc in subset_docs: + asset_id = subset_doc["parent"] + subset_name = subset_doc["name"] + subset_docs_by_parent_id[asset_id][subset_name] = subset_doc + + filtered_subsets = [] + for entity in entities: + asset = entity["asset"] + + parent = asset["parent"] + asset_doc = asset_docs_by_ftrack_id[parent["id"]] + + subsets_by_name = subset_docs_by_parent_id.get(asset_doc["_id"]) + if not subsets_by_name: + continue + + subset_name = asset["name"] + subset_doc = subsets_by_name.get(subset_name) + if subset_doc: + filtered_subsets.append(subset_doc) + return filtered_subsets + + def _get_asset_docs(self, parent_ent_by_id): + asset_docs = list(self.db_con.find({ + "type": "asset", + "data.ftrackId": {"$in": list(parent_ent_by_id.keys())} + })) + asset_docs_by_ftrack_id = { + asset_doc["data"]["ftrackId"]: asset_doc + for asset_doc in asset_docs + } + + entities_by_mongo_id = {} + entities_by_names = {} + for ftrack_id, entity in parent_ent_by_id.items(): + if ftrack_id not in asset_docs_by_ftrack_id: + parent_mongo_id = entity["custom_attributes"].get( + CustAttrIdKey + ) + if parent_mongo_id: + entities_by_mongo_id[ObjectId(parent_mongo_id)] = entity + else: + entities_by_names[entity["name"]] = entity + + expressions = [] + if entities_by_mongo_id: + expression = { + "type": "asset", + "_id": {"$in": list(entities_by_mongo_id.keys())} + } + expressions.append(expression) + + if entities_by_names: + expression = { + "type": "asset", + "name": {"$in": list(entities_by_names.keys())} + } + expressions.append(expression) + + if expressions: + if len(expressions) == 1: + filter = expressions[0] + else: + filter = {"$or": expressions} + + asset_docs = self.db_con.find(filter) + for asset_doc in asset_docs: + if asset_doc["_id"] in entities_by_mongo_id: + entity = entities_by_mongo_id[asset_doc["_id"]] + asset_docs_by_ftrack_id[entity["id"]] = asset_doc + + elif asset_doc["name"] in entities_by_names: + entity = entities_by_names[asset_doc["name"]] + asset_docs_by_ftrack_id[entity["id"]] = asset_doc + + return asset_docs_by_ftrack_id + def launch(self, session, entities, event): if "values" not in event["data"]: return - self.report_items = collections.defaultdict(list) - values = event["data"]["values"] skipped = values.pop("__skipped__") if skipped: return None - component_names = [] + user_id = event["source"]["user"]["id"] + user_entity = session.query("User where id is ".format(user_id)).one() + + job = session.create("Job", { + "user": user_entity, + "status": "running", + "data": json.dumps({ + "description": "Delivery processing." + }) + }) + session.commit() + + try: + self.db_con.install() + self.real_launch(session, entities, event) + job["status"] = "done" + + except Exception: + self.log.warning( + "Failed during processing delivery action.", + exc_info=True + ) + + finally: + if job["status"] != "done": + job["status"] = "failed" + session.commit() + self.db_con.uninstall() + + def real_launch(self, session, entities, event): + self.log.info("Delivery action just started.") + report_items = collections.defaultdict(list) + + values = event["data"]["values"] + location_path = values.pop("__location_path__") anatomy_name = values.pop("__new_anatomies__") project_name = values.pop("__project_name__") + repre_names = [] for key, value in values.items(): if value is True: - component_names.append(key) + repre_names.append(key) - if not component_names: + if not repre_names: return { "success": True, "message": "Not selected components to deliver." @@ -230,64 +421,15 @@ class Delivery(BaseAction): if not os.path.exists(location_path): os.makedirs(location_path) - self.db_con.install() self.db_con.Session["AVALON_PROJECT"] = project_name - repres_to_deliver = [] - for entity in entities: - asset = entity["asset"] - subset_name = asset["name"] - version = entity["version"] - - parent = asset["parent"] - parent_mongo_id = parent["custom_attributes"].get(CustAttrIdKey) - if parent_mongo_id: - parent_mongo_id = ObjectId(parent_mongo_id) - else: - asset_ent = self.db_con.find_one({ - "type": "asset", - "data.ftrackId": parent["id"] - }) - if not asset_ent: - ent_path = "/".join( - [ent["name"] for ent in parent["link"]] - ) - msg = "Not synchronized entities to avalon" - self.report_items[msg].append(ent_path) - self.log.warning("{} <{}>".format(msg, ent_path)) - continue - - parent_mongo_id = asset_ent["_id"] - - subset_ent = self.db_con.find_one({ - "type": "subset", - "parent": parent_mongo_id, - "name": subset_name - }) - - version_ent = self.db_con.find_one({ - "type": "version", - "name": version, - "parent": subset_ent["_id"] - }) - - repre_ents = self.db_con.find({ - "type": "representation", - "parent": version_ent["_id"] - }) - - repres_by_name = {} - for repre in repre_ents: - repre_name = repre["name"] - repres_by_name[repre_name] = repre - - for component in entity["components"]: - comp_name = component["name"] - if comp_name not in component_names: - continue - - repre = repres_by_name.get(comp_name) - repres_to_deliver.append(repre) + self.log.debug("Collecting representations to process.") + version_ids = self._get_interest_version_ids(entities) + repres_to_deliver = list(self.db_con.find({ + "type": "representation", + "parent": {"$in": version_ids}, + "name": {"$in": repre_names} + })) anatomy = Anatomy(project_name) @@ -304,9 +446,17 @@ class Delivery(BaseAction): for name in root_names: format_dict["root"][name] = location_path + datetime_data = config.get_datetime_data() for repre in repres_to_deliver: + source_path = repre.get("data", {}).get("path") + debug_msg = "Processing representation {}".format(repre["_id"]) + if source_path: + debug_msg += " with published path {}.".format(source_path) + self.log.debug(debug_msg) + # Get destination repre path anatomy_data = copy.deepcopy(repre["context"]) + anatomy_data.update(datetime_data) anatomy_filled = anatomy.format_all(anatomy_data) test_path = anatomy_filled["delivery"][anatomy_name] @@ -333,7 +483,7 @@ class Delivery(BaseAction): "- Invalid value DataType: \"{}\"
" ).format(str(repre["_id"]), keys) - self.report_items[msg].append(sub_msg) + report_items[msg].append(sub_msg) self.log.warning( "{} Representation: \"{}\" Filled: <{}>".format( msg, str(repre["_id"]), str(test_path) @@ -355,20 +505,19 @@ class Delivery(BaseAction): anatomy, anatomy_name, anatomy_data, - format_dict + format_dict, + report_items ) - if not frame: self.process_single_file(*args) else: self.process_sequence(*args) - self.db_con.uninstall() - - return self.report() + return self.report(report_items) def process_single_file( - self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict + self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict, + report_items ): anatomy_filled = anatomy.format(anatomy_data) if format_dict: @@ -384,7 +533,8 @@ class Delivery(BaseAction): self.copy_file(repre_path, delivery_path) def process_sequence( - self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict + self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict, + report_items ): dir_path, file_name = os.path.split(str(repre_path)) @@ -398,7 +548,7 @@ class Delivery(BaseAction): if not file_name_items: msg = "Source file was not found" - self.report_items[msg].append(repre_path) + report_items[msg].append(repre_path) self.log.warning("{} <{}>".format(msg, repre_path)) return @@ -418,7 +568,7 @@ class Delivery(BaseAction): if src_collection is None: # TODO log error! msg = "Source collection of files was not found" - self.report_items[msg].append(repre_path) + report_items[msg].append(repre_path) self.log.warning("{} <{}>".format(msg, repre_path)) return @@ -491,10 +641,10 @@ class Delivery(BaseAction): except OSError: shutil.copyfile(src_path, dst_path) - def report(self): + def report(self, report_items): items = [] title = "Delivery report" - for msg, _items in self.report_items.items(): + for msg, _items in report_items.items(): if not _items: continue From 4466db6e1242091946f37aa58acf3dc64abb78e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 27 Aug 2020 14:02:38 +0200 Subject: [PATCH 149/158] fix user id missing formatting brackets --- pype/modules/ftrack/actions/action_delivery.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index 86c3d604ee..663a81aad4 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -366,7 +366,9 @@ class Delivery(BaseAction): return None user_id = event["source"]["user"]["id"] - user_entity = session.query("User where id is ".format(user_id)).one() + user_entity = session.query( + "User where id is {}".format(user_id) + ).one() job = session.create("Job", { "user": user_entity, From 1c3e8c38fed53420866d24354d13fc4dfab73440 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 27 Aug 2020 14:39:44 +0200 Subject: [PATCH 150/158] update version.py --- pype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/version.py b/pype/version.py index 0d4f03098b..9e1a271244 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.11.5" +__version__ = "2.11.8" From b7a847563e0cdddad0c509a3ddb21193297e5009 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 28 Aug 2020 10:53:18 +0100 Subject: [PATCH 151/158] Revamp Build Workfile in Nuke. --- pype/hosts/nuke/lib.py | 305 ---------------------------------------- pype/hosts/nuke/menu.py | 31 ++-- 2 files changed, 21 insertions(+), 315 deletions(-) diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py index 8c0e37b15d..19a0784327 100644 --- a/pype/hosts/nuke/lib.py +++ b/pype/hosts/nuke/lib.py @@ -1,7 +1,6 @@ import os import re import sys -import getpass from collections import OrderedDict from avalon import api, io, lib @@ -1060,310 +1059,6 @@ def get_write_node_template_attr(node): return avalon.nuke.lib.fix_data_for_node_create(correct_data) -class BuildWorkfile(WorkfileSettings): - """ - Building first version of workfile. - - Settings are taken from presets and db. It will add all subsets - in last version for defined representaions - - Arguments: - variable (type): description - - """ - xpos = 0 - ypos = 0 - xpos_size = 80 - ypos_size = 90 - xpos_gap = 50 - ypos_gap = 50 - pos_layer = 10 - - def __init__(self, - root_path=None, - root_node=None, - nodes=None, - to_script=None, - **kwargs): - """ - A short description. - - A bit longer description. - - Argumetns: - root_path (str): description - root_node (nuke.Node): description - nodes (list): list of nuke.Node - nodes_effects (dict): dictionary with subsets - - Example: - nodes_effects = { - "plateMain": { - "nodes": [ - [("Class", "Reformat"), - ("resize", "distort"), - ("flip", True)], - - [("Class", "Grade"), - ("blackpoint", 0.5), - ("multiply", 0.4)] - ] - }, - } - - """ - - WorkfileSettings.__init__(self, - root_node=root_node, - nodes=nodes, - **kwargs) - self.to_script = to_script - # collect data for formating - self.data_tmp = { - "project": {"name": self._project["name"], - "code": self._project["data"].get("code", "")}, - "asset": self._asset or os.environ["AVALON_ASSET"], - "task": kwargs.get("task") or api.Session["AVALON_TASK"], - "hierarchy": kwargs.get("hierarchy") or pype.get_hierarchy(), - "version": kwargs.get("version", {}).get("name", 1), - "user": getpass.getuser(), - "comment": "firstBuild", - "ext": "nk" - } - - # get presets from anatomy - anatomy = get_anatomy() - # format anatomy - anatomy_filled = anatomy.format(self.data_tmp) - - # get dir and file for workfile - self.work_dir = anatomy_filled["work"]["folder"] - self.work_file = anatomy_filled["work"]["file"] - - def save_script_as(self, path=None): - # first clear anything in open window - nuke.scriptClear() - - if not path: - dir = self.work_dir - path = os.path.join( - self.work_dir, - self.work_file).replace("\\", "/") - else: - dir = os.path.dirname(path) - - # check if folder is created - if not os.path.exists(dir): - os.makedirs(dir) - - # save script to path - nuke.scriptSaveAs(path) - - def process(self, - regex_filter=None, - version=None, - representations=["exr", "dpx", "lutJson", "mov", - "preview", "png", "jpeg", "jpg"]): - """ - A short description. - - A bit longer description. - - Args: - regex_filter (raw string): regex pattern to filter out subsets - version (int): define a particular version, None gets last - representations (list): - - Returns: - type: description - - Raises: - Exception: description - - """ - - if not self.to_script: - # save the script - self.save_script_as() - - # create viewer and reset frame range - viewer = self.get_nodes(nodes_filter=["Viewer"]) - if not viewer: - vn = nuke.createNode("Viewer") - vn["xpos"].setValue(self.xpos) - vn["ypos"].setValue(self.ypos) - else: - vn = viewer[-1] - - # move position - self.position_up() - - wn = self.write_create() - wn["xpos"].setValue(self.xpos) - wn["ypos"].setValue(self.ypos) - wn["render"].setValue(True) - vn.setInput(0, wn) - - # adding backdrop under write - self.create_backdrop(label="Render write \n\n\n\nOUTPUT", - color='0xcc1102ff', layer=-1, - nodes=[wn]) - - # move position - self.position_up(4) - - # set frame range for new viewer - self.reset_frame_range_handles() - - # get all available representations - subsets = pype.get_subsets(self._asset, - regex_filter=regex_filter, - version=version, - representations=representations) - - for name, subset in subsets.items(): - log.debug("___________________") - log.debug(name) - log.debug(subset["version"]) - - nodes_backdrop = list() - for name, subset in subsets.items(): - if "lut" in name: - continue - log.info("Building Loader to: `{}`".format(name)) - version = subset["version"] - log.info("Version to: `{}`".format(version["name"])) - representations = subset["representaions"] - for repr in representations: - rn = self.read_loader(repr) - rn["xpos"].setValue(self.xpos) - rn["ypos"].setValue(self.ypos) - wn.setInput(0, rn) - - # get editional nodes - lut_subset = [s for n, s in subsets.items() - if "lut{}".format(name.lower()) in n.lower()] - log.debug(">> lut_subset: `{}`".format(lut_subset)) - - if len(lut_subset) > 0: - lsub = lut_subset[0] - fxn = self.effect_loader(lsub["representaions"][-1]) - fxn_ypos = fxn["ypos"].value() - fxn["ypos"].setValue(fxn_ypos - 100) - nodes_backdrop.append(fxn) - - nodes_backdrop.append(rn) - # move position - self.position_right() - - # adding backdrop under all read nodes - self.create_backdrop(label="Loaded Reads", - color='0x2d7702ff', layer=-1, - nodes=nodes_backdrop) - - def read_loader(self, representation): - """ - Gets Loader plugin for image sequence or mov - - Arguments: - representation (dict): avalon db entity - - """ - context = representation["context"] - - loader_name = "LoadSequence" - if "mov" in context["representation"]: - loader_name = "LoadMov" - - loader_plugin = None - for Loader in api.discover(api.Loader): - if Loader.__name__ != loader_name: - continue - - loader_plugin = Loader - - return api.load(Loader=loader_plugin, - representation=representation["_id"]) - - def effect_loader(self, representation): - """ - Gets Loader plugin for effects - - Arguments: - representation (dict): avalon db entity - - """ - loader_name = "LoadLuts" - - loader_plugin = None - for Loader in api.discover(api.Loader): - if Loader.__name__ != loader_name: - continue - - loader_plugin = Loader - - return api.load(Loader=loader_plugin, - representation=representation["_id"]) - - def write_create(self): - """ - Create render write - - Arguments: - representation (dict): avalon db entity - - """ - task = self.data_tmp["task"] - sanitized_task = re.sub('[^0-9a-zA-Z]+', '', task) - subset_name = "render{}Main".format( - sanitized_task.capitalize()) - - Create_name = "CreateWriteRender" - - creator_plugin = None - for Creator in api.discover(api.Creator): - if Creator.__name__ != Create_name: - continue - - creator_plugin = Creator - - # return api.create() - return creator_plugin(subset_name, self._asset).process() - - def create_backdrop(self, label="", color=None, layer=0, - nodes=None): - """ - Create Backdrop node - - Arguments: - color (str): nuke compatible string with color code - layer (int): layer of node usually used (self.pos_layer - 1) - label (str): the message - nodes (list): list of nodes to be wrapped into backdrop - - """ - assert isinstance(nodes, list), "`nodes` should be a list of nodes" - layer = self.pos_layer + layer - - create_backdrop(label=label, color=color, layer=layer, nodes=nodes) - - def position_reset(self, xpos=0, ypos=0): - self.xpos = xpos - self.ypos = ypos - - def position_right(self, multiply=1): - self.xpos += (self.xpos_size * multiply) + self.xpos_gap - - def position_left(self, multiply=1): - self.xpos -= (self.xpos_size * multiply) + self.xpos_gap - - def position_down(self, multiply=1): - self.ypos -= (self.ypos_size * multiply) + self.ypos_gap - - def position_up(self, multiply=1): - self.ypos -= (self.ypos_size * multiply) + self.ypos_gap - - class ExporterReview: """ Base class object for generating review data from Nuke diff --git a/pype/hosts/nuke/menu.py b/pype/hosts/nuke/menu.py index 7306add9fe..b1ef7f47c4 100644 --- a/pype/hosts/nuke/menu.py +++ b/pype/hosts/nuke/menu.py @@ -2,10 +2,12 @@ import nuke from avalon.api import Session from pype.hosts.nuke import lib +from ...lib import BuildWorkfile from pype.api import Logger log = Logger().get_logger(__name__, "nuke") + def install(): menubar = nuke.menu("Nuke") menu = menubar.findItem(Session["AVALON_LABEL"]) @@ -20,7 +22,11 @@ def install(): log.debug("Changing Item: {}".format(rm_item)) # rm_item[1].setEnabled(False) menu.removeItem(rm_item[1].name()) - menu.addCommand(new_name, lambda: workfile_settings().reset_resolution(), index=(rm_item[0])) + menu.addCommand( + new_name, + lambda: workfile_settings().reset_resolution(), + index=(rm_item[0]) + ) # replace reset frame range from avalon core to pype's name = "Reset Frame Range" @@ -31,33 +37,38 @@ def install(): log.debug("Changing Item: {}".format(rm_item)) # rm_item[1].setEnabled(False) menu.removeItem(rm_item[1].name()) - menu.addCommand(new_name, lambda: workfile_settings().reset_frame_range_handles(), index=(rm_item[0])) + menu.addCommand( + new_name, + lambda: workfile_settings().reset_frame_range_handles(), + index=(rm_item[0]) + ) # add colorspace menu item - name = "Set colorspace" + name = "Set Colorspace" menu.addCommand( name, lambda: workfile_settings().set_colorspace(), - index=(rm_item[0]+2) + index=(rm_item[0] + 2) ) log.debug("Adding menu item: {}".format(name)) # add workfile builder menu item - name = "Build First Workfile.." + name = "Build Workfile" menu.addCommand( - name, lambda: lib.BuildWorkfile().process(), - index=(rm_item[0]+7) + name, lambda: BuildWorkfile().process(), + index=(rm_item[0] + 7) ) log.debug("Adding menu item: {}".format(name)) # add item that applies all setting above - name = "Apply all settings" + name = "Apply All Settings" menu.addCommand( - name, lambda: workfile_settings().set_context_settings(), index=(rm_item[0]+3) + name, + lambda: workfile_settings().set_context_settings(), + index=(rm_item[0] + 3) ) log.debug("Adding menu item: {}".format(name)) - def uninstall(): menubar = nuke.menu("Nuke") From b4e091c3fc50166192d3b86aa128e6a63a397660 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 31 Aug 2020 16:45:36 +0200 Subject: [PATCH 152/158] strip dot from repre names in single frame renders --- pype/plugins/global/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index ab5d6cf9b2..758872e717 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -428,7 +428,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "to render, don't know what to do " "with them.") col = rem[0] - _, ext = os.path.splitext(col) + ext = os.path.splitext(col)[1].lstrip(".") else: # but we really expect only one collection. # Nothing else make sense. From a57fbd8f407cc115bb64674173116440378a9fe6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Sep 2020 15:02:53 +0200 Subject: [PATCH 153/158] feat(nuke): adding image loader --- pype/plugins/nuke/load/load_image.py | 233 ++++++++++++++++++++++++ pype/plugins/nuke/load/load_sequence.py | 18 +- 2 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 pype/plugins/nuke/load/load_image.py diff --git a/pype/plugins/nuke/load/load_image.py b/pype/plugins/nuke/load/load_image.py new file mode 100644 index 0000000000..377d52aa14 --- /dev/null +++ b/pype/plugins/nuke/load/load_image.py @@ -0,0 +1,233 @@ +import re +import nuke + +from avalon.vendor import qargparse +from avalon import api, io + +from pype.hosts.nuke import presets + + +class LoadImage(api.Loader): + """Load still image into Nuke""" + + families = [ + "render2d", "source", "plate", + "render", "prerender", "review", + "image" + ] + representations = ["exr", "dpx", "jpg", "jpeg", "png", "psd"] + + label = "Load Image" + order = -10 + icon = "image" + color = "white" + + options = [ + qargparse.Integer( + "frame_number", + label="Frame Number", + default=int(nuke.root()["first_frame"].getValue()), + min=1, + max=999999, + help="What frame is reading from?" + ) + ] + + def load(self, context, name, namespace, options): + from avalon.nuke import ( + containerise, + viewer_update_and_undo_stop + ) + self.log.info("__ options: `{}`".format(options)) + frame_number = options.get("frame_number", 1) + + version = context['version'] + version_data = version.get("data", {}) + repr_id = context["representation"]["_id"] + + self.log.info("version_data: {}\n".format(version_data)) + self.log.debug( + "Representation id `{}` ".format(repr_id)) + + last = first = int(frame_number) + + # Fallback to asset name when namespace is None + if namespace is None: + namespace = context['asset']['name'] + + file = self.fname + + if not file: + repr_id = context["representation"]["_id"] + self.log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return + + file = file.replace("\\", "/") + + repr_cont = context["representation"]["context"] + frame = repr_cont.get("frame") + if frame: + padding = len(frame) + file = file.replace( + frame, + format(frame_number, "0{}".format(padding))) + + read_name = "Read_{0}_{1}_{2}".format( + repr_cont["asset"], + repr_cont["subset"], + repr_cont["representation"]) + + # Create the Loader with the filename path set + with viewer_update_and_undo_stop(): + r = nuke.createNode( + "Read", + "name {}".format(read_name)) + r["file"].setValue(file) + + # Set colorspace defined in version data + colorspace = context["version"]["data"].get("colorspace") + if colorspace: + r["colorspace"].setValue(str(colorspace)) + + # load nuke presets for Read's colorspace + read_clrs_presets = presets.get_colorspace_preset().get( + "nuke", {}).get("read", {}) + + # check if any colorspace presets for read is mathing + preset_clrsp = next((read_clrs_presets[k] + for k in read_clrs_presets + if bool(re.search(k, file))), + None) + if preset_clrsp is not None: + r["colorspace"].setValue(str(preset_clrsp)) + + r["origfirst"].setValue(first) + r["first"].setValue(first) + r["origlast"].setValue(last) + r["last"].setValue(last) + + # add additional metadata from the version to imprint Avalon knob + add_keys = ["source", "colorspace", "author", "fps", "version"] + + data_imprint = { + "frameStart": first, + "frameEnd": last + } + for k in add_keys: + if k == 'version': + data_imprint.update({k: context["version"]['name']}) + else: + data_imprint.update( + {k: context["version"]['data'].get(k, str(None))}) + + data_imprint.update({"objectName": read_name}) + + r["tile_color"].setValue(int("0x4ecd25ff", 16)) + + return containerise(r, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + """Update the Loader's path + + Nuke automatically tries to reset some variables when changing + the loader's path to a new file. These automatic changes are to its + inputs: + + """ + + from avalon.nuke import ( + update_container + ) + + node = nuke.toNode(container["objectName"]) + frame_number = node["first"].value() + + assert node.Class() == "Read", "Must be Read" + + repr_cont = representation["context"] + + file = api.get_representation_path(representation) + + if not file: + repr_id = representation["_id"] + self.log.warning( + "Representation id `{}` is failing to load".format(repr_id)) + return + + file = file.replace("\\", "/") + + frame = repr_cont.get("frame") + if frame: + padding = len(frame) + file = file.replace( + frame, + format(frame_number, "0{}".format(padding))) + + # Get start frame from version data + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + version_data = version.get("data", {}) + + last = first = int(frame_number) + + # Set the global in to the start frame of the sequence + node["origfirst"].setValue(first) + node["first"].setValue(first) + node["origlast"].setValue(last) + node["last"].setValue(last) + + updated_dict = {} + updated_dict.update({ + "representation": str(representation["_id"]), + "frameStart": str(first), + "frameEnd": str(last), + "version": str(version.get("name")), + "colorspace": version_data.get("colorspace"), + "source": version_data.get("source"), + "fps": str(version_data.get("fps")), + "author": version_data.get("author"), + "outputDir": version_data.get("outputDir"), + }) + + # change color of node + if version.get("name") not in [max_version]: + node["tile_color"].setValue(int("0xd84f20ff", 16)) + else: + node["tile_color"].setValue(int("0x4ecd25ff", 16)) + + # Update the imprinted representation + update_container( + node, + updated_dict + ) + self.log.info("udated to version: {}".format(version.get("name"))) + + def remove(self, container): + + from avalon.nuke import viewer_update_and_undo_stop + + node = nuke.toNode(container['objectName']) + assert node.Class() == "Read", "Must be Read" + + with viewer_update_and_undo_stop(): + nuke.delete(node) diff --git a/pype/plugins/nuke/load/load_sequence.py b/pype/plugins/nuke/load/load_sequence.py index 601e28c7c1..c5ce288540 100644 --- a/pype/plugins/nuke/load/load_sequence.py +++ b/pype/plugins/nuke/load/load_sequence.py @@ -120,12 +120,12 @@ class LoadSequence(api.Loader): if "#" not in file: frame = repr_cont.get("frame") padding = len(frame) - file = file.replace(frame, "#"*padding) + file = file.replace(frame, "#" * padding) read_name = "Read_{0}_{1}_{2}".format( - repr_cont["asset"], - repr_cont["subset"], - repr_cont["representation"]) + repr_cont["asset"], + repr_cont["subset"], + repr_cont["representation"]) # Create the Loader with the filename path set with viewer_update_and_undo_stop(): @@ -250,7 +250,7 @@ class LoadSequence(api.Loader): if "#" not in file: frame = repr_cont.get("frame") padding = len(frame) - file = file.replace(frame, "#"*padding) + file = file.replace(frame, "#" * padding) # Get start frame from version data version = io.find_one({ @@ -276,10 +276,10 @@ class LoadSequence(api.Loader): last = version_data.get("frameEnd") if first is None: - self.log.warning("Missing start frame for updated version" - "assuming starts at frame 0 for: " - "{} ({})".format( - node['name'].value(), representation)) + self.log.warning( + "Missing start frame for updated version" + "assuming starts at frame 0 for: " + "{} ({})".format(node['name'].value(), representation)) first = 0 first -= self.handle_start From 4970aaf5124cc7e0bfc4d5c1b252e486542da993 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 1 Sep 2020 14:22:30 +0100 Subject: [PATCH 154/158] Remove extra dash --- pype/scripts/otio_burnin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py index 718943855c..156896a759 100644 --- a/pype/scripts/otio_burnin.py +++ b/pype/scripts/otio_burnin.py @@ -526,7 +526,7 @@ def burnins_from_data( bit_rate = burnin._streams[0].get("bit_rate") if bit_rate: - ffmpeg_args.append("--b:v {}".format(bit_rate)) + ffmpeg_args.append("-b:v {}".format(bit_rate)) pix_fmt = burnin._streams[0].get("pix_fmt") if pix_fmt: From 427a6109b7ad5d1b7591b60a50e9ba6d6df78dd3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Sep 2020 15:09:42 +0200 Subject: [PATCH 155/158] removed all io_nonsingleton files from pype --- .../adobe_communicator/lib/io_nonsingleton.py | 460 ------------------ pype/modules/ftrack/lib/io_nonsingleton.py | 460 ------------------ 2 files changed, 920 deletions(-) delete mode 100644 pype/modules/adobe_communicator/lib/io_nonsingleton.py delete mode 100644 pype/modules/ftrack/lib/io_nonsingleton.py diff --git a/pype/modules/adobe_communicator/lib/io_nonsingleton.py b/pype/modules/adobe_communicator/lib/io_nonsingleton.py deleted file mode 100644 index da37c657c6..0000000000 --- a/pype/modules/adobe_communicator/lib/io_nonsingleton.py +++ /dev/null @@ -1,460 +0,0 @@ -""" -Wrapper around interactions with the database - -Copy of io module in avalon-core. - - In this case not working as singleton with api.Session! -""" - -import os -import time -import errno -import shutil -import logging -import tempfile -import functools -import contextlib - -from avalon import schema -from avalon.vendor import requests -from avalon.io import extract_port_from_url - -# Third-party dependencies -import pymongo - - -def auto_reconnect(func): - """Handling auto reconnect in 3 retry times""" - @functools.wraps(func) - def decorated(*args, **kwargs): - object = args[0] - for retry in range(3): - try: - return func(*args, **kwargs) - except pymongo.errors.AutoReconnect: - object.log.error("Reconnecting..") - time.sleep(0.1) - else: - raise - - return decorated - - -class DbConnector(object): - - log = logging.getLogger(__name__) - - def __init__(self): - self.Session = {} - self._mongo_client = None - self._sentry_client = None - self._sentry_logging_handler = None - self._database = None - self._is_installed = False - - def __getitem__(self, key): - # gives direct access to collection withou setting `active_table` - return self._database[key] - - def __getattribute__(self, attr): - # not all methods of PyMongo database are implemented with this it is - # possible to use them too - try: - return super(DbConnector, self).__getattribute__(attr) - except AttributeError: - cur_proj = self.Session["AVALON_PROJECT"] - return self._database[cur_proj].__getattribute__(attr) - - def install(self): - """Establish a persistent connection to the database""" - if self._is_installed: - return - - logging.basicConfig() - self.Session.update(self._from_environment()) - - timeout = int(self.Session["AVALON_TIMEOUT"]) - mongo_url = self.Session["AVALON_MONGO"] - kwargs = { - "host": mongo_url, - "serverSelectionTimeoutMS": timeout - } - - port = extract_port_from_url(mongo_url) - if port is not None: - kwargs["port"] = int(port) - - self._mongo_client = pymongo.MongoClient(**kwargs) - - for retry in range(3): - try: - t1 = time.time() - self._mongo_client.server_info() - - except Exception: - self.log.error("Retrying..") - time.sleep(1) - timeout *= 1.5 - - else: - break - - else: - raise IOError( - "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (self.Session["AVALON_MONGO"], timeout)) - - self.log.info("Connected to %s, delay %.3f s" % ( - self.Session["AVALON_MONGO"], time.time() - t1)) - - self._install_sentry() - - self._database = self._mongo_client[self.Session["AVALON_DB"]] - self._is_installed = True - - def _install_sentry(self): - if "AVALON_SENTRY" not in self.Session: - return - - try: - from raven import Client - from raven.handlers.logging import SentryHandler - from raven.conf import setup_logging - except ImportError: - # Note: There was a Sentry address in this Session - return self.log.warning("Sentry disabled, raven not installed") - - client = Client(self.Session["AVALON_SENTRY"]) - - # Transmit log messages to Sentry - handler = SentryHandler(client) - handler.setLevel(logging.WARNING) - - setup_logging(handler) - - self._sentry_client = client - self._sentry_logging_handler = handler - self.log.info( - "Connected to Sentry @ %s" % self.Session["AVALON_SENTRY"] - ) - - def _from_environment(self): - Session = { - item[0]: os.getenv(item[0], item[1]) - for item in ( - # Root directory of projects on disk - ("AVALON_PROJECTS", None), - - # Name of current Project - ("AVALON_PROJECT", ""), - - # Name of current Asset - ("AVALON_ASSET", ""), - - # Name of current silo - ("AVALON_SILO", ""), - - # Name of current task - ("AVALON_TASK", None), - - # Name of current app - ("AVALON_APP", None), - - # Path to working directory - ("AVALON_WORKDIR", None), - - # Name of current Config - # TODO(marcus): Establish a suitable default config - ("AVALON_CONFIG", "no_config"), - - # Name of Avalon in graphical user interfaces - # Use this to customise the visual appearance of Avalon - # to better integrate with your surrounding pipeline - ("AVALON_LABEL", "Avalon"), - - # Used during any connections to the outside world - ("AVALON_TIMEOUT", "1000"), - - # Address to Asset Database - ("AVALON_MONGO", "mongodb://localhost:27017"), - - # Name of database used in MongoDB - ("AVALON_DB", "avalon"), - - # Address to Sentry - ("AVALON_SENTRY", None), - - # Address to Deadline Web Service - # E.g. http://192.167.0.1:8082 - ("AVALON_DEADLINE", None), - - # Enable features not necessarily stable. The user's own risk - ("AVALON_EARLY_ADOPTER", None), - - # Address of central asset repository, contains - # the following interface: - # /upload - # /download - # /manager (optional) - ("AVALON_LOCATION", "http://127.0.0.1"), - - # Boolean of whether to upload published material - # to central asset repository - ("AVALON_UPLOAD", None), - - # Generic username and password - ("AVALON_USERNAME", "avalon"), - ("AVALON_PASSWORD", "secret"), - - # Unique identifier for instances in working files - ("AVALON_INSTANCE_ID", "avalon.instance"), - ("AVALON_CONTAINER_ID", "avalon.container"), - - # Enable debugging - ("AVALON_DEBUG", None), - - ) if os.getenv(item[0], item[1]) is not None - } - - Session["schema"] = "avalon-core:session-2.0" - try: - schema.validate(Session) - except schema.ValidationError as e: - # TODO(marcus): Make this mandatory - self.log.warning(e) - - return Session - - def uninstall(self): - """Close any connection to the database""" - try: - self._mongo_client.close() - except AttributeError: - pass - - self._mongo_client = None - self._database = None - self._is_installed = False - - def active_project(self): - """Return the name of the active project""" - return self.Session["AVALON_PROJECT"] - - def activate_project(self, project_name): - self.Session["AVALON_PROJECT"] = project_name - - def projects(self): - """List available projects - - Returns: - list of project documents - - """ - - collection_names = self.collections() - for project in collection_names: - if project in ("system.indexes",): - continue - - # Each collection will have exactly one project document - document = self.find_project(project) - - if document is not None: - yield document - - def locate(self, path): - """Traverse a hierarchy from top-to-bottom - - Example: - representation = locate(["hulk", "Bruce", "modelDefault", 1, "ma"]) - - Returns: - representation (ObjectId) - - """ - - components = zip( - ("project", "asset", "subset", "version", "representation"), - path - ) - - parent = None - for type_, name in components: - latest = (type_ == "version") and name in (None, -1) - - try: - if latest: - parent = self.find_one( - filter={ - "type": type_, - "parent": parent - }, - projection={"_id": 1}, - sort=[("name", -1)] - )["_id"] - else: - parent = self.find_one( - filter={ - "type": type_, - "name": name, - "parent": parent - }, - projection={"_id": 1}, - )["_id"] - - except TypeError: - return None - - return parent - - @auto_reconnect - def collections(self): - return self._database.collection_names() - - @auto_reconnect - def find_project(self, project): - return self._database[project].find_one({"type": "project"}) - - @auto_reconnect - def insert_one(self, item): - assert isinstance(item, dict), "item must be of type " - schema.validate(item) - return self._database[self.Session["AVALON_PROJECT"]].insert_one(item) - - @auto_reconnect - def insert_many(self, items, ordered=True): - # check if all items are valid - assert isinstance(items, list), "`items` must be of type " - for item in items: - assert isinstance(item, dict), "`item` must be of type " - schema.validate(item) - - return self._database[self.Session["AVALON_PROJECT"]].insert_many( - items, - ordered=ordered) - - @auto_reconnect - def find(self, filter, projection=None, sort=None): - return self._database[self.Session["AVALON_PROJECT"]].find( - filter=filter, - projection=projection, - sort=sort - ) - - @auto_reconnect - def find_one(self, filter, projection=None, sort=None): - assert isinstance(filter, dict), "filter must be " - - return self._database[self.Session["AVALON_PROJECT"]].find_one( - filter=filter, - projection=projection, - sort=sort - ) - - @auto_reconnect - def save(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].save( - *args, **kwargs) - - @auto_reconnect - def replace_one(self, filter, replacement): - return self._database[self.Session["AVALON_PROJECT"]].replace_one( - filter, replacement) - - @auto_reconnect - def update_many(self, filter, update): - return self._database[self.Session["AVALON_PROJECT"]].update_many( - filter, update) - - @auto_reconnect - def distinct(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].distinct( - *args, **kwargs) - - @auto_reconnect - def drop(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].drop( - *args, **kwargs) - - @auto_reconnect - def delete_many(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].delete_many( - *args, **kwargs) - - def parenthood(self, document): - assert document is not None, "This is a bug" - - parents = list() - - while document.get("parent") is not None: - document = self.find_one({"_id": document["parent"]}) - - if document is None: - break - - if document.get("type") == "master_version": - _document = self.find_one({"_id": document["version_id"]}) - document["data"] = _document["data"] - - parents.append(document) - - return parents - - @contextlib.contextmanager - def tempdir(self): - tempdir = tempfile.mkdtemp() - try: - yield tempdir - finally: - shutil.rmtree(tempdir) - - def download(self, src, dst): - """Download `src` to `dst` - - Arguments: - src (str): URL to source file - dst (str): Absolute path to destination file - - Yields tuple (progress, error): - progress (int): Between 0-100 - error (Exception): Any exception raised when first making connection - - """ - - try: - response = requests.get( - src, - stream=True, - auth=requests.auth.HTTPBasicAuth( - self.Session["AVALON_USERNAME"], - self.Session["AVALON_PASSWORD"] - ) - ) - except requests.ConnectionError as e: - yield None, e - return - - with self.tempdir() as dirname: - tmp = os.path.join(dirname, os.path.basename(src)) - - with open(tmp, "wb") as f: - total_length = response.headers.get("content-length") - - if total_length is None: # no content length header - f.write(response.content) - else: - downloaded = 0 - total_length = int(total_length) - for data in response.iter_content(chunk_size=4096): - downloaded += len(data) - f.write(data) - - yield int(100.0 * downloaded / total_length), None - - try: - os.makedirs(os.path.dirname(dst)) - except OSError as e: - # An already existing destination directory is fine. - if e.errno != errno.EEXIST: - raise - - shutil.copy(tmp, dst) diff --git a/pype/modules/ftrack/lib/io_nonsingleton.py b/pype/modules/ftrack/lib/io_nonsingleton.py deleted file mode 100644 index da37c657c6..0000000000 --- a/pype/modules/ftrack/lib/io_nonsingleton.py +++ /dev/null @@ -1,460 +0,0 @@ -""" -Wrapper around interactions with the database - -Copy of io module in avalon-core. - - In this case not working as singleton with api.Session! -""" - -import os -import time -import errno -import shutil -import logging -import tempfile -import functools -import contextlib - -from avalon import schema -from avalon.vendor import requests -from avalon.io import extract_port_from_url - -# Third-party dependencies -import pymongo - - -def auto_reconnect(func): - """Handling auto reconnect in 3 retry times""" - @functools.wraps(func) - def decorated(*args, **kwargs): - object = args[0] - for retry in range(3): - try: - return func(*args, **kwargs) - except pymongo.errors.AutoReconnect: - object.log.error("Reconnecting..") - time.sleep(0.1) - else: - raise - - return decorated - - -class DbConnector(object): - - log = logging.getLogger(__name__) - - def __init__(self): - self.Session = {} - self._mongo_client = None - self._sentry_client = None - self._sentry_logging_handler = None - self._database = None - self._is_installed = False - - def __getitem__(self, key): - # gives direct access to collection withou setting `active_table` - return self._database[key] - - def __getattribute__(self, attr): - # not all methods of PyMongo database are implemented with this it is - # possible to use them too - try: - return super(DbConnector, self).__getattribute__(attr) - except AttributeError: - cur_proj = self.Session["AVALON_PROJECT"] - return self._database[cur_proj].__getattribute__(attr) - - def install(self): - """Establish a persistent connection to the database""" - if self._is_installed: - return - - logging.basicConfig() - self.Session.update(self._from_environment()) - - timeout = int(self.Session["AVALON_TIMEOUT"]) - mongo_url = self.Session["AVALON_MONGO"] - kwargs = { - "host": mongo_url, - "serverSelectionTimeoutMS": timeout - } - - port = extract_port_from_url(mongo_url) - if port is not None: - kwargs["port"] = int(port) - - self._mongo_client = pymongo.MongoClient(**kwargs) - - for retry in range(3): - try: - t1 = time.time() - self._mongo_client.server_info() - - except Exception: - self.log.error("Retrying..") - time.sleep(1) - timeout *= 1.5 - - else: - break - - else: - raise IOError( - "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (self.Session["AVALON_MONGO"], timeout)) - - self.log.info("Connected to %s, delay %.3f s" % ( - self.Session["AVALON_MONGO"], time.time() - t1)) - - self._install_sentry() - - self._database = self._mongo_client[self.Session["AVALON_DB"]] - self._is_installed = True - - def _install_sentry(self): - if "AVALON_SENTRY" not in self.Session: - return - - try: - from raven import Client - from raven.handlers.logging import SentryHandler - from raven.conf import setup_logging - except ImportError: - # Note: There was a Sentry address in this Session - return self.log.warning("Sentry disabled, raven not installed") - - client = Client(self.Session["AVALON_SENTRY"]) - - # Transmit log messages to Sentry - handler = SentryHandler(client) - handler.setLevel(logging.WARNING) - - setup_logging(handler) - - self._sentry_client = client - self._sentry_logging_handler = handler - self.log.info( - "Connected to Sentry @ %s" % self.Session["AVALON_SENTRY"] - ) - - def _from_environment(self): - Session = { - item[0]: os.getenv(item[0], item[1]) - for item in ( - # Root directory of projects on disk - ("AVALON_PROJECTS", None), - - # Name of current Project - ("AVALON_PROJECT", ""), - - # Name of current Asset - ("AVALON_ASSET", ""), - - # Name of current silo - ("AVALON_SILO", ""), - - # Name of current task - ("AVALON_TASK", None), - - # Name of current app - ("AVALON_APP", None), - - # Path to working directory - ("AVALON_WORKDIR", None), - - # Name of current Config - # TODO(marcus): Establish a suitable default config - ("AVALON_CONFIG", "no_config"), - - # Name of Avalon in graphical user interfaces - # Use this to customise the visual appearance of Avalon - # to better integrate with your surrounding pipeline - ("AVALON_LABEL", "Avalon"), - - # Used during any connections to the outside world - ("AVALON_TIMEOUT", "1000"), - - # Address to Asset Database - ("AVALON_MONGO", "mongodb://localhost:27017"), - - # Name of database used in MongoDB - ("AVALON_DB", "avalon"), - - # Address to Sentry - ("AVALON_SENTRY", None), - - # Address to Deadline Web Service - # E.g. http://192.167.0.1:8082 - ("AVALON_DEADLINE", None), - - # Enable features not necessarily stable. The user's own risk - ("AVALON_EARLY_ADOPTER", None), - - # Address of central asset repository, contains - # the following interface: - # /upload - # /download - # /manager (optional) - ("AVALON_LOCATION", "http://127.0.0.1"), - - # Boolean of whether to upload published material - # to central asset repository - ("AVALON_UPLOAD", None), - - # Generic username and password - ("AVALON_USERNAME", "avalon"), - ("AVALON_PASSWORD", "secret"), - - # Unique identifier for instances in working files - ("AVALON_INSTANCE_ID", "avalon.instance"), - ("AVALON_CONTAINER_ID", "avalon.container"), - - # Enable debugging - ("AVALON_DEBUG", None), - - ) if os.getenv(item[0], item[1]) is not None - } - - Session["schema"] = "avalon-core:session-2.0" - try: - schema.validate(Session) - except schema.ValidationError as e: - # TODO(marcus): Make this mandatory - self.log.warning(e) - - return Session - - def uninstall(self): - """Close any connection to the database""" - try: - self._mongo_client.close() - except AttributeError: - pass - - self._mongo_client = None - self._database = None - self._is_installed = False - - def active_project(self): - """Return the name of the active project""" - return self.Session["AVALON_PROJECT"] - - def activate_project(self, project_name): - self.Session["AVALON_PROJECT"] = project_name - - def projects(self): - """List available projects - - Returns: - list of project documents - - """ - - collection_names = self.collections() - for project in collection_names: - if project in ("system.indexes",): - continue - - # Each collection will have exactly one project document - document = self.find_project(project) - - if document is not None: - yield document - - def locate(self, path): - """Traverse a hierarchy from top-to-bottom - - Example: - representation = locate(["hulk", "Bruce", "modelDefault", 1, "ma"]) - - Returns: - representation (ObjectId) - - """ - - components = zip( - ("project", "asset", "subset", "version", "representation"), - path - ) - - parent = None - for type_, name in components: - latest = (type_ == "version") and name in (None, -1) - - try: - if latest: - parent = self.find_one( - filter={ - "type": type_, - "parent": parent - }, - projection={"_id": 1}, - sort=[("name", -1)] - )["_id"] - else: - parent = self.find_one( - filter={ - "type": type_, - "name": name, - "parent": parent - }, - projection={"_id": 1}, - )["_id"] - - except TypeError: - return None - - return parent - - @auto_reconnect - def collections(self): - return self._database.collection_names() - - @auto_reconnect - def find_project(self, project): - return self._database[project].find_one({"type": "project"}) - - @auto_reconnect - def insert_one(self, item): - assert isinstance(item, dict), "item must be of type " - schema.validate(item) - return self._database[self.Session["AVALON_PROJECT"]].insert_one(item) - - @auto_reconnect - def insert_many(self, items, ordered=True): - # check if all items are valid - assert isinstance(items, list), "`items` must be of type " - for item in items: - assert isinstance(item, dict), "`item` must be of type " - schema.validate(item) - - return self._database[self.Session["AVALON_PROJECT"]].insert_many( - items, - ordered=ordered) - - @auto_reconnect - def find(self, filter, projection=None, sort=None): - return self._database[self.Session["AVALON_PROJECT"]].find( - filter=filter, - projection=projection, - sort=sort - ) - - @auto_reconnect - def find_one(self, filter, projection=None, sort=None): - assert isinstance(filter, dict), "filter must be " - - return self._database[self.Session["AVALON_PROJECT"]].find_one( - filter=filter, - projection=projection, - sort=sort - ) - - @auto_reconnect - def save(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].save( - *args, **kwargs) - - @auto_reconnect - def replace_one(self, filter, replacement): - return self._database[self.Session["AVALON_PROJECT"]].replace_one( - filter, replacement) - - @auto_reconnect - def update_many(self, filter, update): - return self._database[self.Session["AVALON_PROJECT"]].update_many( - filter, update) - - @auto_reconnect - def distinct(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].distinct( - *args, **kwargs) - - @auto_reconnect - def drop(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].drop( - *args, **kwargs) - - @auto_reconnect - def delete_many(self, *args, **kwargs): - return self._database[self.Session["AVALON_PROJECT"]].delete_many( - *args, **kwargs) - - def parenthood(self, document): - assert document is not None, "This is a bug" - - parents = list() - - while document.get("parent") is not None: - document = self.find_one({"_id": document["parent"]}) - - if document is None: - break - - if document.get("type") == "master_version": - _document = self.find_one({"_id": document["version_id"]}) - document["data"] = _document["data"] - - parents.append(document) - - return parents - - @contextlib.contextmanager - def tempdir(self): - tempdir = tempfile.mkdtemp() - try: - yield tempdir - finally: - shutil.rmtree(tempdir) - - def download(self, src, dst): - """Download `src` to `dst` - - Arguments: - src (str): URL to source file - dst (str): Absolute path to destination file - - Yields tuple (progress, error): - progress (int): Between 0-100 - error (Exception): Any exception raised when first making connection - - """ - - try: - response = requests.get( - src, - stream=True, - auth=requests.auth.HTTPBasicAuth( - self.Session["AVALON_USERNAME"], - self.Session["AVALON_PASSWORD"] - ) - ) - except requests.ConnectionError as e: - yield None, e - return - - with self.tempdir() as dirname: - tmp = os.path.join(dirname, os.path.basename(src)) - - with open(tmp, "wb") as f: - total_length = response.headers.get("content-length") - - if total_length is None: # no content length header - f.write(response.content) - else: - downloaded = 0 - total_length = int(total_length) - for data in response.iter_content(chunk_size=4096): - downloaded += len(data) - f.write(data) - - yield int(100.0 * downloaded / total_length), None - - try: - os.makedirs(os.path.dirname(dst)) - except OSError as e: - # An already existing destination directory is fine. - if e.errno != errno.EEXIST: - raise - - shutil.copy(tmp, dst) From c68423279c0c1b7dd5a9af4be3d3f42b5d0561da Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 8 Sep 2020 15:10:34 +0200 Subject: [PATCH 156/158] everywhere where io_nonsingleton was used is used AvalonmongoDB now --- pype/modules/adobe_communicator/lib/__init__.py | 2 -- pype/modules/adobe_communicator/lib/rest_api.py | 4 ++-- pype/modules/avalon_apps/rest_api.py | 4 ++-- pype/modules/ftrack/actions/action_delete_asset.py | 4 ++-- pype/modules/ftrack/actions/action_delete_old_versions.py | 4 ++-- pype/modules/ftrack/actions/action_delivery.py | 4 ++-- .../ftrack/actions/action_store_thumbnails_to_avalon.py | 4 ++-- pype/modules/ftrack/events/event_sync_to_avalon.py | 4 ++-- pype/modules/ftrack/events/event_user_assigment.py | 4 ++-- pype/modules/ftrack/lib/avalon_sync.py | 4 ++-- pype/tools/launcher/window.py | 4 ++-- pype/tools/standalonepublish/app.py | 4 ++-- 12 files changed, 22 insertions(+), 24 deletions(-) diff --git a/pype/modules/adobe_communicator/lib/__init__.py b/pype/modules/adobe_communicator/lib/__init__.py index 23aee81275..f918e49a60 100644 --- a/pype/modules/adobe_communicator/lib/__init__.py +++ b/pype/modules/adobe_communicator/lib/__init__.py @@ -1,8 +1,6 @@ -from .io_nonsingleton import DbConnector from .rest_api import AdobeRestApi, PUBLISH_PATHS __all__ = [ "PUBLISH_PATHS", - "DbConnector", "AdobeRestApi" ] diff --git a/pype/modules/adobe_communicator/lib/rest_api.py b/pype/modules/adobe_communicator/lib/rest_api.py index 86739e4d80..35094d10dc 100644 --- a/pype/modules/adobe_communicator/lib/rest_api.py +++ b/pype/modules/adobe_communicator/lib/rest_api.py @@ -2,7 +2,7 @@ import os import sys import copy from pype.modules.rest_api import RestApi, route, abort, CallbackResult -from .io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB from pype.api import config, execute, Logger log = Logger().get_logger("AdobeCommunicator") @@ -14,7 +14,7 @@ PUBLISH_PATHS = [] class AdobeRestApi(RestApi): - dbcon = DbConnector() + dbcon = AvalonMongoDB() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/pype/modules/avalon_apps/rest_api.py b/pype/modules/avalon_apps/rest_api.py index 1cb9e544a7..2408e56bbc 100644 --- a/pype/modules/avalon_apps/rest_api.py +++ b/pype/modules/avalon_apps/rest_api.py @@ -4,14 +4,14 @@ import json import bson import bson.json_util from pype.modules.rest_api import RestApi, abort, CallbackResult -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB class AvalonRestApi(RestApi): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.dbcon = DbConnector() + self.dbcon = AvalonMongoDB() self.dbcon.install() @RestApi.route("/projects/", url_prefix="/avalon", methods="GET") diff --git a/pype/modules/ftrack/actions/action_delete_asset.py b/pype/modules/ftrack/actions/action_delete_asset.py index 27394770e1..7d2dac3320 100644 --- a/pype/modules/ftrack/actions/action_delete_asset.py +++ b/pype/modules/ftrack/actions/action_delete_asset.py @@ -5,7 +5,7 @@ from queue import Queue from bson.objectid import ObjectId from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB class DeleteAssetSubset(BaseAction): @@ -21,7 +21,7 @@ class DeleteAssetSubset(BaseAction): #: roles that are allowed to register this action role_list = ["Pypeclub", "Administrator", "Project Manager"] #: Db connection - dbcon = DbConnector() + dbcon = AvalonMongoDB() splitter = {"type": "label", "value": "---"} action_data_by_id = {} diff --git a/pype/modules/ftrack/actions/action_delete_old_versions.py b/pype/modules/ftrack/actions/action_delete_old_versions.py index 6a4c5a0cae..b55f091fdc 100644 --- a/pype/modules/ftrack/actions/action_delete_old_versions.py +++ b/pype/modules/ftrack/actions/action_delete_old_versions.py @@ -6,7 +6,7 @@ import clique from pymongo import UpdateOne from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB from pype.api import Anatomy import avalon.pipeline @@ -24,7 +24,7 @@ class DeleteOldVersions(BaseAction): role_list = ["Pypeclub", "Project Manager", "Administrator"] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") - dbcon = DbConnector() + dbcon = AvalonMongoDB() inteface_title = "Choose your preferences" splitter_item = {"type": "label", "value": "---"} diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index cf80fd77ff..8812ce9bc7 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -13,7 +13,7 @@ from avalon.vendor import filelink from pype.api import Anatomy, config from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB class Delivery(BaseAction): @@ -24,7 +24,7 @@ class Delivery(BaseAction): role_list = ["Pypeclub", "Administrator", "Project manager"] icon = statics_icon("ftrack", "action_icons", "Delivery.svg") - db_con = DbConnector() + db_con = AvalonMongoDB() def discover(self, session, entities, event): for entity in entities: diff --git a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py index 94ca503233..36f7175768 100644 --- a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py +++ b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py @@ -6,7 +6,7 @@ import json from bson.objectid import ObjectId from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.api import Anatomy -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY @@ -25,7 +25,7 @@ class StoreThumbnailsToAvalon(BaseAction): icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") thumbnail_key = "AVALON_THUMBNAIL_ROOT" - db_con = DbConnector() + db_con = AvalonMongoDB() def discover(self, session, entities, event): for entity in entities: diff --git a/pype/modules/ftrack/events/event_sync_to_avalon.py b/pype/modules/ftrack/events/event_sync_to_avalon.py index efcb74a608..314871f5b3 100644 --- a/pype/modules/ftrack/events/event_sync_to_avalon.py +++ b/pype/modules/ftrack/events/event_sync_to_avalon.py @@ -19,12 +19,12 @@ from pype.modules.ftrack.lib.avalon_sync import ( import ftrack_api from pype.modules.ftrack import BaseEvent -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB class SyncToAvalonEvent(BaseEvent): - dbcon = DbConnector() + dbcon = AvalonMongoDB() interest_entTypes = ["show", "task"] ignore_ent_types = ["Milestone"] diff --git a/pype/modules/ftrack/events/event_user_assigment.py b/pype/modules/ftrack/events/event_user_assigment.py index d1b3439c8f..19a67b745f 100644 --- a/pype/modules/ftrack/events/event_user_assigment.py +++ b/pype/modules/ftrack/events/event_user_assigment.py @@ -4,7 +4,7 @@ import subprocess from pype.modules.ftrack import BaseEvent from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB from bson.objectid import ObjectId @@ -37,7 +37,7 @@ class UserAssigmentEvent(BaseEvent): 3) path to publish files of task user was (de)assigned to """ - db_con = DbConnector() + db_con = AvalonMongoDB() def error(self, *err): for e in err: diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index 4bab1676d4..65a59452da 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -5,7 +5,7 @@ import json import collections import copy -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB import avalon import avalon.api @@ -240,7 +240,7 @@ def get_hierarchical_attributes(session, entity, attr_names, attr_defaults={}): class SyncEntitiesFactory: - dbcon = DbConnector() + dbcon = AvalonMongoDB() project_query = ( "select full_name, name, custom_attributes" diff --git a/pype/tools/launcher/window.py b/pype/tools/launcher/window.py index 13b4abee6e..7c680a927b 100644 --- a/pype/tools/launcher/window.py +++ b/pype/tools/launcher/window.py @@ -4,7 +4,7 @@ import logging from Qt import QtWidgets, QtCore, QtGui from avalon import style -from pype.modules.ftrack.lib.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB from pype.api import resources from avalon.tools import lib as tools_lib @@ -251,7 +251,7 @@ class LauncherWindow(QtWidgets.QDialog): self.log = logging.getLogger( ".".join([__name__, self.__class__.__name__]) ) - self.dbcon = DbConnector() + self.dbcon = AvalonMongoDB() self.setWindowTitle("Launcher") self.setFocusPolicy(QtCore.Qt.StrongFocus) diff --git a/pype/tools/standalonepublish/app.py b/pype/tools/standalonepublish/app.py index d139366a1c..feba46987f 100644 --- a/pype/tools/standalonepublish/app.py +++ b/pype/tools/standalonepublish/app.py @@ -1,7 +1,7 @@ from bson.objectid import ObjectId from Qt import QtWidgets, QtCore from widgets import AssetWidget, FamilyWidget, ComponentsWidget, ShadowWidget -from avalon.tools.libraryloader.io_nonsingleton import DbConnector +from avalon.api import AvalonMongoDB class Window(QtWidgets.QDialog): @@ -10,7 +10,7 @@ class Window(QtWidgets.QDialog): :param parent: Main widget that cares about all GUIs :type parent: QtWidgets.QMainWindow """ - _db = DbConnector() + _db = AvalonMongoDB() _jobs = {} valid_family = False valid_components = False From 73dfecbf1a9f0518e0bd15fb7cfbbe27cced0e45 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 8 Sep 2020 17:53:32 +0200 Subject: [PATCH 157/158] fix tile order for Draft Tile Assembler --- .../maya/publish/submit_maya_deadline.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 747d2727b7..e4048592a7 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -25,6 +25,7 @@ import re import hashlib from datetime import datetime import itertools +from collections import OrderedDict import clique import requests @@ -67,7 +68,7 @@ payload_skeleton = { def _format_tiles( filename, index, tiles_x, tiles_y, - width, height, prefix, origin="blc"): + width, height, prefix): """Generate tile entries for Deadline tile job. Returns two dictionaries - one that can be directly used in Deadline @@ -113,12 +114,14 @@ def _format_tiles( """ tile = 0 out = {"JobInfo": {}, "PluginInfo": {}} - cfg = {} + cfg = OrderedDict() w_space = width / tiles_x h_space = height / tiles_y + cfg["TilesCropped"] = "False" + for tile_x in range(1, tiles_x + 1): - for tile_y in range(1, tiles_y + 1): + for tile_y in reversed(range(1, tiles_y + 1)): tile_prefix = "_tile_{}x{}_{}x{}_".format( tile_x, tile_y, tiles_x, @@ -143,14 +146,13 @@ def _format_tiles( cfg["Tile{}".format(tile)] = new_filename cfg["Tile{}Tile".format(tile)] = new_filename + cfg["Tile{}FileName".format(tile)] = new_filename cfg["Tile{}X".format(tile)] = (tile_x - 1) * w_space - if origin == "blc": - cfg["Tile{}Y".format(tile)] = (tile_y - 1) * h_space - else: - cfg["Tile{}Y".format(tile)] = int(height) - ((tile_y - 1) * h_space) # noqa: E501 - cfg["Tile{}Width".format(tile)] = tile_x * w_space - cfg["Tile{}Height".format(tile)] = tile_y * h_space + cfg["Tile{}Y".format(tile)] = int(height) - (tile_y * h_space) + + cfg["Tile{}Width".format(tile)] = w_space + cfg["Tile{}Height".format(tile)] = h_space tile += 1 return out, cfg @@ -538,7 +540,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "AuxFiles": [], "JobInfo": { "BatchName": payload["JobInfo"]["BatchName"], - "Frames": 0, + "Frames": 1, "Name": "{} - Tile Assembly Job".format( payload["JobInfo"]["Name"]), "OutputDirectory0": @@ -590,7 +592,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): payload["JobInfo"]["Name"], frame, instance.data.get("tilesX") * instance.data.get("tilesY") # noqa: E501 - ) + ) self.log.info( "... preparing job {}".format( new_payload["JobInfo"]["Name"])) From ffa233994e58994192e2715b1acbedea4f3c45ca Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 9 Sep 2020 16:57:19 +0200 Subject: [PATCH 158/158] bump version --- pype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/version.py b/pype/version.py index 9e1a271244..95a6d3a792 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.11.8" +__version__ = "2.12.0"