From f24b912e8a33f74f66398073d057d495c12b6249 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 9 Nov 2021 15:44:44 +0100 Subject: [PATCH 01/60] OP-1950 - added endpoint for configured extensions --- .../webserver_service/webpublish_routes.py | 20 +++++++++++++++++++ .../webserver_service/webserver_cli.py | 10 +++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index 920ed042dc..4a63b0af07 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -12,6 +12,7 @@ from avalon.api import AvalonMongoDB from openpype.lib import OpenPypeMongoConnection from openpype_modules.avalon_apps.rest_api import _RestApiEndpoint from openpype.lib.plugin_tools import parse_json +from openpype.settings import get_project_settings from openpype.lib import PypeLogger @@ -277,3 +278,22 @@ class PublishesStatusEndpoint(_RestApiEndpoint): body=self.resource.encode(output), content_type="application/json" ) + + +class ConfiguredExtensionsEndpoint(_RestApiEndpoint): + """Returns list of extensions which have mapping to family.""" + async def get(self, project_name=None) -> Response: + sett = get_project_settings(project_name) + + configured = [] + collect_conf = sett["webpublisher"]["publish"]["CollectPublishedFiles"] + for _, mapping in collect_conf.get("task_type_to_family", {}).items(): + for _family, config in mapping.items(): + configured.extend(config["extensions"]) + configured = set(configured) + + return Response( + status=200, + body=self.resource.encode(sorted(list(configured))), + content_type="application/json" + ) diff --git a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py index d00d269059..bf828070c1 100644 --- a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py +++ b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py @@ -14,7 +14,8 @@ from .webpublish_routes import ( WebpublisherHiearchyEndpoint, WebpublisherProjectsEndpoint, BatchStatusEndpoint, - PublishesStatusEndpoint + PublishesStatusEndpoint, + ConfiguredExtensionsEndpoint ) @@ -49,6 +50,13 @@ def run_webserver(*args, **kwargs): hiearchy_endpoint.dispatch ) + configured_ext_endpoint = ConfiguredExtensionsEndpoint(resource) + server_manager.add_route( + "GET", + "/api/webpublish/configured_ext/{project_name}", + configured_ext_endpoint.dispatch + ) + # triggers publish webpublisher_task_publish_endpoint = \ WebpublisherBatchPublishEndpoint(resource) From f0c49b8daeaf0140f1c767c0bb20b587542222bb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Nov 2021 12:36:19 +0100 Subject: [PATCH 02/60] OP-1950 - changed returned format of ConfiguredExtensionsEndpoint to dictionary --- .../webserver_service/webpublish_routes.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index 4a63b0af07..e444f5a1fe 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -35,6 +35,8 @@ class RestApiResource: return value.isoformat() if isinstance(value, ObjectId): return str(value) + if isinstance(value, set): + return list(value) raise TypeError(value) @classmethod @@ -281,19 +283,31 @@ class PublishesStatusEndpoint(_RestApiEndpoint): class ConfiguredExtensionsEndpoint(_RestApiEndpoint): - """Returns list of extensions which have mapping to family.""" + """Returns dict of extensions which have mapping to family. + + Returns: + { + "file_exts": [], + "sequence_exts": [] + } + """ async def get(self, project_name=None) -> Response: sett = get_project_settings(project_name) - configured = [] + configured = { + "file_exts": set(), + "sequence_exts": set() + } collect_conf = sett["webpublisher"]["publish"]["CollectPublishedFiles"] for _, mapping in collect_conf.get("task_type_to_family", {}).items(): for _family, config in mapping.items(): - configured.extend(config["extensions"]) - configured = set(configured) + if config["is_sequence"]: + configured["sequence_exts"].update(config["extensions"]) + else: + configured["file_exts"].update(config["extensions"]) return Response( status=200, - body=self.resource.encode(sorted(list(configured))), + body=self.resource.encode(dict(configured)), content_type="application/json" ) From c5ac37e629f655bed9bff00972fc0cafeb35cca4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Nov 2021 12:47:51 +0100 Subject: [PATCH 03/60] OP-1950 - added extensions for Studio Processing Hardcoded for now --- .../hosts/webpublisher/webserver_service/webpublish_routes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index deea894045..a7a1e0920b 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -323,7 +323,9 @@ class ConfiguredExtensionsEndpoint(_RestApiEndpoint): configured = { "file_exts": set(), - "sequence_exts": set() + "sequence_exts": set(), + # workfiles that could have "Studio Procesing" hardcoded for now + "studio_exts": set("psd", "psb", "tvpp", "tvp") } collect_conf = sett["webpublisher"]["publish"]["CollectPublishedFiles"] for _, mapping in collect_conf.get("task_type_to_family", {}).items(): From 30e36cd0019a7cba7aedcb21a372b2a7be292c7a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Nov 2021 12:49:21 +0100 Subject: [PATCH 04/60] OP-1950 - fix - added extensions for Studio Processing --- .../hosts/webpublisher/webserver_service/webpublish_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index a7a1e0920b..a904de0be8 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -325,7 +325,7 @@ class ConfiguredExtensionsEndpoint(_RestApiEndpoint): "file_exts": set(), "sequence_exts": set(), # workfiles that could have "Studio Procesing" hardcoded for now - "studio_exts": set("psd", "psb", "tvpp", "tvp") + "studio_exts": set(["psd", "psb", "tvpp", "tvp"]) } collect_conf = sett["webpublisher"]["publish"]["CollectPublishedFiles"] for _, mapping in collect_conf.get("task_type_to_family", {}).items(): From 7013c6e25eacd1f5c3786588e99d656f0db1514f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Nov 2021 17:36:18 +0100 Subject: [PATCH 05/60] OP-2003 - attempt to remove IS_HEADLESS and replace it with targets WIP, targets not working yet --- .../photoshop/plugins/publish/closePS.py | 3 +- .../publish/collect_remote_instances.py | 4 +- .../webserver_service/webpublish_routes.py | 20 ++---- openpype/lib/remote_publish.py | 26 +++++++ .../plugins/publish/collect_username.py | 6 +- openpype/pype_commands.py | 68 +++++++------------ 6 files changed, 60 insertions(+), 67 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/closePS.py b/openpype/hosts/photoshop/plugins/publish/closePS.py index 19994a0db8..2f0eab0ee5 100644 --- a/openpype/hosts/photoshop/plugins/publish/closePS.py +++ b/openpype/hosts/photoshop/plugins/publish/closePS.py @@ -17,11 +17,10 @@ class ClosePS(pyblish.api.ContextPlugin): active = True hosts = ["photoshop"] + targets = ["remotepublish"] def process(self, context): self.log.info("ClosePS") - if not os.environ.get("IS_HEADLESS"): - return stub = photoshop.stub() self.log.info("Shutting down PS") diff --git a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py index 12f9fa5ab5..c76e15484e 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py @@ -21,6 +21,7 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin): label = "Instances" order = pyblish.api.CollectorOrder hosts = ["photoshop"] + targets = ["remotepublish"] # configurable by Settings color_code_mapping = [] @@ -28,9 +29,6 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin): def process(self, context): self.log.info("CollectRemoteInstances") self.log.info("mapping:: {}".format(self.color_code_mapping)) - if not os.environ.get("IS_HEADLESS"): - self.log.debug("Not headless publishing, skipping.") - return # parse variant if used in webpublishing, comes from webpublisher batch batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA") diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index e34a899c4b..de6a31ecbb 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -12,6 +12,7 @@ from avalon.api import AvalonMongoDB from openpype.lib import OpenPypeMongoConnection from openpype_modules.avalon_apps.rest_api import _RestApiEndpoint from openpype.lib.plugin_tools import parse_json +from openpype.lib.remote_publish import get_task_data from openpype.lib import PypeLogger @@ -205,7 +206,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint): # Make sure targets are set to None for cases that default # would change # - targets argument is not used in 'remotepublishfromapp' - "targets": None + "targets": ["remotepublish"] }, # does publish need to be handled by a queue, eg. only # single process running concurrently? @@ -213,7 +214,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint): } ] - batch_path = os.path.join(self.resource.upload_dir, content["batch"]) + batch_dir = os.path.join(self.resource.upload_dir, content["batch"]) # Default command and arguments command = "remotepublish" @@ -227,18 +228,9 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint): add_to_queue = False if content.get("studio_processing"): - log.info("Post processing called") + log.info("Post processing called for {}".format(batch_dir)) - batch_data = parse_json(os.path.join(batch_path, "manifest.json")) - if not batch_data: - raise ValueError( - "Cannot parse batch manifest in {}".format(batch_path)) - task_dir_name = batch_data["tasks"][0] - task_data = parse_json(os.path.join(batch_path, task_dir_name, - "manifest.json")) - if not task_data: - raise ValueError( - "Cannot parse task manifest in {}".format(task_data)) + task_data = get_task_data(batch_dir) for process_filter in studio_processing_filters: filter_extensions = process_filter.get("extensions") or [] @@ -257,7 +249,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint): args = [ openpype_app, command, - batch_path + batch_dir ] for key, value in add_args.items(): diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 51007cfad2..f7d7955b79 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -8,6 +8,7 @@ import pyblish.api from openpype import uninstall from openpype.lib.mongo import OpenPypeMongoConnection +from openpype.lib.plugin_tools import parse_json def get_webpublish_conn(): @@ -157,3 +158,28 @@ def _get_close_plugin(close_plugin_name, log): return plugin log.warning("Close plugin not found, app might not close.") + + +def get_task_data(batch_dir): + """Return parsed data from first task manifest.json + + Used for `remotepublishfromapp` command where batch contains only + single task with publishable workfile. + + Returns: + (dict) + Throws: + (ValueError) if batch or task manifest not found or broken + """ + batch_data = parse_json(os.path.join(batch_dir, "manifest.json")) + if not batch_data: + raise ValueError( + "Cannot parse batch meta in {} folder".format(batch_dir)) + task_dir_name = batch_data["tasks"][0] + task_data = parse_json(os.path.join(batch_dir, task_dir_name, + "manifest.json")) + if not task_data: + raise ValueError( + "Cannot parse batch meta in {} folder".format(task_data)) + + return task_data diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py b/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py index 844a397066..a5187dd52b 100644 --- a/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py +++ b/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py @@ -27,16 +27,12 @@ class CollectUsername(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.488 label = "Collect ftrack username" hosts = ["webpublisher", "photoshop"] + targets = ["remotepublish", "filespublish"] _context = None def process(self, context): self.log.info("CollectUsername") - # photoshop could be triggered remotely in webpublisher fashion - if os.environ["AVALON_APP"] == "photoshop": - if not os.environ.get("IS_HEADLESS"): - self.log.debug("Regular process, skipping") - return os.environ["FTRACK_API_USER"] = os.environ["FTRACK_BOT_API_USER"] os.environ["FTRACK_API_KEY"] = os.environ["FTRACK_BOT_API_KEY"] diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index fb27de679e..3ea2416776 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -13,7 +13,8 @@ from openpype.lib.remote_publish import ( start_webpublish_log, publish_and_log, fail_batch, - find_variant_key + find_variant_key, + get_task_data ) @@ -138,7 +139,8 @@ class PypeCommands: uninstall() @staticmethod - def remotepublishfromapp(project, batch_dir, host, user, targets=None): + def remotepublishfromapp(project, batch_dir, host_name, + user, targets=None): """Opens installed variant of 'host' and run remote publish there. Currently implemented and tested for Photoshop where customer @@ -153,9 +155,7 @@ class PypeCommands: Runs publish process as user would, in automatic fashion. """ - SLEEP = 5 # seconds for another loop check for concurrently runs - WAIT_FOR = 300 # seconds to wait for conc. runs - + import pyblish.api from openpype.api import Logger from openpype.lib import ApplicationManager @@ -163,54 +163,29 @@ class PypeCommands: log.info("remotepublishphotoshop command") - application_manager = ApplicationManager() - - found_variant_key = find_variant_key(application_manager, host) - - app_name = "{}/{}".format(host, found_variant_key) - - batch_data = None - if batch_dir and os.path.exists(batch_dir): - batch_data = parse_json(os.path.join(batch_dir, "manifest.json")) - - if not batch_data: - raise ValueError( - "Cannot parse batch meta in {} folder".format(batch_dir)) - - asset, task_name, _task_type = get_batch_asset_task_info( - batch_data["context"]) - - # processing from app expects JUST ONE task in batch and 1 workfile - task_dir_name = batch_data["tasks"][0] - task_data = parse_json(os.path.join(batch_dir, task_dir_name, - "manifest.json")) + task_data = get_task_data(batch_dir) workfile_path = os.path.join(batch_dir, - task_dir_name, + task_data["task"], task_data["files"][0]) print("workfile_path {}".format(workfile_path)) - _, batch_id = os.path.split(batch_dir) + batch_id = task_data["batch"] dbcon = get_webpublish_conn() # safer to start logging here, launch might be broken altogether _id = start_webpublish_log(dbcon, batch_id, user) - in_progress = True - slept_times = 0 - while in_progress: - batches_in_progress = list(dbcon.find({ - "status": "in_progress" - })) - if len(batches_in_progress) > 1: - if slept_times * SLEEP >= WAIT_FOR: - fail_batch(_id, batches_in_progress, dbcon) + batches_in_progress = list(dbcon.find({"status": "in_progress"})) + if len(batches_in_progress) > 1: + fail_batch(_id, batches_in_progress, dbcon) + print("Another batch running, probably stuck, ask admin for help") - print("Another batch running, sleeping for a bit") - time.sleep(SLEEP) - slept_times += 1 - else: - in_progress = False + asset, task_name, _ = get_batch_asset_task_info(task_data["context"]) + + application_manager = ApplicationManager() + found_variant_key = find_variant_key(application_manager, host_name) + app_name = "{}/{}".format(host_name, found_variant_key) # must have for proper launch of app env = get_app_environments_for_context( @@ -222,9 +197,16 @@ class PypeCommands: os.environ.update(env) os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir - os.environ["IS_HEADLESS"] = "true" # must pass identifier to update log lines for a batch os.environ["BATCH_LOG_ID"] = str(_id) + os.environ["IS_HEADLESS"] = 'true' + + pyblish.api.register_host(host_name) + if targets: + if isinstance(targets, str): + targets = [targets] + for target in targets: + pyblish.api.register_target(target) data = { "last_workfile_path": workfile_path, From ac70ecad772cb4194f9d0f2468fdd1f3a39577d4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 15 Nov 2021 11:44:08 +0100 Subject: [PATCH 06/60] OP-2003 - updated targets env --- openpype/pype_commands.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 3ea2416776..521d9159d6 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -199,14 +199,19 @@ class PypeCommands: os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir # must pass identifier to update log lines for a batch os.environ["BATCH_LOG_ID"] = str(_id) - os.environ["IS_HEADLESS"] = 'true' + os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib pyblish.api.register_host(host_name) if targets: if isinstance(targets, str): targets = [targets] + current_targets = os.environ.get("PYBLISH_TARGETS", "").split( + os.pathsep) for target in targets: - pyblish.api.register_target(target) + current_targets.append(target) + + os.environ["PYBLISH_TARGETS"] = os.pathsep.join( + set(current_targets)) data = { "last_workfile_path": workfile_path, From 920b908cb16f3268dcfd1df65f5be701a68ab596 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 17:18:39 +0100 Subject: [PATCH 07/60] store new ftrack id to cached project document --- .../ftrack/event_handlers_server/event_sync_to_avalon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py index 178dfc74c7..134cec508f 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -194,6 +194,7 @@ class SyncToAvalonEvent(BaseEvent): ftrack_id = proj["data"].get("ftrackId") if ftrack_id is None: ftrack_id = self._update_project_ftrack_id() + proj["data"]["ftrackId"] = ftrack_id self._avalon_ents_by_ftrack_id[ftrack_id] = proj for ent in ents: ftrack_id = ent["data"].get("ftrackId") From b36ffc85dda2c6a3b842bc7e7f21d41f7c01a1a9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 17:39:38 +0100 Subject: [PATCH 08/60] skip removed projects --- .../ftrack/event_handlers_server/event_sync_to_avalon.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py index 134cec508f..a4982627ff 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -585,6 +585,10 @@ class SyncToAvalonEvent(BaseEvent): continue ftrack_id = ftrack_id[0] + # Skip deleted projects + if action == "remove" and entityType == "show": + return True + # task modified, collect parent id of task, handle separately if entity_type.lower() == "task": changes = ent_info.get("changes") or {} From dfb6b0c12f55eece9a32cb1e7def1d73789f1207 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 17:53:39 +0100 Subject: [PATCH 09/60] create global tasks_widget in utils --- openpype/tools/utils/tasks_widget.py | 295 +++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 openpype/tools/utils/tasks_widget.py diff --git a/openpype/tools/utils/tasks_widget.py b/openpype/tools/utils/tasks_widget.py new file mode 100644 index 0000000000..55b0917f30 --- /dev/null +++ b/openpype/tools/utils/tasks_widget.py @@ -0,0 +1,295 @@ +from Qt import QtWidgets, QtCore, QtGui + +from avalon import style +from avalon.vendor import qtawesome + +from .constants import ( + TASK_ORDER_ROLE, + TASK_TYPE_ROLE, + TASK_NAME_ROLE +) + + +class TasksModel(QtGui.QStandardItemModel): + """A model listing the tasks combined for a list of assets""" + def __init__(self, dbcon, parent=None): + super(TasksModel, self).__init__(parent=parent) + self.dbcon = dbcon + self.setHeaderData( + 0, QtCore.Qt.Horizontal, "Tasks", QtCore.Qt.DisplayRole + ) + self._default_icon = qtawesome.icon( + "fa.male", + color=style.colors.default + ) + self._no_tasks_icon = qtawesome.icon( + "fa.exclamation-circle", + color=style.colors.mid + ) + self._cached_icons = {} + self._project_task_types = {} + + self._empty_tasks_item = None + self._last_asset_id = None + self._loaded_project_name = None + + def _context_is_valid(self): + if self.dbcon.Session.get("AVALON_PROJECT"): + return True + return False + + def refresh(self): + self._refresh_task_types() + self.set_asset_id(self._last_asset_id) + + def _refresh_task_types(self): + # Get the project configured icons from database + task_types = {} + if self._context_is_valid(): + project = self.dbcon.find_one( + {"type": "project"}, + {"config.tasks"} + ) + task_types = project["config"].get("tasks") or task_types + self._project_task_types = task_types + + def _try_get_awesome_icon(self, icon_name): + icon = None + if icon_name: + try: + icon = qtawesome.icon( + "fa.{}".format(icon_name), + color=style.colors.default + ) + + except Exception: + pass + return icon + + def headerData(self, section, orientation, role=None): + if role is None: + role = QtCore.Qt.EditRole + # Show nice labels in the header + if section == 0: + if ( + role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) + and orientation == QtCore.Qt.Horizontal + ): + return "Tasks" + + return super(TasksModel, self).headerData(section, orientation, role) + + def _get_icon(self, task_icon, task_type_icon): + if task_icon in self._cached_icons: + return self._cached_icons[task_icon] + + icon = self._try_get_awesome_icon(task_icon) + if icon is not None: + self._cached_icons[task_icon] = icon + return icon + + if task_type_icon in self._cached_icons: + icon = self._cached_icons[task_type_icon] + self._cached_icons[task_icon] = icon + return icon + + icon = self._try_get_awesome_icon(task_type_icon) + if icon is None: + icon = self._default_icon + + self._cached_icons[task_icon] = icon + self._cached_icons[task_type_icon] = icon + + return icon + + def set_asset_id(self, asset_id): + asset_doc = None + if self._context_is_valid(): + asset_doc = self.dbcon.find_one( + {"_id": asset_id}, + {"data.tasks": True} + ) + self._set_asset(asset_doc) + + def _get_empty_task_item(self): + if self._empty_tasks_item is None: + item = QtGui.QStandardItem("No task") + item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + self._empty_tasks_item = item + return self._empty_tasks_item + + def _set_asset(self, asset_doc): + """Set assets to track by their database id + + Arguments: + asset_doc (dict): Asset document from MongoDB. + """ + asset_tasks = {} + self._last_asset_id = None + if asset_doc: + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + self._last_asset_id = asset_doc["_id"] + + root_item = self.invisibleRootItem() + root_item.removeRows(0, root_item.rowCount()) + + items = [] + for task_name, task_info in asset_tasks.items(): + task_icon = task_info.get("icon") + task_type = task_info.get("type") + task_order = task_info.get("order") + task_type_info = self._project_task_types.get(task_type) or {} + task_type_icon = task_type_info.get("icon") + icon = self._get_icon(task_icon, task_type_icon) + + label = "{} ({})".format(task_name, task_type or "type N/A") + item = QtGui.QStandardItem(label) + item.setData(task_name, TASK_NAME_ROLE) + item.setData(task_type, TASK_TYPE_ROLE) + item.setData(task_order, TASK_ORDER_ROLE) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) + items.append(item) + + if not items: + item = QtGui.QStandardItem("No task") + item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + items.append(item) + + root_item.appendRows(items) + + +class TasksProxyModel(QtCore.QSortFilterProxyModel): + def lessThan(self, x_index, y_index): + x_order = x_index.data(TASK_ORDER_ROLE) + y_order = y_index.data(TASK_ORDER_ROLE) + if x_order is not None and y_order is not None: + if x_order < y_order: + return True + if x_order > y_order: + return False + + elif x_order is None and y_order is not None: + return True + + elif y_order is None and x_order is not None: + return False + + x_name = x_index.data(QtCore.Qt.DisplayRole) + y_name = y_index.data(QtCore.Qt.DisplayRole) + if x_name == y_name: + return True + + if x_name == tuple(sorted((x_name, y_name)))[0]: + return True + return False + + +class TasksWidget(QtWidgets.QWidget): + """Widget showing active Tasks""" + + task_changed = QtCore.Signal() + + def __init__(self, dbcon, parent=None): + super(TasksWidget, self).__init__(parent) + + tasks_view = QtWidgets.QTreeView(self) + tasks_view.setIndentation(0) + tasks_view.setSortingEnabled(True) + tasks_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) + + header_view = tasks_view.header() + header_view.setSortIndicator(0, QtCore.Qt.AscendingOrder) + + tasks_model = TasksModel(dbcon) + tasks_proxy = TasksProxyModel() + tasks_proxy.setSourceModel(tasks_model) + tasks_view.setModel(tasks_proxy) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(tasks_view) + + selection_model = tasks_view.selectionModel() + selection_model.currentChanged.connect(self.task_changed) + + self._tasks_model = tasks_model + self._tasks_proxy = tasks_proxy + self._tasks_view = tasks_view + + self._last_selected_task_name = None + + def refresh(self): + self._tasks_model.refresh() + + def set_asset_id(self, asset_id): + # Asset deselected + if asset_id is None: + 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_selected_task_name() + if current: + self._last_selected_task_name = current + + self._tasks_model.set_asset_id(asset_id) + + if self._last_selected_task_name: + self.select_task_name(self._last_selected_task_name) + + # Force a task changed emit. + self.task_changed.emit() + + def select_task_name(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. + + """ + task_view_model = self._tasks_view.model() + if not task_view_model: + return + + # Clear selection + selection_model = self._tasks_view.selectionModel() + selection_model.clearSelection() + + # Select the task + mode = selection_model.Select | selection_model.Rows + for row in range(task_view_model.rowCount()): + index = task_view_model.index(row, 0) + name = index.data(TASK_NAME_ROLE) + if name == task_name: + selection_model.select(index, mode) + + # Set the currently active index + self._tasks_view.setCurrentIndex(index) + break + + def get_selected_task_name(self): + """Return name of task at current index (selected) + + Returns: + str: Name of the current task. + + """ + index = self._tasks_view.currentIndex() + selection_model = self._tasks_view.selectionModel() + if index.isValid() and selection_model.isSelected(index): + return index.data(TASK_NAME_ROLE) + return None + + def get_selected_task_type(self): + index = self._tasks_view.currentIndex() + selection_model = self._tasks_view.selectionModel() + if index.isValid() and selection_model.isSelected(index): + return index.data(TASK_TYPE_ROLE) + return None From b2a74243387137d21c4b89565a51d24de27a6308 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 17:53:50 +0100 Subject: [PATCH 10/60] fixed TASK_ORDER_ROLE --- openpype/tools/utils/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/utils/constants.py b/openpype/tools/utils/constants.py index 5b6f4126c9..33bdf43c08 100644 --- a/openpype/tools/utils/constants.py +++ b/openpype/tools/utils/constants.py @@ -7,7 +7,7 @@ PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 102 TASK_NAME_ROLE = QtCore.Qt.UserRole + 301 TASK_TYPE_ROLE = QtCore.Qt.UserRole + 302 -TASK_ORDER_ROLE = QtCore.Qt.UserRole + 403 +TASK_ORDER_ROLE = QtCore.Qt.UserRole + 303 LOCAL_PROVIDER_ROLE = QtCore.Qt.UserRole + 500 # provider of active site REMOTE_PROVIDER_ROLE = QtCore.Qt.UserRole + 501 # provider of remote site From e7357008768c6aca6373f74a64a6e5b118cb3e8c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 17:55:05 +0100 Subject: [PATCH 11/60] use TasksWidget in context dialog --- openpype/tools/context_dialog/window.py | 39 ++++++++----------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py index 124a1beda3..bc5ec919b3 100644 --- a/openpype/tools/context_dialog/window.py +++ b/openpype/tools/context_dialog/window.py @@ -8,14 +8,12 @@ from openpype import style from openpype.tools.utils.lib import center_window from openpype.tools.utils.widgets import AssetWidget from openpype.tools.utils.constants import ( - TASK_NAME_ROLE, PROJECT_NAME_ROLE ) +from openpype.tools.utils.tasks_widget import TasksWidget from openpype.tools.utils.models import ( ProjectModel, - ProjectSortFilterProxy, - TasksModel, - TasksProxyModel + ProjectSortFilterProxy ) @@ -77,15 +75,11 @@ class ContextDialog(QtWidgets.QDialog): left_side_layout.addWidget(assets_widget) # Right side of window contains only tasks - task_view = QtWidgets.QListView(main_splitter) - task_model = TasksModel(dbcon) - task_proxy = TasksProxyModel() - task_proxy.setSourceModel(task_model) - task_view.setModel(task_proxy) + tasks_widget = TasksWidget(dbcon, main_splitter) # Add widgets to main splitter main_splitter.addWidget(left_side_widget) - main_splitter.addWidget(task_view) + main_splitter.addWidget(task_widgets) # Set stretch of both sides main_splitter.setStretchFactor(0, 7) @@ -119,7 +113,7 @@ class ContextDialog(QtWidgets.QDialog): assets_widget.selection_changed.connect(self._on_asset_change) assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger) assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished) - task_view.selectionModel().selectionChanged.connect( + tasks_widget.task_changed.selectionChanged.connect( self._on_task_change ) ok_btn.clicked.connect(self._on_ok_click) @@ -133,9 +127,7 @@ class ContextDialog(QtWidgets.QDialog): self._assets_widget = assets_widget - self._task_view = task_view - self._task_model = task_model - self._task_proxy = task_proxy + self._tasks_widget = tasks_widget self._ok_btn = ok_btn @@ -279,15 +271,13 @@ class ContextDialog(QtWidgets.QDialog): self._dbcon.Session["AVALON_ASSET"] = self._set_context_asset self._assets_widget.setEnabled(False) self._assets_widget.select_assets(self._set_context_asset) - self._set_asset_to_task_model() + self._set_asset_to_task_widget() else: self._assets_widget.setEnabled(True) self._assets_widget.set_current_asset_btn_visibility(False) # Refresh tasks - self._task_model.refresh() - # Sort tasks - self._task_proxy.sort(0, QtCore.Qt.AscendingOrder) + self._tasks_widget.refresh() self._ignore_value_changes = False @@ -314,20 +304,19 @@ class ContextDialog(QtWidgets.QDialog): """Selected assets have changed""" if self._ignore_value_changes: return - self._set_asset_to_task_model() + self._set_asset_to_task_widget() def _on_task_change(self): self._validate_strict() - def _set_asset_to_task_model(self): + def _set_asset_to_task_widget(self): # filter None docs they are silo asset_docs = self._assets_widget.get_selected_assets() asset_ids = [asset_doc["_id"] for asset_doc in asset_docs] asset_id = None if asset_ids: asset_id = asset_ids[0] - self._task_model.set_asset_id(asset_id) - self._task_proxy.sort(0, QtCore.Qt.AscendingOrder) + self._tasks_widget.set_asset_id(asset_id) def _confirm_values(self): """Store values to output.""" @@ -355,11 +344,7 @@ class ContextDialog(QtWidgets.QDialog): def get_selected_task(self): """Currently selected task.""" - task_name = None - index = self._task_view.selectionModel().currentIndex() - if index.isValid(): - task_name = index.data(TASK_NAME_ROLE) - return task_name + return self._tasks_widget.get_selected_task_name() def _validate_strict(self): if not self._strict: From ac19a75aa0e6fbfeecc37c9f7abdaad6146b4232 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:26:16 +0100 Subject: [PATCH 12/60] use selctionChanged signal --- openpype/tools/utils/tasks_widget.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/tasks_widget.py b/openpype/tools/utils/tasks_widget.py index 55b0917f30..f2f43442d1 100644 --- a/openpype/tools/utils/tasks_widget.py +++ b/openpype/tools/utils/tasks_widget.py @@ -213,7 +213,7 @@ class TasksWidget(QtWidgets.QWidget): layout.addWidget(tasks_view) selection_model = tasks_view.selectionModel() - selection_model.currentChanged.connect(self.task_changed) + selection_model.selectionChanged.connect(self._on_task_change) self._tasks_model = tasks_model self._tasks_proxy = tasks_proxy @@ -293,3 +293,6 @@ class TasksWidget(QtWidgets.QWidget): if index.isValid() and selection_model.isSelected(index): return index.data(TASK_TYPE_ROLE) return None + + def _on_task_change(self): + self.task_changed.emit() From 811aa44e97eb702c9ac430aeac3afd6d34c0d0bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:26:40 +0100 Subject: [PATCH 13/60] use deselectable view --- openpype/tools/utils/tasks_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/tasks_widget.py b/openpype/tools/utils/tasks_widget.py index f2f43442d1..9ffcde2885 100644 --- a/openpype/tools/utils/tasks_widget.py +++ b/openpype/tools/utils/tasks_widget.py @@ -3,6 +3,7 @@ from Qt import QtWidgets, QtCore, QtGui from avalon import style from avalon.vendor import qtawesome +from .views import DeselectableTreeView from .constants import ( TASK_ORDER_ROLE, TASK_TYPE_ROLE, @@ -195,7 +196,7 @@ class TasksWidget(QtWidgets.QWidget): def __init__(self, dbcon, parent=None): super(TasksWidget, self).__init__(parent) - tasks_view = QtWidgets.QTreeView(self) + tasks_view = DeselectableTreeView(self) tasks_view.setIndentation(0) tasks_view.setSortingEnabled(True) tasks_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) From 3cb50adabd2eca08b2f1d1552453d0821621e318 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:28:40 +0100 Subject: [PATCH 14/60] don't use inner attributes of asset panel --- openpype/tools/launcher/window.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 454445824e..a6e1e12e26 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -197,6 +197,10 @@ class AssetsPanel(QtWidgets.QWidget): btn_size = self.project_bar.height() self._btn_back.setFixedSize(QtCore.QSize(btn_size, btn_size)) + def select_task_name(self, task_name): + self._on_asset_changed() + self._tasks_widget.select_task_name(task_name) + def on_project_changed(self): self.session_changed.emit() @@ -448,5 +452,4 @@ class LauncherWindow(QtWidgets.QDialog): if task_name: # requires a forced refresh first - self.asset_panel.on_asset_changed() - self.asset_panel.tasks_widget.select_task(task_name) + self.asset_panel.select_task_name(task_name) From 7bb9127c7fc1c0368d7b5a0730cafb3db9886b7a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:29:53 +0100 Subject: [PATCH 15/60] use private method names for signal callbacks --- openpype/tools/launcher/window.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index a6e1e12e26..5a6fe8e837 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -176,10 +176,10 @@ class AssetsPanel(QtWidgets.QWidget): layout.addWidget(body) # signals - project_handler.project_changed.connect(self.on_project_changed) - assets_widget.selection_changed.connect(self.on_asset_changed) - assets_widget.refreshed.connect(self.on_asset_changed) - tasks_widget.task_changed.connect(self.on_task_change) + project_handler.project_changed.connect(self._on_project_changed) + assets_widget.selection_changed.connect(self._on_asset_changed) + assets_widget.refreshed.connect(self._on_asset_changed) + tasks_widget.task_changed.connect(self._on_task_change) btn_back.clicked.connect(self.back_clicked) @@ -201,12 +201,12 @@ class AssetsPanel(QtWidgets.QWidget): self._on_asset_changed() self._tasks_widget.select_task_name(task_name) - def on_project_changed(self): + def _on_project_changed(self): self.session_changed.emit() self.assets_widget.refresh() - def on_asset_changed(self): + def _on_asset_changed(self): """Callback on asset selection changed This updates the task view. @@ -243,7 +243,7 @@ class AssetsPanel(QtWidgets.QWidget): asset_id = asset_doc["_id"] self.tasks_widget.set_asset(asset_id) - def on_task_change(self): + def _on_task_change(self): task_name = self.tasks_widget.get_current_task() self.dbcon.Session["AVALON_TASK"] = task_name self.session_changed.emit() From 621029587543cfd7abf0e3574be2f82c6c7429be Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:32:04 +0100 Subject: [PATCH 16/60] replaces tasks widget --- openpype/tools/launcher/window.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 5a6fe8e837..bae2362205 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -9,13 +9,14 @@ from openpype import style from openpype.api import resources from openpype.tools.utils.widgets import AssetWidget +from openpype.tools.utils.tasks_widget import TasksWidget + from avalon.vendor import qtawesome from .models import ProjectModel from .lib import get_action_label, ProjectHandler from .widgets import ( ProjectBar, ActionBar, - TasksWidget, ActionHistory, SlidePageWidget ) @@ -186,7 +187,7 @@ class AssetsPanel(QtWidgets.QWidget): self.project_handler = project_handler self.project_bar = project_bar self.assets_widget = assets_widget - self.tasks_widget = tasks_widget + self._tasks_widget = tasks_widget self._btn_back = btn_back def showEvent(self, event): @@ -241,10 +242,10 @@ class AssetsPanel(QtWidgets.QWidget): asset_id = None if asset_doc: asset_id = asset_doc["_id"] - self.tasks_widget.set_asset(asset_id) + self._tasks_widget.set_asset_id(asset_id) def _on_task_change(self): - task_name = self.tasks_widget.get_current_task() + task_name = self._tasks_widget.get_selected_task_name() self.dbcon.Session["AVALON_TASK"] = task_name self.session_changed.emit() From d9ad32fcea068fb78e3a9cec59a130e5741689b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:32:43 +0100 Subject: [PATCH 17/60] removed tasks model and taskswidget from launcher --- openpype/tools/launcher/models.py | 96 ------------------------------ openpype/tools/launcher/widgets.py | 88 +-------------------------- 2 files changed, 1 insertion(+), 183 deletions(-) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index f87871409e..427475cb4b 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -19,102 +19,6 @@ from openpype.lib import ApplicationManager log = logging.getLogger(__name__) -class TaskModel(QtGui.QStandardItemModel): - """A model listing the tasks combined for a list of assets""" - - def __init__(self, dbcon, parent=None): - super(TaskModel, self).__init__(parent=parent) - self.dbcon = dbcon - - 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 not self.dbcon.Session.get("AVALON_PROJECT"): - return - - # Get the project configured icons from database - project = self.dbcon.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(self.dbcon.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 = set() - for asset_doc in asset_docs: - asset_tasks = asset_doc.get("data", {}).get("tasks") or set() - 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 in sorted(task_names): - 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): def __init__(self, dbcon, parent=None): super(ActionModel, self).__init__(parent=parent) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 5e01488ae6..4f4f9799ff 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -6,7 +6,7 @@ from avalon.vendor import qtawesome from .delegates import ActionDelegate from . import lib -from .models import TaskModel, ActionModel +from .models import ActionModel from openpype.tools.flickcharm import FlickCharm from .constants import ( ACTION_ROLE, @@ -261,92 +261,6 @@ class ActionBar(QtWidgets.QWidget): 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, dbcon, parent=None): - super(TasksWidget, self).__init__(parent) - - self.dbcon = dbcon - - view = QtWidgets.QTreeView(self) - view.setIndentation(0) - view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) - model = TaskModel(self.dbcon) - 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) From eb155525f7038d8df13af76480ca4a87b883e261 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:32:57 +0100 Subject: [PATCH 18/60] action bar has 0 margins --- openpype/tools/launcher/widgets.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 4f4f9799ff..edda8d08b5 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -90,9 +90,6 @@ class ActionBar(QtWidgets.QWidget): self.project_handler = project_handler self.dbcon = dbcon - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(8, 0, 8, 0) - view = QtWidgets.QListView(self) view.setProperty("mode", "icon") view.setObjectName("IconView") @@ -116,6 +113,8 @@ class ActionBar(QtWidgets.QWidget): ) view.setItemDelegate(delegate) + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(view) self.model = model From 235cf38818f743468bb4d8118489507d311f162b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:33:34 +0100 Subject: [PATCH 19/60] project part has only layout without widget --- openpype/tools/launcher/window.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index bae2362205..f38c9b06a6 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -124,20 +124,18 @@ class AssetsPanel(QtWidgets.QWidget): self.dbcon = dbcon - # project bar - project_bar_widget = QtWidgets.QWidget(self) - - layout = QtWidgets.QHBoxLayout(project_bar_widget) - layout.setSpacing(4) - + # Project bar btn_back_icon = qtawesome.icon("fa.angle-left", color="white") - btn_back = QtWidgets.QPushButton(project_bar_widget) + btn_back = QtWidgets.QPushButton(self) btn_back.setIcon(btn_back_icon) - project_bar = ProjectBar(project_handler, project_bar_widget) + project_bar = ProjectBar(project_handler, self) - layout.addWidget(btn_back) - layout.addWidget(project_bar) + project_bar_layout = QtWidgets.QHBoxLayout() + project_bar_layout.setContentsMargins(0, 0, 0, 0) + project_bar_layout.setSpacing(4) + project_bar_layout.addWidget(btn_back) + project_bar_layout.addWidget(project_bar) # assets assets_proxy_widgets = QtWidgets.QWidget(self) @@ -173,7 +171,7 @@ class AssetsPanel(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) - layout.addWidget(project_bar_widget) + layout.addLayout(project_bar_layout) layout.addWidget(body) # signals From 60efc644d8aa3b4efdcf121a178d9ac57a5f4def Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:34:02 +0100 Subject: [PATCH 20/60] asset widget does not have proxy --- openpype/tools/launcher/window.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index f38c9b06a6..654716fae0 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -137,13 +137,8 @@ class AssetsPanel(QtWidgets.QWidget): project_bar_layout.addWidget(btn_back) project_bar_layout.addWidget(project_bar) - # assets - assets_proxy_widgets = QtWidgets.QWidget(self) - assets_proxy_widgets.setContentsMargins(0, 0, 0, 0) - assets_layout = QtWidgets.QVBoxLayout(assets_proxy_widgets) - assets_widget = AssetWidget( - dbcon=self.dbcon, parent=assets_proxy_widgets - ) + # Assets widget + assets_widget = AssetWidget(dbcon=self.dbcon, parent=self) # Make assets view flickable flick = FlickCharm(parent=self) @@ -162,7 +157,7 @@ class AssetsPanel(QtWidgets.QWidget): QtWidgets.QSizePolicy.Expanding ) body.setOrientation(QtCore.Qt.Horizontal) - body.addWidget(assets_proxy_widgets) + body.addWidget(assets_widget) body.addWidget(tasks_widget) body.setStretchFactor(0, 100) body.setStretchFactor(1, 65) From 0961bca9a8b578cb6e1dafbf3805eebe318e0d6b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:34:49 +0100 Subject: [PATCH 21/60] statusbar widget replaced with layout --- openpype/tools/launcher/window.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 654716fae0..4e4d4cb564 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -281,17 +281,15 @@ class LauncherWindow(QtWidgets.QDialog): actions_bar = ActionBar(project_handler, self.dbcon, self) # statusbar - statusbar = QtWidgets.QWidget() - layout = QtWidgets.QHBoxLayout(statusbar) - message_label = QtWidgets.QLabel() message_label.setFixedHeight(15) action_history = ActionHistory() action_history.setStatusTip("Show Action History") - layout.addWidget(message_label) - layout.addWidget(action_history) + status_layout = QtWidgets.QHBoxLayout() + status_layout.addWidget(message_label, 1) + status_layout.addWidget(action_history, 0) # Vertically split Pages and Actions body = QtWidgets.QSplitter() @@ -312,9 +310,9 @@ class LauncherWindow(QtWidgets.QDialog): layout = QtWidgets.QVBoxLayout(self) layout.addWidget(body) - layout.addWidget(statusbar) layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(status_layout) self.project_handler = project_handler From 04b3d2b55f94a3ef0753f20482d257e46e5fd084 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:35:58 +0100 Subject: [PATCH 22/60] fixed layouts --- openpype/tools/launcher/window.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 4e4d4cb564..56de9de435 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -92,8 +92,6 @@ class ProjectsPanel(QtWidgets.QWidget): def __init__(self, project_handler, parent=None): super(ProjectsPanel, self).__init__(parent=parent) - layout = QtWidgets.QVBoxLayout(self) - view = ProjectIconView(parent=self) view.setSelectionMode(QtWidgets.QListView.NoSelection) flick = FlickCharm(parent=self) @@ -101,6 +99,8 @@ class ProjectsPanel(QtWidgets.QWidget): view.setModel(project_handler.model) + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(view) view.clicked.connect(self.on_clicked) @@ -146,11 +146,12 @@ class AssetsPanel(QtWidgets.QWidget): assets_widget.view.setVerticalScrollMode( assets_widget.view.ScrollPerPixel ) - assets_layout.addWidget(assets_widget) - # tasks + # Tasks widget tasks_widget = TasksWidget(self.dbcon, self) - body = QtWidgets.QSplitter() + + # Body + body = QtWidgets.QSplitter(self) body.setContentsMargins(0, 0, 0, 0) body.setSizePolicy( QtWidgets.QSizePolicy.Expanding, @@ -165,7 +166,6 @@ class AssetsPanel(QtWidgets.QWidget): # main layout layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) layout.addLayout(project_bar_layout) layout.addWidget(body) @@ -281,10 +281,9 @@ class LauncherWindow(QtWidgets.QDialog): actions_bar = ActionBar(project_handler, self.dbcon, self) # statusbar - message_label = QtWidgets.QLabel() - message_label.setFixedHeight(15) + message_label = QtWidgets.QLabel(self) - action_history = ActionHistory() + action_history = ActionHistory(self) action_history.setStatusTip("Show Action History") status_layout = QtWidgets.QHBoxLayout() @@ -292,7 +291,7 @@ class LauncherWindow(QtWidgets.QDialog): status_layout.addWidget(action_history, 0) # Vertically split Pages and Actions - body = QtWidgets.QSplitter() + body = QtWidgets.QSplitter(self) body.setContentsMargins(0, 0, 0, 0) body.setSizePolicy( QtWidgets.QSizePolicy.Expanding, @@ -310,8 +309,6 @@ class LauncherWindow(QtWidgets.QDialog): layout = QtWidgets.QVBoxLayout(self) layout.addWidget(body) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(status_layout) self.project_handler = project_handler From 1ccfd405d3ef8a32ac5d75aecbace65a09579fe3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:36:30 +0100 Subject: [PATCH 23/60] replaced unsetting of message with timer --- openpype/tools/launcher/window.py | 33 +++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 56de9de435..8d6b609282 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -245,6 +245,7 @@ class AssetsPanel(QtWidgets.QWidget): class LauncherWindow(QtWidgets.QDialog): """Launcher interface""" + message_timeout = 5000 def __init__(self, parent=None): super(LauncherWindow, self).__init__(parent) @@ -311,15 +312,11 @@ class LauncherWindow(QtWidgets.QDialog): layout.addWidget(body) layout.addLayout(status_layout) - self.project_handler = project_handler + message_timer = QtCore.QTimer() + message_timer.setInterval(self.message_timeout) + message_timer.setSingleShot(True) - 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.page_slider = page_slider - self._page = 0 + message_timer.timeout.connect(self._on_message_timeout) # signals actions_bar.action_clicked.connect(self.on_action_clicked) @@ -331,6 +328,19 @@ class LauncherWindow(QtWidgets.QDialog): self.resize(520, 740) + self._page = 0 + + self._message_timer = message_timer + + self.project_handler = project_handler + + 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.page_slider = page_slider + def showEvent(self, event): self.project_handler.set_active(True) self.project_handler.start_timer(True) @@ -356,9 +366,12 @@ class LauncherWindow(QtWidgets.QDialog): self._page = page self.page_slider.slide_view(page, direction=direction) + def _on_message_timeout(self): + self._message_label.setText("") + def echo(self, message): - self.message_label.setText(str(message)) - QtCore.QTimer.singleShot(5000, lambda: self.message_label.setText("")) + self._message_label.setText(str(message)) + self._message_timer.start() self.log.debug(message) def on_session_changed(self): From ee83e13ec6cbdb6e02030e4faa4c2cca83fd745b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:41:05 +0100 Subject: [PATCH 24/60] minor fixes in context dialog --- openpype/tools/context_dialog/window.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py index bc5ec919b3..0fd9679e83 100644 --- a/openpype/tools/context_dialog/window.py +++ b/openpype/tools/context_dialog/window.py @@ -79,7 +79,7 @@ class ContextDialog(QtWidgets.QDialog): # Add widgets to main splitter main_splitter.addWidget(left_side_widget) - main_splitter.addWidget(task_widgets) + main_splitter.addWidget(tasks_widgets) # Set stretch of both sides main_splitter.setStretchFactor(0, 7) @@ -271,7 +271,7 @@ class ContextDialog(QtWidgets.QDialog): self._dbcon.Session["AVALON_ASSET"] = self._set_context_asset self._assets_widget.setEnabled(False) self._assets_widget.select_assets(self._set_context_asset) - self._set_asset_to_task_widget() + self._set_asset_to_tasks_widget() else: self._assets_widget.setEnabled(True) self._assets_widget.set_current_asset_btn_visibility(False) @@ -304,12 +304,12 @@ class ContextDialog(QtWidgets.QDialog): """Selected assets have changed""" if self._ignore_value_changes: return - self._set_asset_to_task_widget() + self._set_asset_to_tasks_widget() def _on_task_change(self): self._validate_strict() - def _set_asset_to_task_widget(self): + def _set_asset_to_tasks_widget(self): # filter None docs they are silo asset_docs = self._assets_widget.get_selected_assets() asset_ids = [asset_doc["_id"] for asset_doc in asset_docs] From af05e935ccac58d9cbb87bec99e0aba19f76a708 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:49:52 +0100 Subject: [PATCH 25/60] replaced taskswidget in workfiles tool --- openpype/tools/workfiles/app.py | 136 +++----------------------------- 1 file changed, 12 insertions(+), 124 deletions(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 4135eeccc9..edea7bb1e0 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -15,16 +15,9 @@ from openpype.tools.utils.lib import ( schedule, qt_app_context ) from openpype.tools.utils.widgets import AssetWidget +from openpype.tools.utils.tasks_widget import TasksWidget from openpype.tools.utils.delegates import PrettyTimeDelegate -from openpype.tools.utils.constants import ( - TASK_NAME_ROLE, - TASK_TYPE_ROLE -) -from openpype.tools.utils.models import ( - TasksModel, - TasksProxyModel -) from .model import FilesModel from .view import FilesView @@ -323,110 +316,6 @@ class NameWindow(QtWidgets.QDialog): ) -class TasksWidget(QtWidgets.QWidget): - """Widget showing active Tasks""" - - task_changed = QtCore.Signal() - - def __init__(self, dbcon=None, parent=None): - super(TasksWidget, self).__init__(parent) - - tasks_view = QtWidgets.QTreeView(self) - tasks_view.setIndentation(0) - tasks_view.setSortingEnabled(True) - if dbcon is None: - dbcon = io - - tasks_model = TasksModel(dbcon) - tasks_proxy = TasksProxyModel() - tasks_proxy.setSourceModel(tasks_model) - tasks_view.setModel(tasks_proxy) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(tasks_view) - - selection_model = tasks_view.selectionModel() - selection_model.currentChanged.connect(self.task_changed) - - self._tasks_model = tasks_model - self._tasks_proxy = tasks_proxy - self._tasks_view = tasks_view - - self._last_selected_task = None - - def set_asset(self, asset_doc): - # Asset deselected - if asset_doc is None: - 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_name() - if current: - self._last_selected_task = current - - self._tasks_model.set_asset(asset_doc) - self._tasks_proxy.sort(0, QtCore.Qt.AscendingOrder) - - 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. - - """ - task_view_model = self._tasks_view.model() - if not task_view_model: - return - - # Clear selection - selection_model = self._tasks_view.selectionModel() - selection_model.clearSelection() - - # Select the task - mode = selection_model.Select | selection_model.Rows - for row in range(task_view_model.rowCount()): - index = task_view_model.index(row, 0) - name = index.data(TASK_NAME_ROLE) - if name == task_name: - selection_model.select(index, mode) - - # Set the currently active index - self._tasks_view.setCurrentIndex(index) - break - - def get_current_task_name(self): - """Return name of task at current index (selected) - - Returns: - str: Name of the current task. - - """ - index = self._tasks_view.currentIndex() - selection_model = self._tasks_view.selectionModel() - if index.isValid() and selection_model.isSelected(index): - return index.data(TASK_NAME_ROLE) - return None - - def get_current_task_type(self): - index = self._tasks_view.currentIndex() - selection_model = self._tasks_view.selectionModel() - if index.isValid() and selection_model.isSelected(index): - return index.data(TASK_TYPE_ROLE) - return None - - class FilesWidget(QtWidgets.QWidget): """A widget displaying files that allows to save and open files.""" file_selected = QtCore.Signal(str) @@ -1052,7 +941,7 @@ class Window(QtWidgets.QMainWindow): if asset_docs: asset_doc = asset_docs[0] - task_name = self.tasks_widget.get_current_task_name() + task_name = self.tasks_widget.get_selected_task_name() workfile_doc = None if asset_doc and task_name and filepath: @@ -1082,7 +971,7 @@ class Window(QtWidgets.QMainWindow): def _get_current_workfile_doc(self, filepath=None): if filepath is None: filepath = self.files_widget._get_selected_filepath() - task_name = self.tasks_widget.get_current_task_name() + task_name = self.tasks_widget.get_selected_task_name() asset_docs = self.assets_widget.get_selected_assets() if not task_name or not asset_docs or not filepath: return @@ -1113,18 +1002,16 @@ class Window(QtWidgets.QMainWindow): "name": asset, "type": "asset" }, - { - "data.tasks": 1 - } - ) + {"_id": 1} + ) or {} # Select the asset self.assets_widget.select_assets([asset], expand=True) - self.tasks_widget.set_asset(asset_document) + self.tasks_widget.set_asset_id(asset_document.get("_id")) if "task" in context: - self.tasks_widget.select_task(context["task"]) + self.tasks_widget.select_task_name(context["task"]) def refresh(self): # Refresh asset widget @@ -1134,7 +1021,7 @@ class Window(QtWidgets.QMainWindow): def _on_asset_changed(self): asset = self.assets_widget.get_selected_assets() or None - + asset_id = None if not asset: # Force disable the other widgets if no # active selection @@ -1142,16 +1029,17 @@ class Window(QtWidgets.QMainWindow): self.files_widget.setEnabled(False) else: asset = asset[0] + asset_id = asset.get("_id") self.tasks_widget.setEnabled(True) - self.tasks_widget.set_asset(asset) + self.tasks_widget.set_asset_id(asset_id) def _on_task_changed(self): asset = self.assets_widget.get_selected_assets() or None if asset is not None: asset = asset[0] - task_name = self.tasks_widget.get_current_task_name() - task_type = self.tasks_widget.get_current_task_type() + task_name = self.tasks_widget.get_selected_task_name() + task_type = self.tasks_widget.get_selected_task_type() self.tasks_widget.setEnabled(bool(asset)) From 35ef51209a4550782b4a8b5342ed0273c0bc305f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:50:53 +0100 Subject: [PATCH 26/60] typo fix --- openpype/tools/context_dialog/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py index 0fd9679e83..3e7c8c7065 100644 --- a/openpype/tools/context_dialog/window.py +++ b/openpype/tools/context_dialog/window.py @@ -79,7 +79,7 @@ class ContextDialog(QtWidgets.QDialog): # Add widgets to main splitter main_splitter.addWidget(left_side_widget) - main_splitter.addWidget(tasks_widgets) + main_splitter.addWidget(tasks_widget) # Set stretch of both sides main_splitter.setStretchFactor(0, 7) From 7cdae95c73412d343bc1bd50d454b8eaea10dfb9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:52:19 +0100 Subject: [PATCH 27/60] copied sceneinventory to openpype --- openpype/tools/sceneinventory/__init__.py | 7 + openpype/tools/sceneinventory/app.py | 1953 +++++++++++++++++++++ openpype/tools/sceneinventory/lib.py | 8 + openpype/tools/sceneinventory/model.py | 424 +++++ openpype/tools/sceneinventory/proxy.py | 148 ++ 5 files changed, 2540 insertions(+) create mode 100644 openpype/tools/sceneinventory/__init__.py create mode 100644 openpype/tools/sceneinventory/app.py create mode 100644 openpype/tools/sceneinventory/lib.py create mode 100644 openpype/tools/sceneinventory/model.py create mode 100644 openpype/tools/sceneinventory/proxy.py diff --git a/openpype/tools/sceneinventory/__init__.py b/openpype/tools/sceneinventory/__init__.py new file mode 100644 index 0000000000..694caf15fe --- /dev/null +++ b/openpype/tools/sceneinventory/__init__.py @@ -0,0 +1,7 @@ +from .app import ( + show, +) + +__all__ = [ + "show", +] diff --git a/openpype/tools/sceneinventory/app.py b/openpype/tools/sceneinventory/app.py new file mode 100644 index 0000000000..5304b7ac12 --- /dev/null +++ b/openpype/tools/sceneinventory/app.py @@ -0,0 +1,1953 @@ +import os +import sys +import logging +import collections +from functools import partial + +from ...vendor.Qt import QtWidgets, QtCore +from ...vendor import qtawesome +from ... import io, api, style +from ...lib import HeroVersionType + +from .. import lib as tools_lib +from ..delegates import VersionDelegate + +from .proxy import FilterProxyModel +from .model import InventoryModel + +from openpype.modules import ModulesManager + +DEFAULT_COLOR = "#fb9c15" + +module = sys.modules[__name__] +module.window = None + +log = logging.getLogger("SceneInventory") + + +class View(QtWidgets.QTreeView): + data_changed = QtCore.Signal() + hierarchy_view = QtCore.Signal(bool) + + def __init__(self, parent=None): + super(View, self).__init__(parent=parent) + + if not parent: + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint + ) + # view settings + self.setIndentation(12) + self.setAlternatingRowColors(True) + self.setSortingEnabled(True) + self.setSelectionMode(self.ExtendedSelection) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_right_mouse_menu) + self._hierarchy_view = False + self._selected = None + + manager = ModulesManager() + self.sync_server = manager.modules_by_name["sync_server"] + self.sync_enabled = self.sync_server.enabled + + def enter_hierarchy(self, items): + self._selected = set(i["objectName"] for i in items) + self._hierarchy_view = True + self.hierarchy_view.emit(True) + self.data_changed.emit() + self.expandToDepth(1) + self.setStyleSheet(""" + QTreeView { + border-color: #fb9c15; + } + """) + + def leave_hierarchy(self): + self._hierarchy_view = False + self.hierarchy_view.emit(False) + self.data_changed.emit() + self.setStyleSheet("QTreeView {}") + + def build_item_menu_for_selection(self, items, menu): + if not items: + return + + repre_ids = [] + for item in items: + item_id = io.ObjectId(item["representation"]) + if item_id not in repre_ids: + repre_ids.append(item_id) + + repre_docs = io.find( + { + "type": "representation", + "_id": {"$in": repre_ids} + }, + {"parent": 1} + ) + + version_ids = [] + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + if version_id not in version_ids: + version_ids.append(version_id) + + loaded_versions = io.find({ + "_id": {"$in": version_ids}, + "type": {"$in": ["version", "hero_version"]} + }) + + loaded_hero_versions = [] + versions_by_parent_id = collections.defaultdict(list) + version_parents = [] + for version in loaded_versions: + if version["type"] == "hero_version": + loaded_hero_versions.append(version) + else: + parent_id = version["parent"] + versions_by_parent_id[parent_id].append(version) + if parent_id not in version_parents: + version_parents.append(parent_id) + + all_versions = io.find({ + "type": {"$in": ["hero_version", "version"]}, + "parent": {"$in": version_parents} + }) + hero_versions = [] + versions = [] + for version in all_versions: + if version["type"] == "hero_version": + hero_versions.append(version) + else: + versions.append(version) + + has_loaded_hero_versions = len(loaded_hero_versions) > 0 + has_available_hero_version = len(hero_versions) > 0 + has_outdated = False + + for version in versions: + parent_id = version["parent"] + current_versions = versions_by_parent_id[parent_id] + for current_version in current_versions: + if current_version["name"] < version["name"]: + has_outdated = True + break + + if has_outdated: + break + + switch_to_versioned = None + if has_loaded_hero_versions: + def _on_switch_to_versioned(items): + repre_ids = [] + for item in items: + item_id = io.ObjectId(item["representation"]) + if item_id not in repre_ids: + repre_ids.append(item_id) + + repre_docs = io.find( + { + "type": "representation", + "_id": {"$in": repre_ids} + }, + {"parent": 1} + ) + + version_ids = [] + version_id_by_repre_id = {} + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_id_by_repre_id[repre_doc["_id"]] = version_id + if version_id not in version_ids: + version_ids.append(version_id) + hero_versions = io.find( + { + "_id": {"$in": version_ids}, + "type": "hero_version" + }, + {"version_id": 1} + ) + version_ids = set() + for hero_version in hero_versions: + version_id = hero_version["version_id"] + version_ids.add(version_id) + hero_version_id = hero_version["_id"] + for _repre_id, current_version_id in ( + version_id_by_repre_id.items() + ): + if current_version_id == hero_version_id: + version_id_by_repre_id[_repre_id] = version_id + + version_docs = io.find( + { + "_id": {"$in": list(version_ids)}, + "type": "version" + }, + {"name": 1} + ) + version_name_by_id = {} + for version_doc in version_docs: + version_name_by_id[version_doc["_id"]] = \ + version_doc["name"] + + for item in items: + repre_id = io.ObjectId(item["representation"]) + version_id = version_id_by_repre_id.get(repre_id) + version_name = version_name_by_id.get(version_id) + if version_name is not None: + try: + api.update(item, version_name) + except AssertionError: + self._show_version_error_dialog(version_name, + [item]) + log.warning("Update failed", exc_info=True) + + self.data_changed.emit() + + update_icon = qtawesome.icon( + "fa.asterisk", + color=DEFAULT_COLOR + ) + switch_to_versioned = QtWidgets.QAction( + update_icon, + "Switch to versioned", + menu + ) + switch_to_versioned.triggered.connect( + lambda: _on_switch_to_versioned(items) + ) + + update_to_latest_action = None + if has_outdated or has_loaded_hero_versions: + # update to latest version + def _on_update_to_latest(items): + for item in items: + try: + api.update(item, -1) + except AssertionError: + self._show_version_error_dialog(None, [item]) + log.warning("Update failed", exc_info=True) + self.data_changed.emit() + + update_icon = qtawesome.icon( + "fa.angle-double-up", + color=DEFAULT_COLOR + ) + update_to_latest_action = QtWidgets.QAction( + update_icon, + "Update to latest", + menu + ) + update_to_latest_action.triggered.connect( + lambda: _on_update_to_latest(items) + ) + + change_to_hero = None + if has_available_hero_version: + # change to hero version + def _on_update_to_hero(items): + for item in items: + try: + api.update(item, HeroVersionType(-1)) + except AssertionError: + self._show_version_error_dialog('hero', [item]) + log.warning("Update failed", exc_info=True) + self.data_changed.emit() + + # TODO change icon + change_icon = qtawesome.icon( + "fa.asterisk", + color="#00b359" + ) + change_to_hero = QtWidgets.QAction( + change_icon, + "Change to hero", + menu + ) + change_to_hero.triggered.connect( + lambda: _on_update_to_hero(items) + ) + + # set version + set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) + set_version_action = QtWidgets.QAction( + set_version_icon, + "Set version", + menu + ) + set_version_action.triggered.connect( + lambda: self.show_version_dialog(items)) + + # switch asset + switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) + switch_asset_action = QtWidgets.QAction( + switch_asset_icon, + "Switch Asset", + menu + ) + switch_asset_action.triggered.connect( + lambda: self.show_switch_dialog(items)) + + # remove + remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) + remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) + remove_action.triggered.connect( + lambda: self.show_remove_warning_dialog(items)) + + # add the actions + if switch_to_versioned: + menu.addAction(switch_to_versioned) + + if update_to_latest_action: + menu.addAction(update_to_latest_action) + + if change_to_hero: + menu.addAction(change_to_hero) + + menu.addAction(set_version_action) + menu.addAction(switch_asset_action) + + menu.addSeparator() + + menu.addAction(remove_action) + + menu.addSeparator() + + if self.sync_enabled: + menu = self.handle_sync_server(menu, repre_ids) + + def handle_sync_server(self, menu, repre_ids): + """ + Adds actions for download/upload when SyncServer is enabled + + Args: + menu (OptionMenu) + repre_ids (list) of object_ids + Returns: + (OptionMenu) + """ + download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR) + download_active_action = QtWidgets.QAction( + download_icon, + "Download", + menu + ) + download_active_action.triggered.connect( + lambda: self._add_sites(repre_ids, 'active_site')) + + upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR) + upload_remote_action = QtWidgets.QAction( + upload_icon, + "Upload", + menu + ) + upload_remote_action.triggered.connect( + lambda: self._add_sites(repre_ids, 'remote_site')) + + menu.addAction(download_active_action) + menu.addAction(upload_remote_action) + + return menu + + def _add_sites(self, repre_ids, side): + """ + (Re)sync all 'repre_ids' to specific site. + + It checks if opposite site has fully available content to limit + accidents. (ReSync active when no remote >> losing active content) + + Args: + repre_ids (list) + side (str): 'active_site'|'remote_site' + """ + project = io.Session["AVALON_PROJECT"] + active_site = self.sync_server.get_active_site(project) + remote_site = self.sync_server.get_remote_site(project) + + for repre_id in repre_ids: + representation = io.find_one({"type": "representation", + "_id": repre_id}) + if not representation: + continue + + progress = tools_lib.get_progress_for_repre(representation, + active_site, + remote_site) + if side == 'active_site': + # check opposite from added site, must be 1 or unable to sync + check_progress = progress[remote_site] + site = active_site + else: + check_progress = progress[active_site] + site = remote_site + + if check_progress == 1: + self.sync_server.add_site(project, repre_id, site, force=True) + + self.data_changed.emit() + + def build_item_menu(self, items): + """Create menu for the selected items""" + + menu = QtWidgets.QMenu(self) + + # add the actions + self.build_item_menu_for_selection(items, menu) + + # These two actions should be able to work without selection + # expand all items + expandall_action = QtWidgets.QAction(menu, text="Expand all items") + expandall_action.triggered.connect(self.expandAll) + + # collapse all items + collapse_action = QtWidgets.QAction(menu, text="Collapse all items") + collapse_action.triggered.connect(self.collapseAll) + + menu.addAction(expandall_action) + menu.addAction(collapse_action) + + custom_actions = self.get_custom_actions(containers=items) + if custom_actions: + submenu = QtWidgets.QMenu("Actions", self) + for action in custom_actions: + + color = action.color or DEFAULT_COLOR + icon = qtawesome.icon("fa.%s" % action.icon, color=color) + action_item = QtWidgets.QAction(icon, action.label, submenu) + action_item.triggered.connect( + partial(self.process_custom_action, action, items)) + + submenu.addAction(action_item) + + menu.addMenu(submenu) + + # go back to flat view + if self._hierarchy_view: + back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR) + back_to_flat_action = QtWidgets.QAction( + back_to_flat_icon, + "Back to Full-View", + menu + ) + back_to_flat_action.triggered.connect(self.leave_hierarchy) + + # send items to hierarchy view + enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8") + enter_hierarchy_action = QtWidgets.QAction( + enter_hierarchy_icon, + "Cherry-Pick (Hierarchy)", + menu + ) + enter_hierarchy_action.triggered.connect( + lambda: self.enter_hierarchy(items)) + + if items: + menu.addAction(enter_hierarchy_action) + + if self._hierarchy_view: + menu.addAction(back_to_flat_action) + + return menu + + def get_custom_actions(self, containers): + """Get the registered Inventory Actions + + Args: + containers(list): collection of containers + + Returns: + list: collection of filter and initialized actions + """ + + def sorter(Plugin): + """Sort based on order attribute of the plugin""" + return Plugin.order + + # Fedd an empty dict if no selection, this will ensure the compat + # lookup always work, so plugin can interact with Scene Inventory + # reversely. + containers = containers or [dict()] + + # Check which action will be available in the menu + Plugins = api.discover(api.InventoryAction) + compatible = [p() for p in Plugins if + any(p.is_compatible(c) for c in containers)] + + return sorted(compatible, key=sorter) + + def process_custom_action(self, action, containers): + """Run action and if results are returned positive update the view + + If the result is list or dict, will select view items by the result. + + Args: + action (InventoryAction): Inventory Action instance + containers (list): Data of currently selected items + + Returns: + None + """ + + result = action.process(containers) + if result: + self.data_changed.emit() + + if isinstance(result, (list, set)): + self.select_items_by_action(result) + + if isinstance(result, dict): + self.select_items_by_action(result["objectNames"], + result["options"]) + + def select_items_by_action(self, object_names, options=None): + """Select view items by the result of action + + Args: + object_names (list or set): A list/set of container object name + options (dict): GUI operation options. + + Returns: + None + + """ + options = options or dict() + + if options.get("clear", True): + self.clearSelection() + + object_names = set(object_names) + if (self._hierarchy_view and + not self._selected.issuperset(object_names)): + # If any container not in current cherry-picked view, update + # view before selecting them. + self._selected.update(object_names) + self.data_changed.emit() + + model = self.model() + selection_model = self.selectionModel() + + select_mode = { + "select": selection_model.Select, + "deselect": selection_model.Deselect, + "toggle": selection_model.Toggle, + }[options.get("mode", "select")] + + for item in tools_lib.iter_model_rows(model, 0): + item = item.data(InventoryModel.ItemRole) + if item.get("isGroupNode"): + continue + + name = item.get("objectName") + if name in object_names: + self.scrollTo(item) # Ensure item is visible + flags = select_mode | selection_model.Rows + selection_model.select(item, flags) + + object_names.remove(name) + + if len(object_names) == 0: + break + + def show_right_mouse_menu(self, pos): + """Display the menu when at the position of the item clicked""" + + globalpos = self.viewport().mapToGlobal(pos) + + if not self.selectionModel().hasSelection(): + print("No selection") + # Build menu without selection, feed an empty list + menu = self.build_item_menu([]) + menu.exec_(globalpos) + return + + active = self.currentIndex() # index under mouse + active = active.sibling(active.row(), 0) # get first column + + # move index under mouse + indices = self.get_indices() + if active in indices: + indices.remove(active) + + indices.append(active) + + # Extend to the sub-items + all_indices = self.extend_to_children(indices) + items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices + if i.parent().isValid()] + + if self._hierarchy_view: + # Ensure no group item + items = [n for n in items if not n.get("isGroupNode")] + + menu = self.build_item_menu(items) + menu.exec_(globalpos) + + def get_indices(self): + """Get the selected rows""" + selection_model = self.selectionModel() + return selection_model.selectedRows() + + def extend_to_children(self, indices): + """Extend the indices to the children indices. + + Top-level indices are extended to its children indices. Sub-items + are kept as is. + + Args: + indices (list): The indices to extend. + + Returns: + list: The children indices + + """ + def get_children(i): + model = i.model() + rows = model.rowCount(parent=i) + for row in range(rows): + child = model.index(row, 0, parent=i) + yield child + + subitems = set() + for i in indices: + valid_parent = i.parent().isValid() + if valid_parent and i not in subitems: + subitems.add(i) + + if self._hierarchy_view: + # Assume this is a group item + for child in get_children(i): + subitems.add(child) + else: + # is top level item + for child in get_children(i): + subitems.add(child) + + return list(subitems) + + def show_version_dialog(self, items): + """Create a dialog with the available versions for the selected file + + Args: + items (list): list of items to run the "set_version" for + + Returns: + None + """ + + active = items[-1] + + # Get available versions for active representation + representation_id = io.ObjectId(active["representation"]) + representation = io.find_one({"_id": representation_id}) + version = io.find_one({ + "_id": representation["parent"] + }) + + versions = list(io.find( + { + "parent": version["parent"], + "type": "version" + }, + sort=[("name", 1)] + )) + + hero_version = io.find_one({ + "parent": version["parent"], + "type": "hero_version" + }) + if hero_version: + _version_id = hero_version["version_id"] + for _version in versions: + if _version["_id"] != _version_id: + continue + + hero_version["name"] = HeroVersionType( + _version["name"] + ) + hero_version["data"] = _version["data"] + break + + # Get index among the listed versions + current_item = None + current_version = active["version"] + if isinstance(current_version, HeroVersionType): + current_item = hero_version + else: + for version in versions: + if version["name"] == current_version: + current_item = version + break + + all_versions = [] + if hero_version: + all_versions.append(hero_version) + all_versions.extend(reversed(versions)) + + if current_item: + index = all_versions.index(current_item) + else: + index = 0 + + versions_by_label = dict() + labels = [] + for version in all_versions: + is_hero = version["type"] == "hero_version" + label = tools_lib.format_version(version["name"], is_hero) + labels.append(label) + versions_by_label[label] = version["name"] + + label, state = QtWidgets.QInputDialog.getItem( + self, + "Set version..", + "Set version number to", + labels, + current=index, + editable=False + ) + if not state: + return + + if label: + version = versions_by_label[label] + for item in items: + try: + api.update(item, version) + except AssertionError: + self._show_version_error_dialog(version, [item]) + log.warning("Update failed", exc_info=True) + # refresh model when done + self.data_changed.emit() + + def show_switch_dialog(self, items): + """Display Switch dialog""" + dialog = SwitchAssetDialog(self, items) + dialog.switched.connect(self.data_changed.emit) + dialog.show() + + def show_remove_warning_dialog(self, items): + """Prompt a dialog to inform the user the action will remove items""" + + accept = QtWidgets.QMessageBox.Ok + buttons = accept | QtWidgets.QMessageBox.Cancel + + message = ("Are you sure you want to remove " + "{} item(s)".format(len(items))) + state = QtWidgets.QMessageBox.question(self, "Are you sure?", + message, + buttons=buttons, + defaultButton=accept) + + if state != accept: + return + + for item in items: + api.remove(item) + self.data_changed.emit() + + def _show_version_error_dialog(self, version, items): + """Shows QMessageBox when version switch doesn't work + + Args: + version: str or int or None + """ + if not version: + version_str = "latest" + elif version == "hero": + version_str = "hero" + elif isinstance(version, int): + version_str = "v{:03d}".format(version) + else: + version_str = version + + dialog = QtWidgets.QMessageBox() + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle("Update failed") + + switch_btn = dialog.addButton("Switch Asset", + QtWidgets.QMessageBox.ActionRole) + switch_btn.clicked.connect(lambda: self.show_switch_dialog(items)) + + dialog.addButton(QtWidgets.QMessageBox.Cancel) + + msg = "Version update to '{}' ".format(version_str) + \ + "failed as representation doesn't exist.\n\n" \ + "Please update to version with a valid " \ + "representation OR \n use 'Switch Asset' button " \ + "to change asset." + dialog.setText(msg) + dialog.exec_() + + +class SearchComboBox(QtWidgets.QComboBox): + """Searchable ComboBox with empty placeholder value as first value""" + + def __init__(self, parent=None, placeholder=""): + super(SearchComboBox, self).__init__(parent) + + self.setEditable(True) + self.setInsertPolicy(self.NoInsert) + self.lineEdit().setPlaceholderText(placeholder) + + # Apply completer settings + completer = self.completer() + completer.setCompletionMode(completer.PopupCompletion) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + + # Force style sheet on popup menu + # It won't take the parent stylesheet for some reason + # todo: better fix for completer popup stylesheet + if module.window: + popup = completer.popup() + popup.setStyleSheet(module.window.styleSheet()) + + def populate(self, items): + self.clear() + self.addItems([""]) # ensure first item is placeholder + self.addItems(items) + + def get_valid_value(self): + """Return the current text if it's a valid value else None + + Note: The empty placeholder value is valid and returns as "" + + """ + + text = self.currentText() + lookup = set(self.itemText(i) for i in range(self.count())) + if text not in lookup: + return None + + return text or None + + def set_valid_value(self, value): + """Try to locate 'value' and pre-select it in dropdown.""" + index = self.findText(value) + if index > -1: + self.setCurrentIndex(index) + + +class ValidationState: + def __init__(self): + self.asset_ok = True + self.subset_ok = True + self.repre_ok = True + + @property + def all_ok(self): + return ( + self.asset_ok + and self.subset_ok + and self.repre_ok + ) + + +class SwitchAssetDialog(QtWidgets.QDialog): + """Widget to support asset switching""" + + MIN_WIDTH = 550 + + fill_check = False + switched = QtCore.Signal() + + def __init__(self, parent=None, items=None): + QtWidgets.QDialog.__init__(self, parent) + + self.setWindowTitle("Switch selected items ...") + + # Force and keep focus dialog + self.setModal(True) + + self._assets_box = SearchComboBox(placeholder="") + self._subsets_box = SearchComboBox(placeholder="") + self._representations_box = SearchComboBox( + placeholder="" + ) + + self._asset_label = QtWidgets.QLabel("") + self._subset_label = QtWidgets.QLabel("") + self._repre_label = QtWidgets.QLabel("") + + self.current_asset_btn = QtWidgets.QPushButton("Use current asset") + + main_layout = QtWidgets.QGridLayout(self) + + accept_icon = qtawesome.icon("fa.check", color="white") + accept_btn = QtWidgets.QPushButton() + accept_btn.setIcon(accept_icon) + accept_btn.setFixedWidth(24) + accept_btn.setFixedHeight(24) + + # Asset column + main_layout.addWidget(self.current_asset_btn, 0, 0) + main_layout.addWidget(self._assets_box, 1, 0) + main_layout.addWidget(self._asset_label, 2, 0) + # Subset column + main_layout.addWidget(self._subsets_box, 1, 1) + main_layout.addWidget(self._subset_label, 2, 1) + # Representation column + main_layout.addWidget(self._representations_box, 1, 2) + main_layout.addWidget(self._repre_label, 2, 2) + # Btn column + main_layout.addWidget(accept_btn, 1, 3) + + self._accept_btn = accept_btn + + self._assets_box.currentIndexChanged.connect( + self._combobox_value_changed + ) + self._subsets_box.currentIndexChanged.connect( + self._combobox_value_changed + ) + self._representations_box.currentIndexChanged.connect( + self._combobox_value_changed + ) + self._accept_btn.clicked.connect(self._on_accept) + self.current_asset_btn.clicked.connect(self._on_current_asset) + + self._init_asset_name = None + self._init_subset_name = None + self._init_repre_name = None + + self._items = items + self._prepare_content_data() + self.refresh(True) + + self.setMinimumWidth(self.MIN_WIDTH) + + # Set default focus to accept button so you don't directly type in + # first asset field, this also allows to see the placeholder value. + accept_btn.setFocus() + + def _prepare_content_data(self): + repre_ids = [ + io.ObjectId(item["representation"]) + for item in self._items + ] + repres = list(io.find({ + "type": {"$in": ["representation", "archived_representation"]}, + "_id": {"$in": repre_ids} + })) + repres_by_id = {repre["_id"]: repre for repre in repres} + + # stash context values, works only for single representation + if len(repres) == 1: + self._init_asset_name = repres[0]["context"]["asset"] + self._init_subset_name = repres[0]["context"]["subset"] + self._init_repre_name = repres[0]["context"]["representation"] + + content_repres = {} + archived_repres = [] + missing_repres = [] + version_ids = [] + for repre_id in repre_ids: + if repre_id not in repres_by_id: + missing_repres.append(repre_id) + elif repres_by_id[repre_id]["type"] == "archived_representation": + repre = repres_by_id[repre_id] + archived_repres.append(repre) + version_ids.append(repre["parent"]) + else: + repre = repres_by_id[repre_id] + content_repres[repre_id] = repres_by_id[repre_id] + version_ids.append(repre["parent"]) + + versions = io.find({ + "type": {"$in": ["version", "hero_version"]}, + "_id": {"$in": list(set(version_ids))} + }) + content_versions = {} + hero_version_ids = set() + for version in versions: + content_versions[version["_id"]] = version + if version["type"] == "hero_version": + hero_version_ids.add(version["_id"]) + + missing_versions = [] + subset_ids = [] + for version_id in version_ids: + if version_id not in content_versions: + missing_versions.append(version_id) + else: + subset_ids.append(content_versions[version_id]["parent"]) + + subsets = io.find({ + "type": {"$in": ["subset", "archived_subset"]}, + "_id": {"$in": subset_ids} + }) + subsets_by_id = {sub["_id"]: sub for sub in subsets} + + asset_ids = [] + archived_subsets = [] + missing_subsets = [] + content_subsets = {} + for subset_id in subset_ids: + if subset_id not in subsets_by_id: + missing_subsets.append(subset_id) + elif subsets_by_id[subset_id]["type"] == "archived_subset": + subset = subsets_by_id[subset_id] + asset_ids.append(subset["parent"]) + archived_subsets.append(subset) + else: + subset = subsets_by_id[subset_id] + asset_ids.append(subset["parent"]) + content_subsets[subset_id] = subset + + assets = io.find({ + "type": {"$in": ["asset", "archived_asset"]}, + "_id": {"$in": list(asset_ids)} + }) + assets_by_id = {asset["_id"]: asset for asset in assets} + + missing_assets = [] + archived_assets = [] + content_assets = {} + for asset_id in asset_ids: + if asset_id not in assets_by_id: + missing_assets.append(asset_id) + elif assets_by_id[asset_id]["type"] == "archived_asset": + archived_assets.append(assets_by_id[asset_id]) + else: + content_assets[asset_id] = assets_by_id[asset_id] + + self.content_assets = content_assets + self.content_subsets = content_subsets + self.content_versions = content_versions + self.content_repres = content_repres + + self.hero_version_ids = hero_version_ids + + self.missing_assets = missing_assets + self.missing_versions = missing_versions + self.missing_subsets = missing_subsets + self.missing_repres = missing_repres + self.missing_docs = ( + bool(missing_assets) + or bool(missing_versions) + or bool(missing_subsets) + or bool(missing_repres) + ) + + self.archived_assets = archived_assets + self.archived_subsets = archived_subsets + self.archived_repres = archived_repres + + def _combobox_value_changed(self, *args, **kwargs): + self.refresh() + + def refresh(self, init_refresh=False): + """Build the need comboboxes with content""" + if not self.fill_check and not init_refresh: + return + + self.fill_check = False + + if init_refresh: + asset_values = self._get_asset_box_values() + self._fill_combobox(asset_values, "asset") + + validation_state = ValidationState() + + # Set other comboboxes to empty if any document is missing or any asset + # of loaded representations is archived. + self._is_asset_ok(validation_state) + if validation_state.asset_ok: + subset_values = self._get_subset_box_values() + self._fill_combobox(subset_values, "subset") + self._is_subset_ok(validation_state) + + if validation_state.asset_ok and validation_state.subset_ok: + repre_values = sorted(self._representations_box_values()) + self._fill_combobox(repre_values, "repre") + self._is_repre_ok(validation_state) + + # Fill comboboxes with values + self.set_labels() + self.apply_validations(validation_state) + + if init_refresh: # pre select context if possible + self._assets_box.set_valid_value(self._init_asset_name) + self._subsets_box.set_valid_value(self._init_subset_name) + self._representations_box.set_valid_value(self._init_repre_name) + + self.fill_check = True + + def _get_loaders(self, representations): + if not representations: + return list() + + available_loaders = filter( + lambda l: not (hasattr(l, "is_utility") and l.is_utility), + api.discover(api.Loader) + ) + + loaders = set() + + for representation in representations: + for loader in api.loaders_from_representation( + available_loaders, + representation + ): + loaders.add(loader) + + return loaders + + def _fill_combobox(self, values, combobox_type): + if combobox_type == "asset": + combobox_widget = self._assets_box + elif combobox_type == "subset": + combobox_widget = self._subsets_box + elif combobox_type == "repre": + combobox_widget = self._representations_box + else: + return + selected_value = combobox_widget.get_valid_value() + + # Fill combobox + if values is not None: + combobox_widget.populate(list(sorted(values))) + if selected_value and selected_value in values: + index = None + for idx in range(combobox_widget.count()): + if selected_value == str(combobox_widget.itemText(idx)): + index = idx + break + if index is not None: + combobox_widget.setCurrentIndex(index) + + def set_labels(self): + asset_label = self._assets_box.get_valid_value() + subset_label = self._subsets_box.get_valid_value() + repre_label = self._representations_box.get_valid_value() + + default = "*No changes" + self._asset_label.setText(asset_label or default) + self._subset_label.setText(subset_label or default) + self._repre_label.setText(repre_label or default) + + def apply_validations(self, validation_state): + error_msg = "*Please select" + error_sheet = "border: 1px solid red;" + success_sheet = "border: 1px solid green;" + + asset_sheet = None + subset_sheet = None + repre_sheet = None + accept_sheet = None + if validation_state.asset_ok is False: + asset_sheet = error_sheet + self._asset_label.setText(error_msg) + elif validation_state.subset_ok is False: + subset_sheet = error_sheet + self._subset_label.setText(error_msg) + elif validation_state.repre_ok is False: + repre_sheet = error_sheet + self._repre_label.setText(error_msg) + + if validation_state.all_ok: + accept_sheet = success_sheet + + self._assets_box.setStyleSheet(asset_sheet or "") + self._subsets_box.setStyleSheet(subset_sheet or "") + self._representations_box.setStyleSheet(repre_sheet or "") + + self._accept_btn.setEnabled(validation_state.all_ok) + self._accept_btn.setStyleSheet(accept_sheet or "") + + def _get_asset_box_values(self): + asset_docs = io.find( + {"type": "asset"}, + {"_id": 1, "name": 1} + ) + asset_names_by_id = { + asset_doc["_id"]: asset_doc["name"] + for asset_doc in asset_docs + } + subsets = io.find( + { + "type": "subset", + "parent": {"$in": list(asset_names_by_id.keys())} + }, + { + "parent": 1 + } + ) + + filtered_assets = [] + for subset in subsets: + asset_name = asset_names_by_id[subset["parent"]] + if asset_name not in filtered_assets: + filtered_assets.append(asset_name) + return sorted(filtered_assets) + + def _get_subset_box_values(self): + selected_asset = self._assets_box.get_valid_value() + if selected_asset: + asset_doc = io.find_one({"type": "asset", "name": selected_asset}) + asset_ids = [asset_doc["_id"]] + else: + asset_ids = list(self.content_assets.keys()) + + subsets = io.find( + { + "type": "subset", + "parent": {"$in": asset_ids} + }, + { + "parent": 1, + "name": 1 + } + ) + + subset_names_by_parent_id = collections.defaultdict(set) + for subset in subsets: + subset_names_by_parent_id[subset["parent"]].add(subset["name"]) + + possible_subsets = None + for subset_names in subset_names_by_parent_id.values(): + if possible_subsets is None: + possible_subsets = subset_names + else: + possible_subsets = (possible_subsets & subset_names) + + if not possible_subsets: + break + + return list(possible_subsets or list()) + + def _representations_box_values(self): + # NOTE hero versions are not used because it is expected that + # hero version has same representations as latests + selected_asset = self._assets_box.currentText() + selected_subset = self._subsets_box.currentText() + + # If nothing is selected + # [ ] [ ] [?] + if not selected_asset and not selected_subset: + # Find all representations of selection's subsets + possible_repres = list(io.find( + { + "type": "representation", + "parent": {"$in": list(self.content_versions.keys())} + }, + { + "parent": 1, + "name": 1 + } + )) + + possible_repres_by_parent = collections.defaultdict(set) + for repre in possible_repres: + possible_repres_by_parent[repre["parent"]].add(repre["name"]) + + output_repres = None + for repre_names in possible_repres_by_parent.values(): + if output_repres is None: + output_repres = repre_names + else: + output_repres = (output_repres & repre_names) + + if not output_repres: + break + + return list(output_repres or list()) + + # [x] [x] [?] + if selected_asset and selected_subset: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_doc = io.find_one( + { + "type": "subset", + "name": selected_subset, + "parent": asset_doc["_id"] + }, + {"_id": 1} + ) + subset_id = subset_doc["_id"] + last_versions_by_subset_id = self.find_last_versions([subset_id]) + version_doc = last_versions_by_subset_id.get(subset_id) + repre_docs = io.find( + { + "type": "representation", + "parent": version_doc["_id"] + }, + { + "name": 1 + } + ) + return [ + repre_doc["name"] + for repre_doc in repre_docs + ] + + # [x] [ ] [?] + # If asset only is selected + if selected_asset: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + if not asset_doc: + return list() + + # Filter subsets by subset names from content + subset_names = set() + for subset_doc in self.content_subsets.values(): + subset_names.add(subset_doc["name"]) + subset_docs = io.find( + { + "type": "subset", + "parent": asset_doc["_id"], + "name": {"$in": list(subset_names)} + }, + {"_id": 1} + ) + subset_ids = [ + subset_doc["_id"] + for subset_doc in subset_docs + ] + if not subset_ids: + return list() + + last_versions_by_subset_id = self.find_last_versions(subset_ids) + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + if not subset_id_by_version_id: + return list() + + repre_docs = list(io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + )) + if not repre_docs: + return list() + + repre_names_by_parent = collections.defaultdict(set) + for repre_doc in repre_docs: + repre_names_by_parent[repre_doc["parent"]].add( + repre_doc["name"] + ) + + available_repres = None + for repre_names in repre_names_by_parent.values(): + if available_repres is None: + available_repres = repre_names + continue + + available_repres = available_repres.intersection(repre_names) + + return list(available_repres) + + # [ ] [x] [?] + subset_docs = list(io.find( + { + "type": "subset", + "parent": {"$in": list(self.content_assets.keys())}, + "name": selected_subset + }, + {"_id": 1, "parent": 1} + )) + if not subset_docs: + return list() + + subset_docs_by_id = { + subset_doc["_id"]: subset_doc + for subset_doc in subset_docs + } + last_versions_by_subset_id = self.find_last_versions( + subset_docs_by_id.keys() + ) + + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + if not subset_id_by_version_id: + return list() + + repre_docs = list(io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + )) + if not repre_docs: + return list() + + repre_names_by_asset_id = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + asset_id = subset_docs_by_id[subset_id]["parent"] + if asset_id not in repre_names_by_asset_id: + repre_names_by_asset_id[asset_id] = set() + repre_names_by_asset_id[asset_id].add(repre_doc["name"]) + + available_repres = None + for repre_names in repre_names_by_asset_id.values(): + if available_repres is None: + available_repres = repre_names + continue + + available_repres = available_repres.intersection(repre_names) + + return list(available_repres) + + def _is_asset_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + if ( + selected_asset is None + and (self.missing_docs or self.archived_assets) + ): + validation_state.asset_ok = False + + def _is_subset_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + + # [?] [x] [?] + # If subset is selected then must be ok + if selected_subset is not None: + return + + # [ ] [ ] [?] + if selected_asset is None: + # If there were archived subsets and asset is not selected + if self.archived_subsets: + validation_state.subset_ok = False + return + + # [x] [ ] [?] + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_docs = io.find( + {"type": "subset", "parent": asset_doc["_id"]}, + {"name": 1} + ) + subset_names = set( + subset_doc["name"] + for subset_doc in subset_docs + ) + + for subset_doc in self.content_subsets.values(): + if subset_doc["name"] not in subset_names: + validation_state.subset_ok = False + break + + def find_last_versions(self, subset_ids): + _pipeline = [ + # Find all versions of those subsets + {"$match": { + "type": "version", + "parent": {"$in": list(subset_ids)} + }}, + # Sorting versions all together + {"$sort": {"name": 1}}, + # Group them by "parent", but only take the last + {"$group": { + "_id": "$parent", + "_version_id": {"$last": "$_id"}, + "type": {"$last": "$type"} + }} + ] + last_versions_by_subset_id = dict() + for doc in io.aggregate(_pipeline): + doc["parent"] = doc["_id"] + doc["_id"] = doc.pop("_version_id") + last_versions_by_subset_id[doc["parent"]] = doc + return last_versions_by_subset_id + + def _is_repre_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + selected_repre = self._representations_box.get_valid_value() + + # [?] [?] [x] + # If subset is selected then must be ok + if selected_repre is not None: + return + + # [ ] [ ] [ ] + if selected_asset is None and selected_subset is None: + if ( + self.archived_repres + or self.missing_versions + or self.missing_repres + ): + validation_state.repre_ok = False + return + + # [x] [x] [ ] + if selected_asset is not None and selected_subset is not None: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_doc = io.find_one( + { + "type": "subset", + "parent": asset_doc["_id"], + "name": selected_subset + }, + {"_id": 1} + ) + last_versions_by_subset_id = self.find_last_versions( + [subset_doc["_id"]] + ) + last_version = last_versions_by_subset_id.get(subset_doc["_id"]) + if not last_version: + validation_state.repre_ok = False + return + + repre_docs = io.find( + { + "type": "representation", + "parent": last_version["_id"] + }, + {"name": 1} + ) + + repre_names = set( + repre_doc["name"] + for repre_doc in repre_docs + ) + for repre_doc in self.content_repres.values(): + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + return + + # [x] [ ] [ ] + if selected_asset is not None: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_docs = list(io.find( + { + "type": "subset", + "parent": asset_doc["_id"] + }, + {"_id": 1, "name": 1} + )) + + subset_name_by_id = {} + subset_ids = set() + for subset_doc in subset_docs: + subset_id = subset_doc["_id"] + subset_ids.add(subset_id) + subset_name_by_id[subset_id] = subset_doc["name"] + + last_versions_by_subset_id = self.find_last_versions(subset_ids) + + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + repre_docs = io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + ) + repres_by_subset_name = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + subset_name = subset_name_by_id[subset_id] + if subset_name not in repres_by_subset_name: + repres_by_subset_name[subset_name] = set() + repres_by_subset_name[subset_name].add(repre_doc["name"]) + + for repre_doc in self.content_repres.values(): + version_doc = self.content_versions[repre_doc["parent"]] + subset_doc = self.content_subsets[version_doc["parent"]] + repre_names = ( + repres_by_subset_name.get(subset_doc["name"]) or [] + ) + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + return + + # [ ] [x] [ ] + # Subset documents + subset_docs = io.find( + { + "type": "subset", + "parent": {"$in": list(self.content_assets.keys())}, + "name": selected_subset + }, + {"_id": 1, "name": 1, "parent": 1} + ) + + subset_docs_by_id = {} + for subset_doc in subset_docs: + subset_docs_by_id[subset_doc["_id"]] = subset_doc + + last_versions_by_subset_id = self.find_last_versions( + subset_docs_by_id.keys() + ) + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + repre_docs = io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + ) + repres_by_asset_id = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + asset_id = subset_docs_by_id[subset_id]["parent"] + if asset_id not in repres_by_asset_id: + repres_by_asset_id[asset_id] = set() + repres_by_asset_id[asset_id].add(repre_doc["name"]) + + for repre_doc in self.content_repres.values(): + version_doc = self.content_versions[repre_doc["parent"]] + subset_doc = self.content_subsets[version_doc["parent"]] + asset_id = subset_doc["parent"] + repre_names = ( + repres_by_asset_id.get(asset_id) or [] + ) + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + + def _on_current_asset(self): + # Set initial asset as current. + asset_name = api.Session["AVALON_ASSET"] + index = self._assets_box.findText( + asset_name, QtCore.Qt.MatchFixedString + ) + if index >= 0: + print("Setting asset to {}".format(asset_name)) + self._assets_box.setCurrentIndex(index) + + def _on_accept(self): + # Use None when not a valid value or when placeholder value + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + selected_representation = self._representations_box.get_valid_value() + + if selected_asset: + asset_doc = io.find_one({"type": "asset", "name": selected_asset}) + asset_docs_by_id = {asset_doc["_id"]: asset_doc} + else: + asset_docs_by_id = self.content_assets + + asset_docs_by_name = { + asset_doc["name"]: asset_doc + for asset_doc in asset_docs_by_id.values() + } + + asset_ids = list(asset_docs_by_id.keys()) + + subset_query = { + "type": "subset", + "parent": {"$in": asset_ids} + } + if selected_subset: + subset_query["name"] = selected_subset + + subset_docs = list(io.find(subset_query)) + subset_ids = [] + subset_docs_by_parent_and_name = collections.defaultdict(dict) + for subset in subset_docs: + subset_ids.append(subset["_id"]) + parent_id = subset["parent"] + name = subset["name"] + subset_docs_by_parent_and_name[parent_id][name] = subset + + # versions + version_docs = list(io.find({ + "type": "version", + "parent": {"$in": subset_ids} + }, sort=[("name", -1)])) + + hero_version_docs = list(io.find({ + "type": "hero_version", + "parent": {"$in": subset_ids} + })) + + version_ids = list() + + version_docs_by_parent_id = {} + for version_doc in version_docs: + parent_id = version_doc["parent"] + if parent_id not in version_docs_by_parent_id: + version_ids.append(version_doc["_id"]) + version_docs_by_parent_id[parent_id] = version_doc + + hero_version_docs_by_parent_id = {} + for hero_version_doc in hero_version_docs: + version_ids.append(hero_version_doc["_id"]) + parent_id = hero_version_doc["parent"] + hero_version_docs_by_parent_id[parent_id] = hero_version_doc + + repre_docs = io.find({ + "type": "representation", + "parent": {"$in": version_ids} + }) + repre_docs_by_parent_id_by_name = collections.defaultdict(dict) + for repre_doc in repre_docs: + parent_id = repre_doc["parent"] + name = repre_doc["name"] + repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc + + for container in self._items: + container_repre_id = io.ObjectId(container["representation"]) + container_repre = self.content_repres[container_repre_id] + container_repre_name = container_repre["name"] + + container_version_id = container_repre["parent"] + container_version = self.content_versions[container_version_id] + + container_subset_id = container_version["parent"] + container_subset = self.content_subsets[container_subset_id] + container_subset_name = container_subset["name"] + + container_asset_id = container_subset["parent"] + container_asset = self.content_assets[container_asset_id] + container_asset_name = container_asset["name"] + + if selected_asset: + asset_doc = asset_docs_by_name[selected_asset] + else: + asset_doc = asset_docs_by_name[container_asset_name] + + subsets_by_name = subset_docs_by_parent_and_name[asset_doc["_id"]] + if selected_subset: + subset_doc = subsets_by_name[selected_subset] + else: + subset_doc = subsets_by_name[container_subset_name] + + repre_doc = None + subset_id = subset_doc["_id"] + if container_version["type"] == "hero_version": + hero_version = hero_version_docs_by_parent_id.get( + subset_id + ) + if hero_version: + _repres = repre_docs_by_parent_id_by_name.get( + hero_version["_id"] + ) + if selected_representation: + repre_doc = _repres.get(selected_representation) + else: + repre_doc = _repres.get(container_repre_name) + + if not repre_doc: + version_doc = version_docs_by_parent_id[subset_id] + version_id = version_doc["_id"] + repres_by_name = repre_docs_by_parent_id_by_name[version_id] + if selected_representation: + repre_doc = repres_by_name[selected_representation] + else: + repre_doc = repres_by_name[container_repre_name] + + try: + api.switch(container, repre_doc) + except Exception: + log.warning( + ( + "Couldn't switch asset." + "See traceback for more information." + ), + exc_info=True + ) + dialog = QtWidgets.QMessageBox() + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle("Switch asset failed") + msg = "Switch asset failed. "\ + "Search console log for more details" + dialog.setText(msg) + dialog.exec_() + + self.switched.emit() + + self.close() + + +class Window(QtWidgets.QDialog): + """Scene Inventory window""" + + def __init__(self, parent=None): + QtWidgets.QDialog.__init__(self, parent) + + self.resize(1100, 480) + self.setWindowTitle( + "Scene Inventory 1.0 - {}".format( + os.getenv("AVALON_PROJECT") or "" + ) + ) + self.setObjectName("SceneInventory") + self.setProperty("saveWindowPref", True) # Maya only property! + + layout = QtWidgets.QVBoxLayout(self) + + # region control + control_layout = QtWidgets.QHBoxLayout() + filter_label = QtWidgets.QLabel("Search") + text_filter = QtWidgets.QLineEdit() + + outdated_only = QtWidgets.QCheckBox("Filter to outdated") + outdated_only.setToolTip("Show outdated files only") + outdated_only.setChecked(False) + + icon = qtawesome.icon("fa.refresh", color="white") + refresh_button = QtWidgets.QPushButton() + refresh_button.setIcon(icon) + + control_layout.addWidget(filter_label) + control_layout.addWidget(text_filter) + control_layout.addWidget(outdated_only) + control_layout.addWidget(refresh_button) + + # endregion control + self.family_config_cache = tools_lib.global_family_cache() + + model = InventoryModel(self.family_config_cache) + proxy = FilterProxyModel() + view = View() + view.setModel(proxy) + + # apply delegates + version_delegate = VersionDelegate(io, self) + column = model.Columns.index("version") + view.setItemDelegateForColumn(column, version_delegate) + + layout.addLayout(control_layout) + layout.addWidget(view) + + self.filter = text_filter + self.outdated_only = outdated_only + self.view = view + self.refresh_button = refresh_button + self.model = model + self.proxy = proxy + + # signals + text_filter.textChanged.connect(self.proxy.setFilterRegExp) + outdated_only.stateChanged.connect(self.proxy.set_filter_outdated) + refresh_button.clicked.connect(self.refresh) + view.data_changed.connect(self.refresh) + view.hierarchy_view.connect(self.model.set_hierarchy_view) + view.hierarchy_view.connect(self.proxy.set_hierarchy_view) + + # proxy settings + proxy.setSourceModel(self.model) + proxy.setDynamicSortFilter(True) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + + self.data = { + "delegates": { + "version": version_delegate + } + } + + # set some nice default widths for the view + self.view.setColumnWidth(0, 250) # name + self.view.setColumnWidth(1, 55) # version + self.view.setColumnWidth(2, 55) # count + self.view.setColumnWidth(3, 150) # family + self.view.setColumnWidth(4, 100) # namespace + + self.family_config_cache.refresh() + + def keyPressEvent(self, event): + """Custom keyPressEvent. + + Override keyPressEvent to do nothing so that Maya's panels won't + take focus when pressing "SHIFT" whilst mouse is over viewport or + outliner. This way users don't accidently perform Maya commands + whilst trying to name an instance. + + """ + + def refresh(self, items=None): + with tools_lib.preserve_expanded_rows(tree_view=self.view, + role=self.model.UniqueRole): + with tools_lib.preserve_selection(tree_view=self.view, + role=self.model.UniqueRole, + current_index=False): + if self.view._hierarchy_view: + self.model.refresh(selected=self.view._selected, + items=items) + else: + self.model.refresh(items=items) + + +def show(root=None, debug=False, parent=None, items=None): + """Display Scene Inventory GUI + + Arguments: + debug (bool, optional): Run in debug-mode, + defaults to False + parent (QtCore.QObject, optional): When provided parent the interface + to this QObject. + items (list) of dictionaries - for injection of items for standalone + testing + + """ + + try: + module.window.close() + del module.window + except (RuntimeError, AttributeError): + pass + + if debug is True: + io.install() + + if not os.environ.get("AVALON_PROJECT"): + any_project = next( + project for project in io.projects() + if project.get("active", True) is not False + ) + + api.Session["AVALON_PROJECT"] = any_project["name"] + else: + api.Session["AVALON_PROJECT"] = os.environ.get("AVALON_PROJECT") + + with tools_lib.application(): + window = Window(parent) + window.setStyleSheet(style.load_stylesheet()) + window.show() + window.refresh(items=items) + + module.window = window + + # Pull window to the front. + module.window.raise_() + module.window.activateWindow() diff --git a/openpype/tools/sceneinventory/lib.py b/openpype/tools/sceneinventory/lib.py new file mode 100644 index 0000000000..0ac7622d65 --- /dev/null +++ b/openpype/tools/sceneinventory/lib.py @@ -0,0 +1,8 @@ +def walk_hierarchy(node): + """Recursively yield group node.""" + for child in node.children(): + if child.get("isGroupNode"): + yield child + + for _child in walk_hierarchy(child): + yield _child diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py new file mode 100644 index 0000000000..7b4e051b36 --- /dev/null +++ b/openpype/tools/sceneinventory/model.py @@ -0,0 +1,424 @@ +import logging + +from collections import defaultdict + +from ... import api, io, style, schema +from ...vendor.Qt import QtCore, QtGui +from ...vendor import qtawesome + +from .. import lib as tools_lib +from ...lib import HeroVersionType +from ..models import TreeModel, Item + +from . import lib + +from openpype.modules import ModulesManager + + +class InventoryModel(TreeModel): + """The model for the inventory""" + + Columns = ["Name", "version", "count", "family", "loader", "objectName"] + + OUTDATED_COLOR = QtGui.QColor(235, 30, 30) + CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30) + GRAYOUT_COLOR = QtGui.QColor(160, 160, 160) + + UniqueRole = QtCore.Qt.UserRole + 2 # unique label role + + def __init__(self, family_config_cache, parent=None): + super(InventoryModel, self).__init__(parent) + self.log = logging.getLogger(self.__class__.__name__) + + self.family_config_cache = family_config_cache + + self._hierarchy_view = False + + manager = ModulesManager() + sync_server = manager.modules_by_name["sync_server"] + self.sync_enabled = sync_server.enabled + self._icons = {} + self.active_site = self.remote_site = None + self.active_provider = self.remote_provider = None + + if self.sync_enabled: + project = io.Session['AVALON_PROJECT'] + active_site = sync_server.get_active_site(project) + remote_site = sync_server.get_remote_site(project) + + # TODO refactor + active_provider = \ + sync_server.get_provider_for_site(project, + active_site) + if active_site == 'studio': + active_provider = 'studio' # sanitized for icon + + remote_provider = \ + sync_server.get_provider_for_site(project, + remote_site) + if remote_site == 'studio': + remote_provider = 'studio' + + # self.sync_server = sync_server + self.active_site = active_site + self.active_provider = active_provider + self.remote_site = remote_site + self.remote_provider = remote_provider + self._icons = tools_lib.get_repre_icons() + if 'active_site' not in self.Columns and \ + 'remote_site' not in self.Columns: + self.Columns.extend(['active_site', 'remote_site']) + + def outdated(self, item): + value = item.get("version") + if isinstance(value, HeroVersionType): + return False + + if item.get("version") == item.get("highest_version"): + return False + return True + + def data(self, index, role): + + if not index.isValid(): + return + + item = index.internalPointer() + + if role == QtCore.Qt.FontRole: + # Make top-level entries bold + if item.get("isGroupNode") or item.get("isNotSet"): # group-item + font = QtGui.QFont() + font.setBold(True) + return font + + if role == QtCore.Qt.ForegroundRole: + # Set the text color to the OUTDATED_COLOR when the + # collected version is not the same as the highest version + key = self.Columns[index.column()] + if key == "version": # version + if item.get("isGroupNode"): # group-item + if self.outdated(item): + return self.OUTDATED_COLOR + + if self._hierarchy_view: + # If current group is not outdated, check if any + # outdated children. + for _node in lib.walk_hierarchy(item): + if self.outdated(_node): + return self.CHILD_OUTDATED_COLOR + else: + + if self._hierarchy_view: + # Although this is not a group item, we still need + # to distinguish which one contain outdated child. + for _node in lib.walk_hierarchy(item): + if self.outdated(_node): + return self.CHILD_OUTDATED_COLOR.darker(150) + + return self.GRAYOUT_COLOR + + if key == "Name" and not item.get("isGroupNode"): + return self.GRAYOUT_COLOR + + # Add icons + if role == QtCore.Qt.DecorationRole: + if index.column() == 0: + # Override color + color = item.get("color", style.colors.default) + if item.get("isGroupNode"): # group-item + return qtawesome.icon("fa.folder", color=color) + elif item.get("isNotSet"): + return qtawesome.icon("fa.exclamation-circle", color=color) + else: + return qtawesome.icon("fa.file-o", color=color) + + if index.column() == 3: + # Family icon + return item.get("familyIcon", None) + + if item.get("isGroupNode"): + column_name = self.Columns[index.column()] + if column_name == 'active_site': + return self._icons.get(item.get('active_site_provider')) + if column_name == 'remote_site': + return self._icons.get(item.get('remote_site_provider')) + + if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"): + column_name = self.Columns[index.column()] + progress = None + if column_name == 'active_site': + progress = item.get("active_site_progress", 0) + elif column_name == 'remote_site': + progress = item.get("remote_site_progress", 0) + if progress is not None: + return "{}%".format(max(progress, 0) * 100) + + if role == self.UniqueRole: + return item["representation"] + item.get("objectName", "") + + return super(InventoryModel, self).data(index, role) + + def set_hierarchy_view(self, state): + """Set whether to display subsets in hierarchy view.""" + state = bool(state) + + if state != self._hierarchy_view: + self._hierarchy_view = state + + def refresh(self, selected=None, items=None): + """Refresh the model""" + + host = api.registered_host() + if not items: # for debugging or testing, injecting items from outside + items = host.ls() + + self.clear() + + if self._hierarchy_view and selected: + + if not hasattr(host.pipeline, "update_hierarchy"): + # If host doesn't support hierarchical containers, then + # cherry-pick only. + self.add_items((item for item in items + if item["objectName"] in selected)) + + # Update hierarchy info for all containers + items_by_name = {item["objectName"]: item + for item in host.pipeline.update_hierarchy(items)} + + selected_items = set() + + def walk_children(names): + """Select containers and extend to chlid containers""" + for name in [n for n in names if n not in selected_items]: + selected_items.add(name) + item = items_by_name[name] + yield item + + for child in walk_children(item["children"]): + yield child + + items = list(walk_children(selected)) # Cherry-picked and extended + + # Cut unselected upstream containers + for item in items: + if not item.get("parent") in selected_items: + # Parent not in selection, this is root item. + item["parent"] = None + + parents = [self._root_item] + + # The length of `items` array is the maximum depth that a + # hierarchy could be. + # Take this as an easiest way to prevent looping forever. + maximum_loop = len(items) + count = 0 + while items: + if count > maximum_loop: + self.log.warning("Maximum loop count reached, possible " + "missing parent node.") + break + + _parents = list() + for parent in parents: + _unparented = list() + + def _children(): + """Child item provider""" + for item in items: + if item.get("parent") == parent.get("objectName"): + # (NOTE) + # Since `self._root_node` has no "objectName" + # entry, it will be paired with root item if + # the value of key "parent" is None, or not + # having the key. + yield item + else: + # Not current parent's child, try next + _unparented.append(item) + + self.add_items(_children(), parent) + + items[:] = _unparented + + # Parents of next level + for group_node in parent.children(): + _parents += group_node.children() + + parents[:] = _parents + count += 1 + + else: + self.add_items(items) + + def add_items(self, items, parent=None): + """Add the items to the model. + + The items should be formatted similar to `api.ls()` returns, an item + is then represented as: + {"filename_v001.ma": [full/filename/of/loaded/filename_v001.ma, + full/filename/of/loaded/filename_v001.ma], + "nodetype" : "reference", + "node": "referenceNode1"} + + Note: When performing an additional call to `add_items` it will *not* + group the new items with previously existing item groups of the + same type. + + Args: + items (generator): the items to be processed as returned by `ls()` + parent (Item, optional): Set this item as parent for the added + items when provided. Defaults to the root of the model. + + Returns: + node.Item: root node which has children added based on the data + """ + + self.beginResetModel() + + # Group by representation + grouped = defaultdict(lambda: {"items": list()}) + for item in items: + grouped[item["representation"]]["items"].append(item) + + # Add to model + not_found = defaultdict(list) + not_found_ids = [] + for repre_id, group_dict in sorted(grouped.items()): + group_items = group_dict["items"] + # Get parenthood per group + representation = io.find_one({"_id": io.ObjectId(repre_id)}) + if not representation: + not_found["representation"].append(group_items) + not_found_ids.append(repre_id) + continue + + version = io.find_one({"_id": representation["parent"]}) + if not version: + not_found["version"].append(group_items) + not_found_ids.append(repre_id) + continue + + elif version["type"] == "hero_version": + _version = io.find_one({ + "_id": version["version_id"] + }) + version["name"] = HeroVersionType(_version["name"]) + version["data"] = _version["data"] + + subset = io.find_one({"_id": version["parent"]}) + if not subset: + not_found["subset"].append(group_items) + not_found_ids.append(repre_id) + continue + + asset = io.find_one({"_id": subset["parent"]}) + if not asset: + not_found["asset"].append(group_items) + not_found_ids.append(repre_id) + continue + + grouped[repre_id].update({ + "representation": representation, + "version": version, + "subset": subset, + "asset": asset + }) + + for id in not_found_ids: + grouped.pop(id) + + for where, group_items in not_found.items(): + # create the group header + group_node = Item() + name = "< NOT FOUND - {} >".format(where) + group_node["Name"] = name + group_node["representation"] = name + group_node["count"] = len(group_items) + group_node["isGroupNode"] = False + group_node["isNotSet"] = True + + self.add_child(group_node, parent=parent) + + for items in group_items: + item_node = Item() + item_node["Name"] = ", ".join( + [item["objectName"] for item in items] + ) + self.add_child(item_node, parent=group_node) + + for repre_id, group_dict in sorted(grouped.items()): + group_items = group_dict["items"] + representation = grouped[repre_id]["representation"] + version = grouped[repre_id]["version"] + subset = grouped[repre_id]["subset"] + asset = grouped[repre_id]["asset"] + + # Get the primary family + no_family = "" + maj_version, _ = schema.get_schema_version(subset["schema"]) + if maj_version < 3: + prim_family = version["data"].get("family") + if not prim_family: + families = version["data"].get("families") + prim_family = families[0] if families else no_family + else: + families = subset["data"].get("families") or [] + prim_family = families[0] if families else no_family + + # Get the label and icon for the family if in configuration + family_config = self.family_config_cache.family_config(prim_family) + family = family_config.get("label", prim_family) + family_icon = family_config.get("icon", None) + + # Store the highest available version so the model can know + # whether current version is currently up-to-date. + highest_version = io.find_one({ + "type": "version", + "parent": version["parent"] + }, sort=[("name", -1)]) + + # create the group header + group_node = Item() + group_node["Name"] = "%s_%s: (%s)" % (asset["name"], + subset["name"], + representation["name"]) + group_node["representation"] = repre_id + group_node["version"] = version["name"] + group_node["highest_version"] = highest_version["name"] + group_node["family"] = family + group_node["familyIcon"] = family_icon + group_node["count"] = len(group_items) + group_node["isGroupNode"] = True + + if self.sync_enabled: + progress = tools_lib.get_progress_for_repre(representation, + self.active_site, + self.remote_site) + group_node["active_site"] = self.active_site + group_node["active_site_provider"] = self.active_provider + group_node["remote_site"] = self.remote_site + group_node["remote_site_provider"] = self.remote_provider + group_node["active_site_progress"] = progress[self.active_site] + group_node["remote_site_progress"] = progress[self.remote_site] + + self.add_child(group_node, parent=parent) + + for item in group_items: + item_node = Item() + item_node.update(item) + + # store the current version on the item + item_node["version"] = version["name"] + + # Remapping namespace to item name. + # Noted that the name key is capital "N", by doing this, we + # can view namespace in GUI without changing container data. + item_node["Name"] = item["namespace"] + + self.add_child(item_node, parent=group_node) + + self.endResetModel() + + return self._root_item diff --git a/openpype/tools/sceneinventory/proxy.py b/openpype/tools/sceneinventory/proxy.py new file mode 100644 index 0000000000..307e032eb6 --- /dev/null +++ b/openpype/tools/sceneinventory/proxy.py @@ -0,0 +1,148 @@ +import re + +from ...vendor.Qt import QtCore + +from . import lib + + +class FilterProxyModel(QtCore.QSortFilterProxyModel): + """Filter model to where key column's value is in the filtered tags""" + + def __init__(self, *args, **kwargs): + super(FilterProxyModel, self).__init__(*args, **kwargs) + self._filter_outdated = False + self._hierarchy_view = False + + def filterAcceptsRow(self, row, parent): + + model = self.sourceModel() + source_index = model.index(row, + self.filterKeyColumn(), + parent) + + # Always allow bottom entries (individual containers), since their + # parent group hidden if it wouldn't have been validated. + rows = model.rowCount(source_index) + if not rows: + return True + + # Filter by regex + if not self.filterRegExp().isEmpty(): + pattern = re.escape(self.filterRegExp().pattern()) + + if not self._matches(row, parent, pattern): + return False + + if self._filter_outdated: + # When filtering to outdated we filter the up to date entries + # thus we "allow" them when they are outdated + if not self._is_outdated(row, parent): + return False + + return True + + def set_filter_outdated(self, state): + """Set whether to show the outdated entries only.""" + state = bool(state) + + if state != self._filter_outdated: + self._filter_outdated = bool(state) + self.invalidateFilter() + + def set_hierarchy_view(self, state): + state = bool(state) + + if state != self._hierarchy_view: + self._hierarchy_view = state + + def _is_outdated(self, row, parent): + """Return whether row is outdated. + + A row is considered outdated if it has "version" and "highest_version" + data and in the internal data structure, and they are not of an + equal value. + + """ + def outdated(node): + version = node.get("version", None) + highest = node.get("highest_version", None) + + # Always allow indices that have no version data at all + if version is None and highest is None: + return True + + # If either a version or highest is present but not the other + # consider the item invalid. + if not self._hierarchy_view: + # Skip this check if in hierarchy view, or the child item + # node will be hidden even it's actually outdated. + if version is None or highest is None: + return False + return version != highest + + index = self.sourceModel().index(row, self.filterKeyColumn(), parent) + + # The scene contents are grouped by "representation", e.g. the same + # "representation" loaded twice is grouped under the same header. + # Since the version check filters these parent groups we skip that + # check for the individual children. + has_parent = index.parent().isValid() + if has_parent and not self._hierarchy_view: + return True + + # Filter to those that have the different version numbers + node = index.internalPointer() + is_outdated = outdated(node) + + if is_outdated: + return True + + elif self._hierarchy_view: + for _node in lib.walk_hierarchy(node): + if outdated(_node): + return True + return False + else: + return False + + def _matches(self, row, parent, pattern): + """Return whether row matches regex pattern. + + Args: + row (int): row number in model + parent (QtCore.QModelIndex): parent index + pattern (regex.pattern): pattern to check for in key + + Returns: + bool + + """ + model = self.sourceModel() + column = self.filterKeyColumn() + role = self.filterRole() + + def matches(row, parent, pattern): + index = model.index(row, column, parent) + key = model.data(index, role) + if re.search(pattern, key, re.IGNORECASE): + return True + + if not matches(row, parent, pattern): + # Also allow if any of the children matches + source_index = model.index(row, column, parent) + rows = model.rowCount(source_index) + + if not any(matches(i, source_index, pattern) + for i in range(rows)): + + if self._hierarchy_view: + for i in range(rows): + child_i = model.index(i, column, source_index) + child_rows = model.rowCount(child_i) + return any(self._matches(ch_i, child_i, pattern) + for ch_i in range(child_rows)) + + else: + return False + + return True From 48005747cdf956b1356919840ec2b4cb2c79a322 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 18:53:45 +0100 Subject: [PATCH 28/60] renamed app.py to window.py and renamed Window to SceneInventoryWindow --- openpype/tools/sceneinventory/__init__.py | 8 +++++--- openpype/tools/sceneinventory/{app.py => window.py} | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) rename openpype/tools/sceneinventory/{app.py => window.py} (99%) diff --git a/openpype/tools/sceneinventory/__init__.py b/openpype/tools/sceneinventory/__init__.py index 694caf15fe..410b52e5fe 100644 --- a/openpype/tools/sceneinventory/__init__.py +++ b/openpype/tools/sceneinventory/__init__.py @@ -1,7 +1,9 @@ -from .app import ( +from .window import ( show, + SceneInventoryWindow ) -__all__ = [ +__all__ = ( "show", -] + "SceneInventoryWindow" +) diff --git a/openpype/tools/sceneinventory/app.py b/openpype/tools/sceneinventory/window.py similarity index 99% rename from openpype/tools/sceneinventory/app.py rename to openpype/tools/sceneinventory/window.py index 5304b7ac12..93c1debe3d 100644 --- a/openpype/tools/sceneinventory/app.py +++ b/openpype/tools/sceneinventory/window.py @@ -1799,11 +1799,11 @@ class SwitchAssetDialog(QtWidgets.QDialog): self.close() -class Window(QtWidgets.QDialog): +class SceneInventoryWindow(QtWidgets.QDialog): """Scene Inventory window""" def __init__(self, parent=None): - QtWidgets.QDialog.__init__(self, parent) + super(SceneInventoryWindow, self).__init__(parent) self.resize(1100, 480) self.setWindowTitle( @@ -1941,7 +1941,7 @@ def show(root=None, debug=False, parent=None, items=None): api.Session["AVALON_PROJECT"] = os.environ.get("AVALON_PROJECT") with tools_lib.application(): - window = Window(parent) + window = SceneInventoryWindow(parent) window.setStyleSheet(style.load_stylesheet()) window.show() window.refresh(items=items) From 0c8a517d075fddd3eff73e68a124a167231977ee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 19:06:16 +0100 Subject: [PATCH 29/60] separated switch dialog from window file --- .../tools/sceneinventory/switch_dialog.py | 989 ++++++++++++++++ openpype/tools/sceneinventory/widgets.py | 51 + openpype/tools/sceneinventory/window.py | 1021 +---------------- 3 files changed, 1041 insertions(+), 1020 deletions(-) create mode 100644 openpype/tools/sceneinventory/switch_dialog.py create mode 100644 openpype/tools/sceneinventory/widgets.py diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py new file mode 100644 index 0000000000..37659b2370 --- /dev/null +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -0,0 +1,989 @@ +import collections +from Qt import QtWidgets, QtCore + +from avalon import io, api, style +from avalon.vendor import qtawesome + +from .widgets import SearchComboBox + + +class ValidationState: + def __init__(self): + self.asset_ok = True + self.subset_ok = True + self.repre_ok = True + + @property + def all_ok(self): + return ( + self.asset_ok + and self.subset_ok + and self.repre_ok + ) + + +class SwitchAssetDialog(QtWidgets.QDialog): + """Widget to support asset switching""" + + MIN_WIDTH = 550 + + switched = QtCore.Signal() + + def __init__(self, parent=None, items=None): + super(SwitchAssetDialog, self).__init__(parent) + + self.setWindowTitle("Switch selected items ...") + + # Force and keep focus dialog + self.setModal(True) + + assets_combox = SearchComboBox(self) + subsets_combox = SearchComboBox(self) + repres_combobox = SearchComboBox(self) + + assets_combox.set_placeholder("") + subsets_combox.set_placeholder("") + repres_combobox.set_placeholder("") + + asset_label = QtWidgets.QLabel(self) + subset_label = QtWidgets.QLabel(self) + repre_label = QtWidgets.QLabel(self) + + current_asset_btn = QtWidgets.QPushButton("Use current asset") + + accept_icon = qtawesome.icon("fa.check", color="white") + accept_btn = QtWidgets.QPushButton(self) + accept_btn.setIcon(accept_icon) + + main_layout = QtWidgets.QGridLayout(self) + # Asset column + main_layout.addWidget(current_asset_btn, 0, 0) + main_layout.addWidget(assets_combox, 1, 0) + main_layout.addWidget(asset_label, 2, 0) + # Subset column + main_layout.addWidget(subsets_combox, 1, 1) + main_layout.addWidget(subset_label, 2, 1) + # Representation column + main_layout.addWidget(repres_combobox, 1, 2) + main_layout.addWidget(repre_label, 2, 2) + # Btn column + main_layout.addWidget(accept_btn, 1, 3) + + assets_combox.currentIndexChanged.connect( + self._combobox_value_changed + ) + subsets_combox.currentIndexChanged.connect( + self._combobox_value_changed + ) + repres_combobox.currentIndexChanged.connect( + self._combobox_value_changed + ) + accept_btn.clicked.connect(self._on_accept) + current_asset_btn.clicked.connect(self._on_current_asset) + + self._current_asset_btn = current_asset_btn + + self._assets_box = assets_combox + self._subsets_box = subsets_combox + self._representations_box = repres_combobox + + self._asset_label = asset_label + self._subset_label = subset_label + self._repre_label = repre_label + + self._accept_btn = accept_btn + + self._init_asset_name = None + self._init_subset_name = None + self._init_repre_name = None + + self._fill_check = False + + self._items = items + self._prepare_content_data() + self.refresh(True) + + self.setMinimumWidth(self.MIN_WIDTH) + + # Set default focus to accept button so you don't directly type in + # first asset field, this also allows to see the placeholder value. + accept_btn.setFocus() + + def _prepare_content_data(self): + repre_ids = [ + io.ObjectId(item["representation"]) + for item in self._items + ] + repres = list(io.find({ + "type": {"$in": ["representation", "archived_representation"]}, + "_id": {"$in": repre_ids} + })) + repres_by_id = {repre["_id"]: repre for repre in repres} + + # stash context values, works only for single representation + if len(repres) == 1: + self._init_asset_name = repres[0]["context"]["asset"] + self._init_subset_name = repres[0]["context"]["subset"] + self._init_repre_name = repres[0]["context"]["representation"] + + content_repres = {} + archived_repres = [] + missing_repres = [] + version_ids = [] + for repre_id in repre_ids: + if repre_id not in repres_by_id: + missing_repres.append(repre_id) + elif repres_by_id[repre_id]["type"] == "archived_representation": + repre = repres_by_id[repre_id] + archived_repres.append(repre) + version_ids.append(repre["parent"]) + else: + repre = repres_by_id[repre_id] + content_repres[repre_id] = repres_by_id[repre_id] + version_ids.append(repre["parent"]) + + versions = io.find({ + "type": {"$in": ["version", "hero_version"]}, + "_id": {"$in": list(set(version_ids))} + }) + content_versions = {} + hero_version_ids = set() + for version in versions: + content_versions[version["_id"]] = version + if version["type"] == "hero_version": + hero_version_ids.add(version["_id"]) + + missing_versions = [] + subset_ids = [] + for version_id in version_ids: + if version_id not in content_versions: + missing_versions.append(version_id) + else: + subset_ids.append(content_versions[version_id]["parent"]) + + subsets = io.find({ + "type": {"$in": ["subset", "archived_subset"]}, + "_id": {"$in": subset_ids} + }) + subsets_by_id = {sub["_id"]: sub for sub in subsets} + + asset_ids = [] + archived_subsets = [] + missing_subsets = [] + content_subsets = {} + for subset_id in subset_ids: + if subset_id not in subsets_by_id: + missing_subsets.append(subset_id) + elif subsets_by_id[subset_id]["type"] == "archived_subset": + subset = subsets_by_id[subset_id] + asset_ids.append(subset["parent"]) + archived_subsets.append(subset) + else: + subset = subsets_by_id[subset_id] + asset_ids.append(subset["parent"]) + content_subsets[subset_id] = subset + + assets = io.find({ + "type": {"$in": ["asset", "archived_asset"]}, + "_id": {"$in": list(asset_ids)} + }) + assets_by_id = {asset["_id"]: asset for asset in assets} + + missing_assets = [] + archived_assets = [] + content_assets = {} + for asset_id in asset_ids: + if asset_id not in assets_by_id: + missing_assets.append(asset_id) + elif assets_by_id[asset_id]["type"] == "archived_asset": + archived_assets.append(assets_by_id[asset_id]) + else: + content_assets[asset_id] = assets_by_id[asset_id] + + self.content_assets = content_assets + self.content_subsets = content_subsets + self.content_versions = content_versions + self.content_repres = content_repres + + self.hero_version_ids = hero_version_ids + + self.missing_assets = missing_assets + self.missing_versions = missing_versions + self.missing_subsets = missing_subsets + self.missing_repres = missing_repres + self.missing_docs = ( + bool(missing_assets) + or bool(missing_versions) + or bool(missing_subsets) + or bool(missing_repres) + ) + + self.archived_assets = archived_assets + self.archived_subsets = archived_subsets + self.archived_repres = archived_repres + + def _combobox_value_changed(self, *args, **kwargs): + self.refresh() + + def refresh(self, init_refresh=False): + """Build the need comboboxes with content""" + if not self._fill_check and not init_refresh: + return + + self._fill_check = False + + if init_refresh: + asset_values = self._get_asset_box_values() + self._fill_combobox(asset_values, "asset") + + validation_state = ValidationState() + + # Set other comboboxes to empty if any document is missing or any asset + # of loaded representations is archived. + self._is_asset_ok(validation_state) + if validation_state.asset_ok: + subset_values = self._get_subset_box_values() + self._fill_combobox(subset_values, "subset") + self._is_subset_ok(validation_state) + + if validation_state.asset_ok and validation_state.subset_ok: + repre_values = sorted(self._representations_box_values()) + self._fill_combobox(repre_values, "repre") + self._is_repre_ok(validation_state) + + # Fill comboboxes with values + self.set_labels() + self.apply_validations(validation_state) + + if init_refresh: # pre select context if possible + self._assets_box.set_valid_value(self._init_asset_name) + self._subsets_box.set_valid_value(self._init_subset_name) + self._representations_box.set_valid_value(self._init_repre_name) + + self._fill_check = True + + def _get_loaders(self, representations): + if not representations: + return list() + + available_loaders = filter( + lambda l: not (hasattr(l, "is_utility") and l.is_utility), + api.discover(api.Loader) + ) + + loaders = set() + + for representation in representations: + for loader in api.loaders_from_representation( + available_loaders, + representation + ): + loaders.add(loader) + + return loaders + + def _fill_combobox(self, values, combobox_type): + if combobox_type == "asset": + combobox_widget = self._assets_box + elif combobox_type == "subset": + combobox_widget = self._subsets_box + elif combobox_type == "repre": + combobox_widget = self._representations_box + else: + return + selected_value = combobox_widget.get_valid_value() + + # Fill combobox + if values is not None: + combobox_widget.populate(list(sorted(values))) + if selected_value and selected_value in values: + index = None + for idx in range(combobox_widget.count()): + if selected_value == str(combobox_widget.itemText(idx)): + index = idx + break + if index is not None: + combobox_widget.setCurrentIndex(index) + + def set_labels(self): + asset_label = self._assets_box.get_valid_value() + subset_label = self._subsets_box.get_valid_value() + repre_label = self._representations_box.get_valid_value() + + default = "*No changes" + self._asset_label.setText(asset_label or default) + self._subset_label.setText(subset_label or default) + self._repre_label.setText(repre_label or default) + + def apply_validations(self, validation_state): + error_msg = "*Please select" + error_sheet = "border: 1px solid red;" + success_sheet = "border: 1px solid green;" + + asset_sheet = None + subset_sheet = None + repre_sheet = None + accept_sheet = None + if validation_state.asset_ok is False: + asset_sheet = error_sheet + self._asset_label.setText(error_msg) + elif validation_state.subset_ok is False: + subset_sheet = error_sheet + self._subset_label.setText(error_msg) + elif validation_state.repre_ok is False: + repre_sheet = error_sheet + self._repre_label.setText(error_msg) + + if validation_state.all_ok: + accept_sheet = success_sheet + + self._assets_box.setStyleSheet(asset_sheet or "") + self._subsets_box.setStyleSheet(subset_sheet or "") + self._representations_box.setStyleSheet(repre_sheet or "") + + self._accept_btn.setEnabled(validation_state.all_ok) + self._accept_btn.setStyleSheet(accept_sheet or "") + + def _get_asset_box_values(self): + asset_docs = io.find( + {"type": "asset"}, + {"_id": 1, "name": 1} + ) + asset_names_by_id = { + asset_doc["_id"]: asset_doc["name"] + for asset_doc in asset_docs + } + subsets = io.find( + { + "type": "subset", + "parent": {"$in": list(asset_names_by_id.keys())} + }, + { + "parent": 1 + } + ) + + filtered_assets = [] + for subset in subsets: + asset_name = asset_names_by_id[subset["parent"]] + if asset_name not in filtered_assets: + filtered_assets.append(asset_name) + return sorted(filtered_assets) + + def _get_subset_box_values(self): + selected_asset = self._assets_box.get_valid_value() + if selected_asset: + asset_doc = io.find_one({"type": "asset", "name": selected_asset}) + asset_ids = [asset_doc["_id"]] + else: + asset_ids = list(self.content_assets.keys()) + + subsets = io.find( + { + "type": "subset", + "parent": {"$in": asset_ids} + }, + { + "parent": 1, + "name": 1 + } + ) + + subset_names_by_parent_id = collections.defaultdict(set) + for subset in subsets: + subset_names_by_parent_id[subset["parent"]].add(subset["name"]) + + possible_subsets = None + for subset_names in subset_names_by_parent_id.values(): + if possible_subsets is None: + possible_subsets = subset_names + else: + possible_subsets = (possible_subsets & subset_names) + + if not possible_subsets: + break + + return list(possible_subsets or list()) + + def _representations_box_values(self): + # NOTE hero versions are not used because it is expected that + # hero version has same representations as latests + selected_asset = self._assets_box.currentText() + selected_subset = self._subsets_box.currentText() + + # If nothing is selected + # [ ] [ ] [?] + if not selected_asset and not selected_subset: + # Find all representations of selection's subsets + possible_repres = list(io.find( + { + "type": "representation", + "parent": {"$in": list(self.content_versions.keys())} + }, + { + "parent": 1, + "name": 1 + } + )) + + possible_repres_by_parent = collections.defaultdict(set) + for repre in possible_repres: + possible_repres_by_parent[repre["parent"]].add(repre["name"]) + + output_repres = None + for repre_names in possible_repres_by_parent.values(): + if output_repres is None: + output_repres = repre_names + else: + output_repres = (output_repres & repre_names) + + if not output_repres: + break + + return list(output_repres or list()) + + # [x] [x] [?] + if selected_asset and selected_subset: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_doc = io.find_one( + { + "type": "subset", + "name": selected_subset, + "parent": asset_doc["_id"] + }, + {"_id": 1} + ) + subset_id = subset_doc["_id"] + last_versions_by_subset_id = self.find_last_versions([subset_id]) + version_doc = last_versions_by_subset_id.get(subset_id) + repre_docs = io.find( + { + "type": "representation", + "parent": version_doc["_id"] + }, + { + "name": 1 + } + ) + return [ + repre_doc["name"] + for repre_doc in repre_docs + ] + + # [x] [ ] [?] + # If asset only is selected + if selected_asset: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + if not asset_doc: + return list() + + # Filter subsets by subset names from content + subset_names = set() + for subset_doc in self.content_subsets.values(): + subset_names.add(subset_doc["name"]) + subset_docs = io.find( + { + "type": "subset", + "parent": asset_doc["_id"], + "name": {"$in": list(subset_names)} + }, + {"_id": 1} + ) + subset_ids = [ + subset_doc["_id"] + for subset_doc in subset_docs + ] + if not subset_ids: + return list() + + last_versions_by_subset_id = self.find_last_versions(subset_ids) + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + if not subset_id_by_version_id: + return list() + + repre_docs = list(io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + )) + if not repre_docs: + return list() + + repre_names_by_parent = collections.defaultdict(set) + for repre_doc in repre_docs: + repre_names_by_parent[repre_doc["parent"]].add( + repre_doc["name"] + ) + + available_repres = None + for repre_names in repre_names_by_parent.values(): + if available_repres is None: + available_repres = repre_names + continue + + available_repres = available_repres.intersection(repre_names) + + return list(available_repres) + + # [ ] [x] [?] + subset_docs = list(io.find( + { + "type": "subset", + "parent": {"$in": list(self.content_assets.keys())}, + "name": selected_subset + }, + {"_id": 1, "parent": 1} + )) + if not subset_docs: + return list() + + subset_docs_by_id = { + subset_doc["_id"]: subset_doc + for subset_doc in subset_docs + } + last_versions_by_subset_id = self.find_last_versions( + subset_docs_by_id.keys() + ) + + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + if not subset_id_by_version_id: + return list() + + repre_docs = list(io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + )) + if not repre_docs: + return list() + + repre_names_by_asset_id = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + asset_id = subset_docs_by_id[subset_id]["parent"] + if asset_id not in repre_names_by_asset_id: + repre_names_by_asset_id[asset_id] = set() + repre_names_by_asset_id[asset_id].add(repre_doc["name"]) + + available_repres = None + for repre_names in repre_names_by_asset_id.values(): + if available_repres is None: + available_repres = repre_names + continue + + available_repres = available_repres.intersection(repre_names) + + return list(available_repres) + + def _is_asset_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + if ( + selected_asset is None + and (self.missing_docs or self.archived_assets) + ): + validation_state.asset_ok = False + + def _is_subset_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + + # [?] [x] [?] + # If subset is selected then must be ok + if selected_subset is not None: + return + + # [ ] [ ] [?] + if selected_asset is None: + # If there were archived subsets and asset is not selected + if self.archived_subsets: + validation_state.subset_ok = False + return + + # [x] [ ] [?] + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_docs = io.find( + {"type": "subset", "parent": asset_doc["_id"]}, + {"name": 1} + ) + subset_names = set( + subset_doc["name"] + for subset_doc in subset_docs + ) + + for subset_doc in self.content_subsets.values(): + if subset_doc["name"] not in subset_names: + validation_state.subset_ok = False + break + + def find_last_versions(self, subset_ids): + _pipeline = [ + # Find all versions of those subsets + {"$match": { + "type": "version", + "parent": {"$in": list(subset_ids)} + }}, + # Sorting versions all together + {"$sort": {"name": 1}}, + # Group them by "parent", but only take the last + {"$group": { + "_id": "$parent", + "_version_id": {"$last": "$_id"}, + "type": {"$last": "$type"} + }} + ] + last_versions_by_subset_id = dict() + for doc in io.aggregate(_pipeline): + doc["parent"] = doc["_id"] + doc["_id"] = doc.pop("_version_id") + last_versions_by_subset_id[doc["parent"]] = doc + return last_versions_by_subset_id + + def _is_repre_ok(self, validation_state): + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + selected_repre = self._representations_box.get_valid_value() + + # [?] [?] [x] + # If subset is selected then must be ok + if selected_repre is not None: + return + + # [ ] [ ] [ ] + if selected_asset is None and selected_subset is None: + if ( + self.archived_repres + or self.missing_versions + or self.missing_repres + ): + validation_state.repre_ok = False + return + + # [x] [x] [ ] + if selected_asset is not None and selected_subset is not None: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_doc = io.find_one( + { + "type": "subset", + "parent": asset_doc["_id"], + "name": selected_subset + }, + {"_id": 1} + ) + last_versions_by_subset_id = self.find_last_versions( + [subset_doc["_id"]] + ) + last_version = last_versions_by_subset_id.get(subset_doc["_id"]) + if not last_version: + validation_state.repre_ok = False + return + + repre_docs = io.find( + { + "type": "representation", + "parent": last_version["_id"] + }, + {"name": 1} + ) + + repre_names = set( + repre_doc["name"] + for repre_doc in repre_docs + ) + for repre_doc in self.content_repres.values(): + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + return + + # [x] [ ] [ ] + if selected_asset is not None: + asset_doc = io.find_one( + {"type": "asset", "name": selected_asset}, + {"_id": 1} + ) + subset_docs = list(io.find( + { + "type": "subset", + "parent": asset_doc["_id"] + }, + {"_id": 1, "name": 1} + )) + + subset_name_by_id = {} + subset_ids = set() + for subset_doc in subset_docs: + subset_id = subset_doc["_id"] + subset_ids.add(subset_id) + subset_name_by_id[subset_id] = subset_doc["name"] + + last_versions_by_subset_id = self.find_last_versions(subset_ids) + + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + repre_docs = io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + ) + repres_by_subset_name = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + subset_name = subset_name_by_id[subset_id] + if subset_name not in repres_by_subset_name: + repres_by_subset_name[subset_name] = set() + repres_by_subset_name[subset_name].add(repre_doc["name"]) + + for repre_doc in self.content_repres.values(): + version_doc = self.content_versions[repre_doc["parent"]] + subset_doc = self.content_subsets[version_doc["parent"]] + repre_names = ( + repres_by_subset_name.get(subset_doc["name"]) or [] + ) + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + return + + # [ ] [x] [ ] + # Subset documents + subset_docs = io.find( + { + "type": "subset", + "parent": {"$in": list(self.content_assets.keys())}, + "name": selected_subset + }, + {"_id": 1, "name": 1, "parent": 1} + ) + + subset_docs_by_id = {} + for subset_doc in subset_docs: + subset_docs_by_id[subset_doc["_id"]] = subset_doc + + last_versions_by_subset_id = self.find_last_versions( + subset_docs_by_id.keys() + ) + subset_id_by_version_id = {} + for subset_id, last_version in last_versions_by_subset_id.items(): + version_id = last_version["_id"] + subset_id_by_version_id[version_id] = subset_id + + repre_docs = io.find( + { + "type": "representation", + "parent": {"$in": list(subset_id_by_version_id.keys())} + }, + { + "name": 1, + "parent": 1 + } + ) + repres_by_asset_id = {} + for repre_doc in repre_docs: + subset_id = subset_id_by_version_id[repre_doc["parent"]] + asset_id = subset_docs_by_id[subset_id]["parent"] + if asset_id not in repres_by_asset_id: + repres_by_asset_id[asset_id] = set() + repres_by_asset_id[asset_id].add(repre_doc["name"]) + + for repre_doc in self.content_repres.values(): + version_doc = self.content_versions[repre_doc["parent"]] + subset_doc = self.content_subsets[version_doc["parent"]] + asset_id = subset_doc["parent"] + repre_names = ( + repres_by_asset_id.get(asset_id) or [] + ) + if repre_doc["name"] not in repre_names: + validation_state.repre_ok = False + break + + def _on_current_asset(self): + # Set initial asset as current. + asset_name = io.Session["AVALON_ASSET"] + index = self._assets_box.findText( + asset_name, QtCore.Qt.MatchFixedString + ) + if index >= 0: + print("Setting asset to {}".format(asset_name)) + self._assets_box.setCurrentIndex(index) + + def _on_accept(self): + # Use None when not a valid value or when placeholder value + selected_asset = self._assets_box.get_valid_value() + selected_subset = self._subsets_box.get_valid_value() + selected_representation = self._representations_box.get_valid_value() + + if selected_asset: + asset_doc = io.find_one({"type": "asset", "name": selected_asset}) + asset_docs_by_id = {asset_doc["_id"]: asset_doc} + else: + asset_docs_by_id = self.content_assets + + asset_docs_by_name = { + asset_doc["name"]: asset_doc + for asset_doc in asset_docs_by_id.values() + } + + asset_ids = list(asset_docs_by_id.keys()) + + subset_query = { + "type": "subset", + "parent": {"$in": asset_ids} + } + if selected_subset: + subset_query["name"] = selected_subset + + subset_docs = list(io.find(subset_query)) + subset_ids = [] + subset_docs_by_parent_and_name = collections.defaultdict(dict) + for subset in subset_docs: + subset_ids.append(subset["_id"]) + parent_id = subset["parent"] + name = subset["name"] + subset_docs_by_parent_and_name[parent_id][name] = subset + + # versions + version_docs = list(io.find({ + "type": "version", + "parent": {"$in": subset_ids} + }, sort=[("name", -1)])) + + hero_version_docs = list(io.find({ + "type": "hero_version", + "parent": {"$in": subset_ids} + })) + + version_ids = list() + + version_docs_by_parent_id = {} + for version_doc in version_docs: + parent_id = version_doc["parent"] + if parent_id not in version_docs_by_parent_id: + version_ids.append(version_doc["_id"]) + version_docs_by_parent_id[parent_id] = version_doc + + hero_version_docs_by_parent_id = {} + for hero_version_doc in hero_version_docs: + version_ids.append(hero_version_doc["_id"]) + parent_id = hero_version_doc["parent"] + hero_version_docs_by_parent_id[parent_id] = hero_version_doc + + repre_docs = io.find({ + "type": "representation", + "parent": {"$in": version_ids} + }) + repre_docs_by_parent_id_by_name = collections.defaultdict(dict) + for repre_doc in repre_docs: + parent_id = repre_doc["parent"] + name = repre_doc["name"] + repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc + + for container in self._items: + container_repre_id = io.ObjectId(container["representation"]) + container_repre = self.content_repres[container_repre_id] + container_repre_name = container_repre["name"] + + container_version_id = container_repre["parent"] + container_version = self.content_versions[container_version_id] + + container_subset_id = container_version["parent"] + container_subset = self.content_subsets[container_subset_id] + container_subset_name = container_subset["name"] + + container_asset_id = container_subset["parent"] + container_asset = self.content_assets[container_asset_id] + container_asset_name = container_asset["name"] + + if selected_asset: + asset_doc = asset_docs_by_name[selected_asset] + else: + asset_doc = asset_docs_by_name[container_asset_name] + + subsets_by_name = subset_docs_by_parent_and_name[asset_doc["_id"]] + if selected_subset: + subset_doc = subsets_by_name[selected_subset] + else: + subset_doc = subsets_by_name[container_subset_name] + + repre_doc = None + subset_id = subset_doc["_id"] + if container_version["type"] == "hero_version": + hero_version = hero_version_docs_by_parent_id.get( + subset_id + ) + if hero_version: + _repres = repre_docs_by_parent_id_by_name.get( + hero_version["_id"] + ) + if selected_representation: + repre_doc = _repres.get(selected_representation) + else: + repre_doc = _repres.get(container_repre_name) + + if not repre_doc: + version_doc = version_docs_by_parent_id[subset_id] + version_id = version_doc["_id"] + repres_by_name = repre_docs_by_parent_id_by_name[version_id] + if selected_representation: + repre_doc = repres_by_name[selected_representation] + else: + repre_doc = repres_by_name[container_repre_name] + + try: + api.switch(container, repre_doc) + except Exception: + log.warning( + ( + "Couldn't switch asset." + "See traceback for more information." + ), + exc_info=True + ) + dialog = QtWidgets.QMessageBox() + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle("Switch asset failed") + msg = "Switch asset failed. "\ + "Search console log for more details" + dialog.setText(msg) + dialog.exec_() + + self.switched.emit() + + self.close() diff --git a/openpype/tools/sceneinventory/widgets.py b/openpype/tools/sceneinventory/widgets.py new file mode 100644 index 0000000000..6bb74d2d1b --- /dev/null +++ b/openpype/tools/sceneinventory/widgets.py @@ -0,0 +1,51 @@ +from Qt import QtWidgets, QtCore + + +class SearchComboBox(QtWidgets.QComboBox): + """Searchable ComboBox with empty placeholder value as first value""" + + def __init__(self, parent=None): + super(SearchComboBox, self).__init__(parent) + + self.setEditable(True) + self.setInsertPolicy(self.NoInsert) + + # Apply completer settings + completer = self.completer() + completer.setCompletionMode(completer.PopupCompletion) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + + # Force style sheet on popup menu + # It won't take the parent stylesheet for some reason + # todo: better fix for completer popup stylesheet + # if module.window: + # popup = completer.popup() + # popup.setStyleSheet(module.window.styleSheet()) + + def set_placeholder(self, placeholder): + self.lineEdit().setPlaceholderText(placeholder) + + def populate(self, items): + self.clear() + self.addItems([""]) # ensure first item is placeholder + self.addItems(items) + + def get_valid_value(self): + """Return the current text if it's a valid value else None + + Note: The empty placeholder value is valid and returns as "" + + """ + + text = self.currentText() + lookup = set(self.itemText(i) for i in range(self.count())) + if text not in lookup: + return None + + return text or None + + def set_valid_value(self, value): + """Try to locate 'value' and pre-select it in dropdown.""" + index = self.findText(value) + if index > -1: + self.setCurrentIndex(index) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 93c1debe3d..1bd96ef85e 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -14,6 +14,7 @@ from ..delegates import VersionDelegate from .proxy import FilterProxyModel from .model import InventoryModel +from .switch_dialog import SwitchAssetDialog from openpype.modules import ModulesManager @@ -779,1026 +780,6 @@ class View(QtWidgets.QTreeView): dialog.exec_() -class SearchComboBox(QtWidgets.QComboBox): - """Searchable ComboBox with empty placeholder value as first value""" - - def __init__(self, parent=None, placeholder=""): - super(SearchComboBox, self).__init__(parent) - - self.setEditable(True) - self.setInsertPolicy(self.NoInsert) - self.lineEdit().setPlaceholderText(placeholder) - - # Apply completer settings - completer = self.completer() - completer.setCompletionMode(completer.PopupCompletion) - completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) - - # Force style sheet on popup menu - # It won't take the parent stylesheet for some reason - # todo: better fix for completer popup stylesheet - if module.window: - popup = completer.popup() - popup.setStyleSheet(module.window.styleSheet()) - - def populate(self, items): - self.clear() - self.addItems([""]) # ensure first item is placeholder - self.addItems(items) - - def get_valid_value(self): - """Return the current text if it's a valid value else None - - Note: The empty placeholder value is valid and returns as "" - - """ - - text = self.currentText() - lookup = set(self.itemText(i) for i in range(self.count())) - if text not in lookup: - return None - - return text or None - - def set_valid_value(self, value): - """Try to locate 'value' and pre-select it in dropdown.""" - index = self.findText(value) - if index > -1: - self.setCurrentIndex(index) - - -class ValidationState: - def __init__(self): - self.asset_ok = True - self.subset_ok = True - self.repre_ok = True - - @property - def all_ok(self): - return ( - self.asset_ok - and self.subset_ok - and self.repre_ok - ) - - -class SwitchAssetDialog(QtWidgets.QDialog): - """Widget to support asset switching""" - - MIN_WIDTH = 550 - - fill_check = False - switched = QtCore.Signal() - - def __init__(self, parent=None, items=None): - QtWidgets.QDialog.__init__(self, parent) - - self.setWindowTitle("Switch selected items ...") - - # Force and keep focus dialog - self.setModal(True) - - self._assets_box = SearchComboBox(placeholder="") - self._subsets_box = SearchComboBox(placeholder="") - self._representations_box = SearchComboBox( - placeholder="" - ) - - self._asset_label = QtWidgets.QLabel("") - self._subset_label = QtWidgets.QLabel("") - self._repre_label = QtWidgets.QLabel("") - - self.current_asset_btn = QtWidgets.QPushButton("Use current asset") - - main_layout = QtWidgets.QGridLayout(self) - - accept_icon = qtawesome.icon("fa.check", color="white") - accept_btn = QtWidgets.QPushButton() - accept_btn.setIcon(accept_icon) - accept_btn.setFixedWidth(24) - accept_btn.setFixedHeight(24) - - # Asset column - main_layout.addWidget(self.current_asset_btn, 0, 0) - main_layout.addWidget(self._assets_box, 1, 0) - main_layout.addWidget(self._asset_label, 2, 0) - # Subset column - main_layout.addWidget(self._subsets_box, 1, 1) - main_layout.addWidget(self._subset_label, 2, 1) - # Representation column - main_layout.addWidget(self._representations_box, 1, 2) - main_layout.addWidget(self._repre_label, 2, 2) - # Btn column - main_layout.addWidget(accept_btn, 1, 3) - - self._accept_btn = accept_btn - - self._assets_box.currentIndexChanged.connect( - self._combobox_value_changed - ) - self._subsets_box.currentIndexChanged.connect( - self._combobox_value_changed - ) - self._representations_box.currentIndexChanged.connect( - self._combobox_value_changed - ) - self._accept_btn.clicked.connect(self._on_accept) - self.current_asset_btn.clicked.connect(self._on_current_asset) - - self._init_asset_name = None - self._init_subset_name = None - self._init_repre_name = None - - self._items = items - self._prepare_content_data() - self.refresh(True) - - self.setMinimumWidth(self.MIN_WIDTH) - - # Set default focus to accept button so you don't directly type in - # first asset field, this also allows to see the placeholder value. - accept_btn.setFocus() - - def _prepare_content_data(self): - repre_ids = [ - io.ObjectId(item["representation"]) - for item in self._items - ] - repres = list(io.find({ - "type": {"$in": ["representation", "archived_representation"]}, - "_id": {"$in": repre_ids} - })) - repres_by_id = {repre["_id"]: repre for repre in repres} - - # stash context values, works only for single representation - if len(repres) == 1: - self._init_asset_name = repres[0]["context"]["asset"] - self._init_subset_name = repres[0]["context"]["subset"] - self._init_repre_name = repres[0]["context"]["representation"] - - content_repres = {} - archived_repres = [] - missing_repres = [] - version_ids = [] - for repre_id in repre_ids: - if repre_id not in repres_by_id: - missing_repres.append(repre_id) - elif repres_by_id[repre_id]["type"] == "archived_representation": - repre = repres_by_id[repre_id] - archived_repres.append(repre) - version_ids.append(repre["parent"]) - else: - repre = repres_by_id[repre_id] - content_repres[repre_id] = repres_by_id[repre_id] - version_ids.append(repre["parent"]) - - versions = io.find({ - "type": {"$in": ["version", "hero_version"]}, - "_id": {"$in": list(set(version_ids))} - }) - content_versions = {} - hero_version_ids = set() - for version in versions: - content_versions[version["_id"]] = version - if version["type"] == "hero_version": - hero_version_ids.add(version["_id"]) - - missing_versions = [] - subset_ids = [] - for version_id in version_ids: - if version_id not in content_versions: - missing_versions.append(version_id) - else: - subset_ids.append(content_versions[version_id]["parent"]) - - subsets = io.find({ - "type": {"$in": ["subset", "archived_subset"]}, - "_id": {"$in": subset_ids} - }) - subsets_by_id = {sub["_id"]: sub for sub in subsets} - - asset_ids = [] - archived_subsets = [] - missing_subsets = [] - content_subsets = {} - for subset_id in subset_ids: - if subset_id not in subsets_by_id: - missing_subsets.append(subset_id) - elif subsets_by_id[subset_id]["type"] == "archived_subset": - subset = subsets_by_id[subset_id] - asset_ids.append(subset["parent"]) - archived_subsets.append(subset) - else: - subset = subsets_by_id[subset_id] - asset_ids.append(subset["parent"]) - content_subsets[subset_id] = subset - - assets = io.find({ - "type": {"$in": ["asset", "archived_asset"]}, - "_id": {"$in": list(asset_ids)} - }) - assets_by_id = {asset["_id"]: asset for asset in assets} - - missing_assets = [] - archived_assets = [] - content_assets = {} - for asset_id in asset_ids: - if asset_id not in assets_by_id: - missing_assets.append(asset_id) - elif assets_by_id[asset_id]["type"] == "archived_asset": - archived_assets.append(assets_by_id[asset_id]) - else: - content_assets[asset_id] = assets_by_id[asset_id] - - self.content_assets = content_assets - self.content_subsets = content_subsets - self.content_versions = content_versions - self.content_repres = content_repres - - self.hero_version_ids = hero_version_ids - - self.missing_assets = missing_assets - self.missing_versions = missing_versions - self.missing_subsets = missing_subsets - self.missing_repres = missing_repres - self.missing_docs = ( - bool(missing_assets) - or bool(missing_versions) - or bool(missing_subsets) - or bool(missing_repres) - ) - - self.archived_assets = archived_assets - self.archived_subsets = archived_subsets - self.archived_repres = archived_repres - - def _combobox_value_changed(self, *args, **kwargs): - self.refresh() - - def refresh(self, init_refresh=False): - """Build the need comboboxes with content""" - if not self.fill_check and not init_refresh: - return - - self.fill_check = False - - if init_refresh: - asset_values = self._get_asset_box_values() - self._fill_combobox(asset_values, "asset") - - validation_state = ValidationState() - - # Set other comboboxes to empty if any document is missing or any asset - # of loaded representations is archived. - self._is_asset_ok(validation_state) - if validation_state.asset_ok: - subset_values = self._get_subset_box_values() - self._fill_combobox(subset_values, "subset") - self._is_subset_ok(validation_state) - - if validation_state.asset_ok and validation_state.subset_ok: - repre_values = sorted(self._representations_box_values()) - self._fill_combobox(repre_values, "repre") - self._is_repre_ok(validation_state) - - # Fill comboboxes with values - self.set_labels() - self.apply_validations(validation_state) - - if init_refresh: # pre select context if possible - self._assets_box.set_valid_value(self._init_asset_name) - self._subsets_box.set_valid_value(self._init_subset_name) - self._representations_box.set_valid_value(self._init_repre_name) - - self.fill_check = True - - def _get_loaders(self, representations): - if not representations: - return list() - - available_loaders = filter( - lambda l: not (hasattr(l, "is_utility") and l.is_utility), - api.discover(api.Loader) - ) - - loaders = set() - - for representation in representations: - for loader in api.loaders_from_representation( - available_loaders, - representation - ): - loaders.add(loader) - - return loaders - - def _fill_combobox(self, values, combobox_type): - if combobox_type == "asset": - combobox_widget = self._assets_box - elif combobox_type == "subset": - combobox_widget = self._subsets_box - elif combobox_type == "repre": - combobox_widget = self._representations_box - else: - return - selected_value = combobox_widget.get_valid_value() - - # Fill combobox - if values is not None: - combobox_widget.populate(list(sorted(values))) - if selected_value and selected_value in values: - index = None - for idx in range(combobox_widget.count()): - if selected_value == str(combobox_widget.itemText(idx)): - index = idx - break - if index is not None: - combobox_widget.setCurrentIndex(index) - - def set_labels(self): - asset_label = self._assets_box.get_valid_value() - subset_label = self._subsets_box.get_valid_value() - repre_label = self._representations_box.get_valid_value() - - default = "*No changes" - self._asset_label.setText(asset_label or default) - self._subset_label.setText(subset_label or default) - self._repre_label.setText(repre_label or default) - - def apply_validations(self, validation_state): - error_msg = "*Please select" - error_sheet = "border: 1px solid red;" - success_sheet = "border: 1px solid green;" - - asset_sheet = None - subset_sheet = None - repre_sheet = None - accept_sheet = None - if validation_state.asset_ok is False: - asset_sheet = error_sheet - self._asset_label.setText(error_msg) - elif validation_state.subset_ok is False: - subset_sheet = error_sheet - self._subset_label.setText(error_msg) - elif validation_state.repre_ok is False: - repre_sheet = error_sheet - self._repre_label.setText(error_msg) - - if validation_state.all_ok: - accept_sheet = success_sheet - - self._assets_box.setStyleSheet(asset_sheet or "") - self._subsets_box.setStyleSheet(subset_sheet or "") - self._representations_box.setStyleSheet(repre_sheet or "") - - self._accept_btn.setEnabled(validation_state.all_ok) - self._accept_btn.setStyleSheet(accept_sheet or "") - - def _get_asset_box_values(self): - asset_docs = io.find( - {"type": "asset"}, - {"_id": 1, "name": 1} - ) - asset_names_by_id = { - asset_doc["_id"]: asset_doc["name"] - for asset_doc in asset_docs - } - subsets = io.find( - { - "type": "subset", - "parent": {"$in": list(asset_names_by_id.keys())} - }, - { - "parent": 1 - } - ) - - filtered_assets = [] - for subset in subsets: - asset_name = asset_names_by_id[subset["parent"]] - if asset_name not in filtered_assets: - filtered_assets.append(asset_name) - return sorted(filtered_assets) - - def _get_subset_box_values(self): - selected_asset = self._assets_box.get_valid_value() - if selected_asset: - asset_doc = io.find_one({"type": "asset", "name": selected_asset}) - asset_ids = [asset_doc["_id"]] - else: - asset_ids = list(self.content_assets.keys()) - - subsets = io.find( - { - "type": "subset", - "parent": {"$in": asset_ids} - }, - { - "parent": 1, - "name": 1 - } - ) - - subset_names_by_parent_id = collections.defaultdict(set) - for subset in subsets: - subset_names_by_parent_id[subset["parent"]].add(subset["name"]) - - possible_subsets = None - for subset_names in subset_names_by_parent_id.values(): - if possible_subsets is None: - possible_subsets = subset_names - else: - possible_subsets = (possible_subsets & subset_names) - - if not possible_subsets: - break - - return list(possible_subsets or list()) - - def _representations_box_values(self): - # NOTE hero versions are not used because it is expected that - # hero version has same representations as latests - selected_asset = self._assets_box.currentText() - selected_subset = self._subsets_box.currentText() - - # If nothing is selected - # [ ] [ ] [?] - if not selected_asset and not selected_subset: - # Find all representations of selection's subsets - possible_repres = list(io.find( - { - "type": "representation", - "parent": {"$in": list(self.content_versions.keys())} - }, - { - "parent": 1, - "name": 1 - } - )) - - possible_repres_by_parent = collections.defaultdict(set) - for repre in possible_repres: - possible_repres_by_parent[repre["parent"]].add(repre["name"]) - - output_repres = None - for repre_names in possible_repres_by_parent.values(): - if output_repres is None: - output_repres = repre_names - else: - output_repres = (output_repres & repre_names) - - if not output_repres: - break - - return list(output_repres or list()) - - # [x] [x] [?] - if selected_asset and selected_subset: - asset_doc = io.find_one( - {"type": "asset", "name": selected_asset}, - {"_id": 1} - ) - subset_doc = io.find_one( - { - "type": "subset", - "name": selected_subset, - "parent": asset_doc["_id"] - }, - {"_id": 1} - ) - subset_id = subset_doc["_id"] - last_versions_by_subset_id = self.find_last_versions([subset_id]) - version_doc = last_versions_by_subset_id.get(subset_id) - repre_docs = io.find( - { - "type": "representation", - "parent": version_doc["_id"] - }, - { - "name": 1 - } - ) - return [ - repre_doc["name"] - for repre_doc in repre_docs - ] - - # [x] [ ] [?] - # If asset only is selected - if selected_asset: - asset_doc = io.find_one( - {"type": "asset", "name": selected_asset}, - {"_id": 1} - ) - if not asset_doc: - return list() - - # Filter subsets by subset names from content - subset_names = set() - for subset_doc in self.content_subsets.values(): - subset_names.add(subset_doc["name"]) - subset_docs = io.find( - { - "type": "subset", - "parent": asset_doc["_id"], - "name": {"$in": list(subset_names)} - }, - {"_id": 1} - ) - subset_ids = [ - subset_doc["_id"] - for subset_doc in subset_docs - ] - if not subset_ids: - return list() - - last_versions_by_subset_id = self.find_last_versions(subset_ids) - subset_id_by_version_id = {} - for subset_id, last_version in last_versions_by_subset_id.items(): - version_id = last_version["_id"] - subset_id_by_version_id[version_id] = subset_id - - if not subset_id_by_version_id: - return list() - - repre_docs = list(io.find( - { - "type": "representation", - "parent": {"$in": list(subset_id_by_version_id.keys())} - }, - { - "name": 1, - "parent": 1 - } - )) - if not repre_docs: - return list() - - repre_names_by_parent = collections.defaultdict(set) - for repre_doc in repre_docs: - repre_names_by_parent[repre_doc["parent"]].add( - repre_doc["name"] - ) - - available_repres = None - for repre_names in repre_names_by_parent.values(): - if available_repres is None: - available_repres = repre_names - continue - - available_repres = available_repres.intersection(repre_names) - - return list(available_repres) - - # [ ] [x] [?] - subset_docs = list(io.find( - { - "type": "subset", - "parent": {"$in": list(self.content_assets.keys())}, - "name": selected_subset - }, - {"_id": 1, "parent": 1} - )) - if not subset_docs: - return list() - - subset_docs_by_id = { - subset_doc["_id"]: subset_doc - for subset_doc in subset_docs - } - last_versions_by_subset_id = self.find_last_versions( - subset_docs_by_id.keys() - ) - - subset_id_by_version_id = {} - for subset_id, last_version in last_versions_by_subset_id.items(): - version_id = last_version["_id"] - subset_id_by_version_id[version_id] = subset_id - - if not subset_id_by_version_id: - return list() - - repre_docs = list(io.find( - { - "type": "representation", - "parent": {"$in": list(subset_id_by_version_id.keys())} - }, - { - "name": 1, - "parent": 1 - } - )) - if not repre_docs: - return list() - - repre_names_by_asset_id = {} - for repre_doc in repre_docs: - subset_id = subset_id_by_version_id[repre_doc["parent"]] - asset_id = subset_docs_by_id[subset_id]["parent"] - if asset_id not in repre_names_by_asset_id: - repre_names_by_asset_id[asset_id] = set() - repre_names_by_asset_id[asset_id].add(repre_doc["name"]) - - available_repres = None - for repre_names in repre_names_by_asset_id.values(): - if available_repres is None: - available_repres = repre_names - continue - - available_repres = available_repres.intersection(repre_names) - - return list(available_repres) - - def _is_asset_ok(self, validation_state): - selected_asset = self._assets_box.get_valid_value() - if ( - selected_asset is None - and (self.missing_docs or self.archived_assets) - ): - validation_state.asset_ok = False - - def _is_subset_ok(self, validation_state): - selected_asset = self._assets_box.get_valid_value() - selected_subset = self._subsets_box.get_valid_value() - - # [?] [x] [?] - # If subset is selected then must be ok - if selected_subset is not None: - return - - # [ ] [ ] [?] - if selected_asset is None: - # If there were archived subsets and asset is not selected - if self.archived_subsets: - validation_state.subset_ok = False - return - - # [x] [ ] [?] - asset_doc = io.find_one( - {"type": "asset", "name": selected_asset}, - {"_id": 1} - ) - subset_docs = io.find( - {"type": "subset", "parent": asset_doc["_id"]}, - {"name": 1} - ) - subset_names = set( - subset_doc["name"] - for subset_doc in subset_docs - ) - - for subset_doc in self.content_subsets.values(): - if subset_doc["name"] not in subset_names: - validation_state.subset_ok = False - break - - def find_last_versions(self, subset_ids): - _pipeline = [ - # Find all versions of those subsets - {"$match": { - "type": "version", - "parent": {"$in": list(subset_ids)} - }}, - # Sorting versions all together - {"$sort": {"name": 1}}, - # Group them by "parent", but only take the last - {"$group": { - "_id": "$parent", - "_version_id": {"$last": "$_id"}, - "type": {"$last": "$type"} - }} - ] - last_versions_by_subset_id = dict() - for doc in io.aggregate(_pipeline): - doc["parent"] = doc["_id"] - doc["_id"] = doc.pop("_version_id") - last_versions_by_subset_id[doc["parent"]] = doc - return last_versions_by_subset_id - - def _is_repre_ok(self, validation_state): - selected_asset = self._assets_box.get_valid_value() - selected_subset = self._subsets_box.get_valid_value() - selected_repre = self._representations_box.get_valid_value() - - # [?] [?] [x] - # If subset is selected then must be ok - if selected_repre is not None: - return - - # [ ] [ ] [ ] - if selected_asset is None and selected_subset is None: - if ( - self.archived_repres - or self.missing_versions - or self.missing_repres - ): - validation_state.repre_ok = False - return - - # [x] [x] [ ] - if selected_asset is not None and selected_subset is not None: - asset_doc = io.find_one( - {"type": "asset", "name": selected_asset}, - {"_id": 1} - ) - subset_doc = io.find_one( - { - "type": "subset", - "parent": asset_doc["_id"], - "name": selected_subset - }, - {"_id": 1} - ) - last_versions_by_subset_id = self.find_last_versions( - [subset_doc["_id"]] - ) - last_version = last_versions_by_subset_id.get(subset_doc["_id"]) - if not last_version: - validation_state.repre_ok = False - return - - repre_docs = io.find( - { - "type": "representation", - "parent": last_version["_id"] - }, - {"name": 1} - ) - - repre_names = set( - repre_doc["name"] - for repre_doc in repre_docs - ) - for repre_doc in self.content_repres.values(): - if repre_doc["name"] not in repre_names: - validation_state.repre_ok = False - break - return - - # [x] [ ] [ ] - if selected_asset is not None: - asset_doc = io.find_one( - {"type": "asset", "name": selected_asset}, - {"_id": 1} - ) - subset_docs = list(io.find( - { - "type": "subset", - "parent": asset_doc["_id"] - }, - {"_id": 1, "name": 1} - )) - - subset_name_by_id = {} - subset_ids = set() - for subset_doc in subset_docs: - subset_id = subset_doc["_id"] - subset_ids.add(subset_id) - subset_name_by_id[subset_id] = subset_doc["name"] - - last_versions_by_subset_id = self.find_last_versions(subset_ids) - - subset_id_by_version_id = {} - for subset_id, last_version in last_versions_by_subset_id.items(): - version_id = last_version["_id"] - subset_id_by_version_id[version_id] = subset_id - - repre_docs = io.find( - { - "type": "representation", - "parent": {"$in": list(subset_id_by_version_id.keys())} - }, - { - "name": 1, - "parent": 1 - } - ) - repres_by_subset_name = {} - for repre_doc in repre_docs: - subset_id = subset_id_by_version_id[repre_doc["parent"]] - subset_name = subset_name_by_id[subset_id] - if subset_name not in repres_by_subset_name: - repres_by_subset_name[subset_name] = set() - repres_by_subset_name[subset_name].add(repre_doc["name"]) - - for repre_doc in self.content_repres.values(): - version_doc = self.content_versions[repre_doc["parent"]] - subset_doc = self.content_subsets[version_doc["parent"]] - repre_names = ( - repres_by_subset_name.get(subset_doc["name"]) or [] - ) - if repre_doc["name"] not in repre_names: - validation_state.repre_ok = False - break - return - - # [ ] [x] [ ] - # Subset documents - subset_docs = io.find( - { - "type": "subset", - "parent": {"$in": list(self.content_assets.keys())}, - "name": selected_subset - }, - {"_id": 1, "name": 1, "parent": 1} - ) - - subset_docs_by_id = {} - for subset_doc in subset_docs: - subset_docs_by_id[subset_doc["_id"]] = subset_doc - - last_versions_by_subset_id = self.find_last_versions( - subset_docs_by_id.keys() - ) - subset_id_by_version_id = {} - for subset_id, last_version in last_versions_by_subset_id.items(): - version_id = last_version["_id"] - subset_id_by_version_id[version_id] = subset_id - - repre_docs = io.find( - { - "type": "representation", - "parent": {"$in": list(subset_id_by_version_id.keys())} - }, - { - "name": 1, - "parent": 1 - } - ) - repres_by_asset_id = {} - for repre_doc in repre_docs: - subset_id = subset_id_by_version_id[repre_doc["parent"]] - asset_id = subset_docs_by_id[subset_id]["parent"] - if asset_id not in repres_by_asset_id: - repres_by_asset_id[asset_id] = set() - repres_by_asset_id[asset_id].add(repre_doc["name"]) - - for repre_doc in self.content_repres.values(): - version_doc = self.content_versions[repre_doc["parent"]] - subset_doc = self.content_subsets[version_doc["parent"]] - asset_id = subset_doc["parent"] - repre_names = ( - repres_by_asset_id.get(asset_id) or [] - ) - if repre_doc["name"] not in repre_names: - validation_state.repre_ok = False - break - - def _on_current_asset(self): - # Set initial asset as current. - asset_name = api.Session["AVALON_ASSET"] - index = self._assets_box.findText( - asset_name, QtCore.Qt.MatchFixedString - ) - if index >= 0: - print("Setting asset to {}".format(asset_name)) - self._assets_box.setCurrentIndex(index) - - def _on_accept(self): - # Use None when not a valid value or when placeholder value - selected_asset = self._assets_box.get_valid_value() - selected_subset = self._subsets_box.get_valid_value() - selected_representation = self._representations_box.get_valid_value() - - if selected_asset: - asset_doc = io.find_one({"type": "asset", "name": selected_asset}) - asset_docs_by_id = {asset_doc["_id"]: asset_doc} - else: - asset_docs_by_id = self.content_assets - - asset_docs_by_name = { - asset_doc["name"]: asset_doc - for asset_doc in asset_docs_by_id.values() - } - - asset_ids = list(asset_docs_by_id.keys()) - - subset_query = { - "type": "subset", - "parent": {"$in": asset_ids} - } - if selected_subset: - subset_query["name"] = selected_subset - - subset_docs = list(io.find(subset_query)) - subset_ids = [] - subset_docs_by_parent_and_name = collections.defaultdict(dict) - for subset in subset_docs: - subset_ids.append(subset["_id"]) - parent_id = subset["parent"] - name = subset["name"] - subset_docs_by_parent_and_name[parent_id][name] = subset - - # versions - version_docs = list(io.find({ - "type": "version", - "parent": {"$in": subset_ids} - }, sort=[("name", -1)])) - - hero_version_docs = list(io.find({ - "type": "hero_version", - "parent": {"$in": subset_ids} - })) - - version_ids = list() - - version_docs_by_parent_id = {} - for version_doc in version_docs: - parent_id = version_doc["parent"] - if parent_id not in version_docs_by_parent_id: - version_ids.append(version_doc["_id"]) - version_docs_by_parent_id[parent_id] = version_doc - - hero_version_docs_by_parent_id = {} - for hero_version_doc in hero_version_docs: - version_ids.append(hero_version_doc["_id"]) - parent_id = hero_version_doc["parent"] - hero_version_docs_by_parent_id[parent_id] = hero_version_doc - - repre_docs = io.find({ - "type": "representation", - "parent": {"$in": version_ids} - }) - repre_docs_by_parent_id_by_name = collections.defaultdict(dict) - for repre_doc in repre_docs: - parent_id = repre_doc["parent"] - name = repre_doc["name"] - repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc - - for container in self._items: - container_repre_id = io.ObjectId(container["representation"]) - container_repre = self.content_repres[container_repre_id] - container_repre_name = container_repre["name"] - - container_version_id = container_repre["parent"] - container_version = self.content_versions[container_version_id] - - container_subset_id = container_version["parent"] - container_subset = self.content_subsets[container_subset_id] - container_subset_name = container_subset["name"] - - container_asset_id = container_subset["parent"] - container_asset = self.content_assets[container_asset_id] - container_asset_name = container_asset["name"] - - if selected_asset: - asset_doc = asset_docs_by_name[selected_asset] - else: - asset_doc = asset_docs_by_name[container_asset_name] - - subsets_by_name = subset_docs_by_parent_and_name[asset_doc["_id"]] - if selected_subset: - subset_doc = subsets_by_name[selected_subset] - else: - subset_doc = subsets_by_name[container_subset_name] - - repre_doc = None - subset_id = subset_doc["_id"] - if container_version["type"] == "hero_version": - hero_version = hero_version_docs_by_parent_id.get( - subset_id - ) - if hero_version: - _repres = repre_docs_by_parent_id_by_name.get( - hero_version["_id"] - ) - if selected_representation: - repre_doc = _repres.get(selected_representation) - else: - repre_doc = _repres.get(container_repre_name) - - if not repre_doc: - version_doc = version_docs_by_parent_id[subset_id] - version_id = version_doc["_id"] - repres_by_name = repre_docs_by_parent_id_by_name[version_id] - if selected_representation: - repre_doc = repres_by_name[selected_representation] - else: - repre_doc = repres_by_name[container_repre_name] - - try: - api.switch(container, repre_doc) - except Exception: - log.warning( - ( - "Couldn't switch asset." - "See traceback for more information." - ), - exc_info=True - ) - dialog = QtWidgets.QMessageBox() - dialog.setStyleSheet(style.load_stylesheet()) - dialog.setWindowTitle("Switch asset failed") - msg = "Switch asset failed. "\ - "Search console log for more details" - dialog.setText(msg) - dialog.exec_() - - self.switched.emit() - - self.close() - - class SceneInventoryWindow(QtWidgets.QDialog): """Scene Inventory window""" From 87cdaa829c33550e995ac10f5ebf42153adf2a39 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 19:11:11 +0100 Subject: [PATCH 30/60] moved View to separated file --- openpype/tools/sceneinventory/view.py | 774 +++++++++++++++++++++++ openpype/tools/sceneinventory/window.py | 776 +----------------------- 2 files changed, 781 insertions(+), 769 deletions(-) create mode 100644 openpype/tools/sceneinventory/view.py diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py new file mode 100644 index 0000000000..512c65e143 --- /dev/null +++ b/openpype/tools/sceneinventory/view.py @@ -0,0 +1,774 @@ +import collections +import logging +from functools import partial + +from Qt import QtWidgets, QtCore + +from avalon import io, api, style +from avalon.vendor import qtawesome +from avalon.lib import HeroVersionType +from avalon.tools import lib as tools_lib + +from openpype.modules import ModulesManager + +from .switch_dialog import SwitchAssetDialog +from .model import InventoryModel + + +DEFAULT_COLOR = "#fb9c15" + +log = logging.getLogger("SceneInventory") + + +class View(QtWidgets.QTreeView): + data_changed = QtCore.Signal() + hierarchy_view = QtCore.Signal(bool) + + def __init__(self, parent=None): + super(View, self).__init__(parent=parent) + + if not parent: + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint + ) + # view settings + self.setIndentation(12) + self.setAlternatingRowColors(True) + self.setSortingEnabled(True) + self.setSelectionMode(self.ExtendedSelection) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_right_mouse_menu) + self._hierarchy_view = False + self._selected = None + + manager = ModulesManager() + self.sync_server = manager.modules_by_name["sync_server"] + self.sync_enabled = self.sync_server.enabled + + def enter_hierarchy(self, items): + self._selected = set(i["objectName"] for i in items) + self._hierarchy_view = True + self.hierarchy_view.emit(True) + self.data_changed.emit() + self.expandToDepth(1) + self.setStyleSheet(""" + QTreeView { + border-color: #fb9c15; + } + """) + + def leave_hierarchy(self): + self._hierarchy_view = False + self.hierarchy_view.emit(False) + self.data_changed.emit() + self.setStyleSheet("QTreeView {}") + + def build_item_menu_for_selection(self, items, menu): + if not items: + return + + repre_ids = [] + for item in items: + item_id = io.ObjectId(item["representation"]) + if item_id not in repre_ids: + repre_ids.append(item_id) + + repre_docs = io.find( + { + "type": "representation", + "_id": {"$in": repre_ids} + }, + {"parent": 1} + ) + + version_ids = [] + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + if version_id not in version_ids: + version_ids.append(version_id) + + loaded_versions = io.find({ + "_id": {"$in": version_ids}, + "type": {"$in": ["version", "hero_version"]} + }) + + loaded_hero_versions = [] + versions_by_parent_id = collections.defaultdict(list) + version_parents = [] + for version in loaded_versions: + if version["type"] == "hero_version": + loaded_hero_versions.append(version) + else: + parent_id = version["parent"] + versions_by_parent_id[parent_id].append(version) + if parent_id not in version_parents: + version_parents.append(parent_id) + + all_versions = io.find({ + "type": {"$in": ["hero_version", "version"]}, + "parent": {"$in": version_parents} + }) + hero_versions = [] + versions = [] + for version in all_versions: + if version["type"] == "hero_version": + hero_versions.append(version) + else: + versions.append(version) + + has_loaded_hero_versions = len(loaded_hero_versions) > 0 + has_available_hero_version = len(hero_versions) > 0 + has_outdated = False + + for version in versions: + parent_id = version["parent"] + current_versions = versions_by_parent_id[parent_id] + for current_version in current_versions: + if current_version["name"] < version["name"]: + has_outdated = True + break + + if has_outdated: + break + + switch_to_versioned = None + if has_loaded_hero_versions: + def _on_switch_to_versioned(items): + repre_ids = [] + for item in items: + item_id = io.ObjectId(item["representation"]) + if item_id not in repre_ids: + repre_ids.append(item_id) + + repre_docs = io.find( + { + "type": "representation", + "_id": {"$in": repre_ids} + }, + {"parent": 1} + ) + + version_ids = [] + version_id_by_repre_id = {} + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_id_by_repre_id[repre_doc["_id"]] = version_id + if version_id not in version_ids: + version_ids.append(version_id) + hero_versions = io.find( + { + "_id": {"$in": version_ids}, + "type": "hero_version" + }, + {"version_id": 1} + ) + version_ids = set() + for hero_version in hero_versions: + version_id = hero_version["version_id"] + version_ids.add(version_id) + hero_version_id = hero_version["_id"] + for _repre_id, current_version_id in ( + version_id_by_repre_id.items() + ): + if current_version_id == hero_version_id: + version_id_by_repre_id[_repre_id] = version_id + + version_docs = io.find( + { + "_id": {"$in": list(version_ids)}, + "type": "version" + }, + {"name": 1} + ) + version_name_by_id = {} + for version_doc in version_docs: + version_name_by_id[version_doc["_id"]] = \ + version_doc["name"] + + for item in items: + repre_id = io.ObjectId(item["representation"]) + version_id = version_id_by_repre_id.get(repre_id) + version_name = version_name_by_id.get(version_id) + if version_name is not None: + try: + api.update(item, version_name) + except AssertionError: + self._show_version_error_dialog(version_name, + [item]) + log.warning("Update failed", exc_info=True) + + self.data_changed.emit() + + update_icon = qtawesome.icon( + "fa.asterisk", + color=DEFAULT_COLOR + ) + switch_to_versioned = QtWidgets.QAction( + update_icon, + "Switch to versioned", + menu + ) + switch_to_versioned.triggered.connect( + lambda: _on_switch_to_versioned(items) + ) + + update_to_latest_action = None + if has_outdated or has_loaded_hero_versions: + # update to latest version + def _on_update_to_latest(items): + for item in items: + try: + api.update(item, -1) + except AssertionError: + self._show_version_error_dialog(None, [item]) + log.warning("Update failed", exc_info=True) + self.data_changed.emit() + + update_icon = qtawesome.icon( + "fa.angle-double-up", + color=DEFAULT_COLOR + ) + update_to_latest_action = QtWidgets.QAction( + update_icon, + "Update to latest", + menu + ) + update_to_latest_action.triggered.connect( + lambda: _on_update_to_latest(items) + ) + + change_to_hero = None + if has_available_hero_version: + # change to hero version + def _on_update_to_hero(items): + for item in items: + try: + api.update(item, HeroVersionType(-1)) + except AssertionError: + self._show_version_error_dialog('hero', [item]) + log.warning("Update failed", exc_info=True) + self.data_changed.emit() + + # TODO change icon + change_icon = qtawesome.icon( + "fa.asterisk", + color="#00b359" + ) + change_to_hero = QtWidgets.QAction( + change_icon, + "Change to hero", + menu + ) + change_to_hero.triggered.connect( + lambda: _on_update_to_hero(items) + ) + + # set version + set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) + set_version_action = QtWidgets.QAction( + set_version_icon, + "Set version", + menu + ) + set_version_action.triggered.connect( + lambda: self.show_version_dialog(items)) + + # switch asset + switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) + switch_asset_action = QtWidgets.QAction( + switch_asset_icon, + "Switch Asset", + menu + ) + switch_asset_action.triggered.connect( + lambda: self.show_switch_dialog(items)) + + # remove + remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) + remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) + remove_action.triggered.connect( + lambda: self.show_remove_warning_dialog(items)) + + # add the actions + if switch_to_versioned: + menu.addAction(switch_to_versioned) + + if update_to_latest_action: + menu.addAction(update_to_latest_action) + + if change_to_hero: + menu.addAction(change_to_hero) + + menu.addAction(set_version_action) + menu.addAction(switch_asset_action) + + menu.addSeparator() + + menu.addAction(remove_action) + + menu.addSeparator() + + if self.sync_enabled: + menu = self.handle_sync_server(menu, repre_ids) + + def handle_sync_server(self, menu, repre_ids): + """ + Adds actions for download/upload when SyncServer is enabled + + Args: + menu (OptionMenu) + repre_ids (list) of object_ids + Returns: + (OptionMenu) + """ + download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR) + download_active_action = QtWidgets.QAction( + download_icon, + "Download", + menu + ) + download_active_action.triggered.connect( + lambda: self._add_sites(repre_ids, 'active_site')) + + upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR) + upload_remote_action = QtWidgets.QAction( + upload_icon, + "Upload", + menu + ) + upload_remote_action.triggered.connect( + lambda: self._add_sites(repre_ids, 'remote_site')) + + menu.addAction(download_active_action) + menu.addAction(upload_remote_action) + + return menu + + def _add_sites(self, repre_ids, side): + """ + (Re)sync all 'repre_ids' to specific site. + + It checks if opposite site has fully available content to limit + accidents. (ReSync active when no remote >> losing active content) + + Args: + repre_ids (list) + side (str): 'active_site'|'remote_site' + """ + project = io.Session["AVALON_PROJECT"] + active_site = self.sync_server.get_active_site(project) + remote_site = self.sync_server.get_remote_site(project) + + for repre_id in repre_ids: + representation = io.find_one({"type": "representation", + "_id": repre_id}) + if not representation: + continue + + progress = tools_lib.get_progress_for_repre(representation, + active_site, + remote_site) + if side == 'active_site': + # check opposite from added site, must be 1 or unable to sync + check_progress = progress[remote_site] + site = active_site + else: + check_progress = progress[active_site] + site = remote_site + + if check_progress == 1: + self.sync_server.add_site(project, repre_id, site, force=True) + + self.data_changed.emit() + + def build_item_menu(self, items): + """Create menu for the selected items""" + + menu = QtWidgets.QMenu(self) + + # add the actions + self.build_item_menu_for_selection(items, menu) + + # These two actions should be able to work without selection + # expand all items + expandall_action = QtWidgets.QAction(menu, text="Expand all items") + expandall_action.triggered.connect(self.expandAll) + + # collapse all items + collapse_action = QtWidgets.QAction(menu, text="Collapse all items") + collapse_action.triggered.connect(self.collapseAll) + + menu.addAction(expandall_action) + menu.addAction(collapse_action) + + custom_actions = self.get_custom_actions(containers=items) + if custom_actions: + submenu = QtWidgets.QMenu("Actions", self) + for action in custom_actions: + + color = action.color or DEFAULT_COLOR + icon = qtawesome.icon("fa.%s" % action.icon, color=color) + action_item = QtWidgets.QAction(icon, action.label, submenu) + action_item.triggered.connect( + partial(self.process_custom_action, action, items)) + + submenu.addAction(action_item) + + menu.addMenu(submenu) + + # go back to flat view + if self._hierarchy_view: + back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR) + back_to_flat_action = QtWidgets.QAction( + back_to_flat_icon, + "Back to Full-View", + menu + ) + back_to_flat_action.triggered.connect(self.leave_hierarchy) + + # send items to hierarchy view + enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8") + enter_hierarchy_action = QtWidgets.QAction( + enter_hierarchy_icon, + "Cherry-Pick (Hierarchy)", + menu + ) + enter_hierarchy_action.triggered.connect( + lambda: self.enter_hierarchy(items)) + + if items: + menu.addAction(enter_hierarchy_action) + + if self._hierarchy_view: + menu.addAction(back_to_flat_action) + + return menu + + def get_custom_actions(self, containers): + """Get the registered Inventory Actions + + Args: + containers(list): collection of containers + + Returns: + list: collection of filter and initialized actions + """ + + def sorter(Plugin): + """Sort based on order attribute of the plugin""" + return Plugin.order + + # Fedd an empty dict if no selection, this will ensure the compat + # lookup always work, so plugin can interact with Scene Inventory + # reversely. + containers = containers or [dict()] + + # Check which action will be available in the menu + Plugins = api.discover(api.InventoryAction) + compatible = [p() for p in Plugins if + any(p.is_compatible(c) for c in containers)] + + return sorted(compatible, key=sorter) + + def process_custom_action(self, action, containers): + """Run action and if results are returned positive update the view + + If the result is list or dict, will select view items by the result. + + Args: + action (InventoryAction): Inventory Action instance + containers (list): Data of currently selected items + + Returns: + None + """ + + result = action.process(containers) + if result: + self.data_changed.emit() + + if isinstance(result, (list, set)): + self.select_items_by_action(result) + + if isinstance(result, dict): + self.select_items_by_action(result["objectNames"], + result["options"]) + + def select_items_by_action(self, object_names, options=None): + """Select view items by the result of action + + Args: + object_names (list or set): A list/set of container object name + options (dict): GUI operation options. + + Returns: + None + + """ + options = options or dict() + + if options.get("clear", True): + self.clearSelection() + + object_names = set(object_names) + if (self._hierarchy_view and + not self._selected.issuperset(object_names)): + # If any container not in current cherry-picked view, update + # view before selecting them. + self._selected.update(object_names) + self.data_changed.emit() + + model = self.model() + selection_model = self.selectionModel() + + select_mode = { + "select": selection_model.Select, + "deselect": selection_model.Deselect, + "toggle": selection_model.Toggle, + }[options.get("mode", "select")] + + for item in tools_lib.iter_model_rows(model, 0): + item = item.data(InventoryModel.ItemRole) + if item.get("isGroupNode"): + continue + + name = item.get("objectName") + if name in object_names: + self.scrollTo(item) # Ensure item is visible + flags = select_mode | selection_model.Rows + selection_model.select(item, flags) + + object_names.remove(name) + + if len(object_names) == 0: + break + + def show_right_mouse_menu(self, pos): + """Display the menu when at the position of the item clicked""" + + globalpos = self.viewport().mapToGlobal(pos) + + if not self.selectionModel().hasSelection(): + print("No selection") + # Build menu without selection, feed an empty list + menu = self.build_item_menu([]) + menu.exec_(globalpos) + return + + active = self.currentIndex() # index under mouse + active = active.sibling(active.row(), 0) # get first column + + # move index under mouse + indices = self.get_indices() + if active in indices: + indices.remove(active) + + indices.append(active) + + # Extend to the sub-items + all_indices = self.extend_to_children(indices) + items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices + if i.parent().isValid()] + + if self._hierarchy_view: + # Ensure no group item + items = [n for n in items if not n.get("isGroupNode")] + + menu = self.build_item_menu(items) + menu.exec_(globalpos) + + def get_indices(self): + """Get the selected rows""" + selection_model = self.selectionModel() + return selection_model.selectedRows() + + def extend_to_children(self, indices): + """Extend the indices to the children indices. + + Top-level indices are extended to its children indices. Sub-items + are kept as is. + + Args: + indices (list): The indices to extend. + + Returns: + list: The children indices + + """ + def get_children(i): + model = i.model() + rows = model.rowCount(parent=i) + for row in range(rows): + child = model.index(row, 0, parent=i) + yield child + + subitems = set() + for i in indices: + valid_parent = i.parent().isValid() + if valid_parent and i not in subitems: + subitems.add(i) + + if self._hierarchy_view: + # Assume this is a group item + for child in get_children(i): + subitems.add(child) + else: + # is top level item + for child in get_children(i): + subitems.add(child) + + return list(subitems) + + def show_version_dialog(self, items): + """Create a dialog with the available versions for the selected file + + Args: + items (list): list of items to run the "set_version" for + + Returns: + None + """ + + active = items[-1] + + # Get available versions for active representation + representation_id = io.ObjectId(active["representation"]) + representation = io.find_one({"_id": representation_id}) + version = io.find_one({ + "_id": representation["parent"] + }) + + versions = list(io.find( + { + "parent": version["parent"], + "type": "version" + }, + sort=[("name", 1)] + )) + + hero_version = io.find_one({ + "parent": version["parent"], + "type": "hero_version" + }) + if hero_version: + _version_id = hero_version["version_id"] + for _version in versions: + if _version["_id"] != _version_id: + continue + + hero_version["name"] = HeroVersionType( + _version["name"] + ) + hero_version["data"] = _version["data"] + break + + # Get index among the listed versions + current_item = None + current_version = active["version"] + if isinstance(current_version, HeroVersionType): + current_item = hero_version + else: + for version in versions: + if version["name"] == current_version: + current_item = version + break + + all_versions = [] + if hero_version: + all_versions.append(hero_version) + all_versions.extend(reversed(versions)) + + if current_item: + index = all_versions.index(current_item) + else: + index = 0 + + versions_by_label = dict() + labels = [] + for version in all_versions: + is_hero = version["type"] == "hero_version" + label = tools_lib.format_version(version["name"], is_hero) + labels.append(label) + versions_by_label[label] = version["name"] + + label, state = QtWidgets.QInputDialog.getItem( + self, + "Set version..", + "Set version number to", + labels, + current=index, + editable=False + ) + if not state: + return + + if label: + version = versions_by_label[label] + for item in items: + try: + api.update(item, version) + except AssertionError: + self._show_version_error_dialog(version, [item]) + log.warning("Update failed", exc_info=True) + # refresh model when done + self.data_changed.emit() + + def show_switch_dialog(self, items): + """Display Switch dialog""" + dialog = SwitchAssetDialog(self, items) + dialog.switched.connect(self.data_changed.emit) + dialog.show() + + def show_remove_warning_dialog(self, items): + """Prompt a dialog to inform the user the action will remove items""" + + accept = QtWidgets.QMessageBox.Ok + buttons = accept | QtWidgets.QMessageBox.Cancel + + message = ("Are you sure you want to remove " + "{} item(s)".format(len(items))) + state = QtWidgets.QMessageBox.question(self, "Are you sure?", + message, + buttons=buttons, + defaultButton=accept) + + if state != accept: + return + + for item in items: + api.remove(item) + self.data_changed.emit() + + def _show_version_error_dialog(self, version, items): + """Shows QMessageBox when version switch doesn't work + + Args: + version: str or int or None + """ + if not version: + version_str = "latest" + elif version == "hero": + version_str = "hero" + elif isinstance(version, int): + version_str = "v{:03d}".format(version) + else: + version_str = version + + dialog = QtWidgets.QMessageBox() + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle("Update failed") + + switch_btn = dialog.addButton("Switch Asset", + QtWidgets.QMessageBox.ActionRole) + switch_btn.clicked.connect(lambda: self.show_switch_dialog(items)) + + dialog.addButton(QtWidgets.QMessageBox.Cancel) + + msg = "Version update to '{}' ".format(version_str) + \ + "failed as representation doesn't exist.\n\n" \ + "Please update to version with a valid " \ + "representation OR \n use 'Switch Asset' button " \ + "to change asset." + dialog.setText(msg) + dialog.exec_() diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 1bd96ef85e..e0bbedf297 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -1,784 +1,22 @@ import os import sys -import logging -import collections -from functools import partial -from ...vendor.Qt import QtWidgets, QtCore -from ...vendor import qtawesome -from ... import io, api, style -from ...lib import HeroVersionType +from Qt import QtWidgets, QtCore +from avalon.vendor import qtawesome +from avalon import io, api, style -from .. import lib as tools_lib -from ..delegates import VersionDelegate + +from avalon.tools import lib as tools_lib +from avalon.tools.delegates import VersionDelegate from .proxy import FilterProxyModel from .model import InventoryModel -from .switch_dialog import SwitchAssetDialog +from .view import View -from openpype.modules import ModulesManager - -DEFAULT_COLOR = "#fb9c15" module = sys.modules[__name__] module.window = None -log = logging.getLogger("SceneInventory") - - -class View(QtWidgets.QTreeView): - data_changed = QtCore.Signal() - hierarchy_view = QtCore.Signal(bool) - - def __init__(self, parent=None): - super(View, self).__init__(parent=parent) - - if not parent: - self.setWindowFlags( - self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint - ) - # view settings - self.setIndentation(12) - self.setAlternatingRowColors(True) - self.setSortingEnabled(True) - self.setSelectionMode(self.ExtendedSelection) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.show_right_mouse_menu) - self._hierarchy_view = False - self._selected = None - - manager = ModulesManager() - self.sync_server = manager.modules_by_name["sync_server"] - self.sync_enabled = self.sync_server.enabled - - def enter_hierarchy(self, items): - self._selected = set(i["objectName"] for i in items) - self._hierarchy_view = True - self.hierarchy_view.emit(True) - self.data_changed.emit() - self.expandToDepth(1) - self.setStyleSheet(""" - QTreeView { - border-color: #fb9c15; - } - """) - - def leave_hierarchy(self): - self._hierarchy_view = False - self.hierarchy_view.emit(False) - self.data_changed.emit() - self.setStyleSheet("QTreeView {}") - - def build_item_menu_for_selection(self, items, menu): - if not items: - return - - repre_ids = [] - for item in items: - item_id = io.ObjectId(item["representation"]) - if item_id not in repre_ids: - repre_ids.append(item_id) - - repre_docs = io.find( - { - "type": "representation", - "_id": {"$in": repre_ids} - }, - {"parent": 1} - ) - - version_ids = [] - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - if version_id not in version_ids: - version_ids.append(version_id) - - loaded_versions = io.find({ - "_id": {"$in": version_ids}, - "type": {"$in": ["version", "hero_version"]} - }) - - loaded_hero_versions = [] - versions_by_parent_id = collections.defaultdict(list) - version_parents = [] - for version in loaded_versions: - if version["type"] == "hero_version": - loaded_hero_versions.append(version) - else: - parent_id = version["parent"] - versions_by_parent_id[parent_id].append(version) - if parent_id not in version_parents: - version_parents.append(parent_id) - - all_versions = io.find({ - "type": {"$in": ["hero_version", "version"]}, - "parent": {"$in": version_parents} - }) - hero_versions = [] - versions = [] - for version in all_versions: - if version["type"] == "hero_version": - hero_versions.append(version) - else: - versions.append(version) - - has_loaded_hero_versions = len(loaded_hero_versions) > 0 - has_available_hero_version = len(hero_versions) > 0 - has_outdated = False - - for version in versions: - parent_id = version["parent"] - current_versions = versions_by_parent_id[parent_id] - for current_version in current_versions: - if current_version["name"] < version["name"]: - has_outdated = True - break - - if has_outdated: - break - - switch_to_versioned = None - if has_loaded_hero_versions: - def _on_switch_to_versioned(items): - repre_ids = [] - for item in items: - item_id = io.ObjectId(item["representation"]) - if item_id not in repre_ids: - repre_ids.append(item_id) - - repre_docs = io.find( - { - "type": "representation", - "_id": {"$in": repre_ids} - }, - {"parent": 1} - ) - - version_ids = [] - version_id_by_repre_id = {} - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - version_id_by_repre_id[repre_doc["_id"]] = version_id - if version_id not in version_ids: - version_ids.append(version_id) - hero_versions = io.find( - { - "_id": {"$in": version_ids}, - "type": "hero_version" - }, - {"version_id": 1} - ) - version_ids = set() - for hero_version in hero_versions: - version_id = hero_version["version_id"] - version_ids.add(version_id) - hero_version_id = hero_version["_id"] - for _repre_id, current_version_id in ( - version_id_by_repre_id.items() - ): - if current_version_id == hero_version_id: - version_id_by_repre_id[_repre_id] = version_id - - version_docs = io.find( - { - "_id": {"$in": list(version_ids)}, - "type": "version" - }, - {"name": 1} - ) - version_name_by_id = {} - for version_doc in version_docs: - version_name_by_id[version_doc["_id"]] = \ - version_doc["name"] - - for item in items: - repre_id = io.ObjectId(item["representation"]) - version_id = version_id_by_repre_id.get(repre_id) - version_name = version_name_by_id.get(version_id) - if version_name is not None: - try: - api.update(item, version_name) - except AssertionError: - self._show_version_error_dialog(version_name, - [item]) - log.warning("Update failed", exc_info=True) - - self.data_changed.emit() - - update_icon = qtawesome.icon( - "fa.asterisk", - color=DEFAULT_COLOR - ) - switch_to_versioned = QtWidgets.QAction( - update_icon, - "Switch to versioned", - menu - ) - switch_to_versioned.triggered.connect( - lambda: _on_switch_to_versioned(items) - ) - - update_to_latest_action = None - if has_outdated or has_loaded_hero_versions: - # update to latest version - def _on_update_to_latest(items): - for item in items: - try: - api.update(item, -1) - except AssertionError: - self._show_version_error_dialog(None, [item]) - log.warning("Update failed", exc_info=True) - self.data_changed.emit() - - update_icon = qtawesome.icon( - "fa.angle-double-up", - color=DEFAULT_COLOR - ) - update_to_latest_action = QtWidgets.QAction( - update_icon, - "Update to latest", - menu - ) - update_to_latest_action.triggered.connect( - lambda: _on_update_to_latest(items) - ) - - change_to_hero = None - if has_available_hero_version: - # change to hero version - def _on_update_to_hero(items): - for item in items: - try: - api.update(item, HeroVersionType(-1)) - except AssertionError: - self._show_version_error_dialog('hero', [item]) - log.warning("Update failed", exc_info=True) - self.data_changed.emit() - - # TODO change icon - change_icon = qtawesome.icon( - "fa.asterisk", - color="#00b359" - ) - change_to_hero = QtWidgets.QAction( - change_icon, - "Change to hero", - menu - ) - change_to_hero.triggered.connect( - lambda: _on_update_to_hero(items) - ) - - # set version - set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) - set_version_action = QtWidgets.QAction( - set_version_icon, - "Set version", - menu - ) - set_version_action.triggered.connect( - lambda: self.show_version_dialog(items)) - - # switch asset - switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) - switch_asset_action = QtWidgets.QAction( - switch_asset_icon, - "Switch Asset", - menu - ) - switch_asset_action.triggered.connect( - lambda: self.show_switch_dialog(items)) - - # remove - remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) - remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) - remove_action.triggered.connect( - lambda: self.show_remove_warning_dialog(items)) - - # add the actions - if switch_to_versioned: - menu.addAction(switch_to_versioned) - - if update_to_latest_action: - menu.addAction(update_to_latest_action) - - if change_to_hero: - menu.addAction(change_to_hero) - - menu.addAction(set_version_action) - menu.addAction(switch_asset_action) - - menu.addSeparator() - - menu.addAction(remove_action) - - menu.addSeparator() - - if self.sync_enabled: - menu = self.handle_sync_server(menu, repre_ids) - - def handle_sync_server(self, menu, repre_ids): - """ - Adds actions for download/upload when SyncServer is enabled - - Args: - menu (OptionMenu) - repre_ids (list) of object_ids - Returns: - (OptionMenu) - """ - download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR) - download_active_action = QtWidgets.QAction( - download_icon, - "Download", - menu - ) - download_active_action.triggered.connect( - lambda: self._add_sites(repre_ids, 'active_site')) - - upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR) - upload_remote_action = QtWidgets.QAction( - upload_icon, - "Upload", - menu - ) - upload_remote_action.triggered.connect( - lambda: self._add_sites(repre_ids, 'remote_site')) - - menu.addAction(download_active_action) - menu.addAction(upload_remote_action) - - return menu - - def _add_sites(self, repre_ids, side): - """ - (Re)sync all 'repre_ids' to specific site. - - It checks if opposite site has fully available content to limit - accidents. (ReSync active when no remote >> losing active content) - - Args: - repre_ids (list) - side (str): 'active_site'|'remote_site' - """ - project = io.Session["AVALON_PROJECT"] - active_site = self.sync_server.get_active_site(project) - remote_site = self.sync_server.get_remote_site(project) - - for repre_id in repre_ids: - representation = io.find_one({"type": "representation", - "_id": repre_id}) - if not representation: - continue - - progress = tools_lib.get_progress_for_repre(representation, - active_site, - remote_site) - if side == 'active_site': - # check opposite from added site, must be 1 or unable to sync - check_progress = progress[remote_site] - site = active_site - else: - check_progress = progress[active_site] - site = remote_site - - if check_progress == 1: - self.sync_server.add_site(project, repre_id, site, force=True) - - self.data_changed.emit() - - def build_item_menu(self, items): - """Create menu for the selected items""" - - menu = QtWidgets.QMenu(self) - - # add the actions - self.build_item_menu_for_selection(items, menu) - - # These two actions should be able to work without selection - # expand all items - expandall_action = QtWidgets.QAction(menu, text="Expand all items") - expandall_action.triggered.connect(self.expandAll) - - # collapse all items - collapse_action = QtWidgets.QAction(menu, text="Collapse all items") - collapse_action.triggered.connect(self.collapseAll) - - menu.addAction(expandall_action) - menu.addAction(collapse_action) - - custom_actions = self.get_custom_actions(containers=items) - if custom_actions: - submenu = QtWidgets.QMenu("Actions", self) - for action in custom_actions: - - color = action.color or DEFAULT_COLOR - icon = qtawesome.icon("fa.%s" % action.icon, color=color) - action_item = QtWidgets.QAction(icon, action.label, submenu) - action_item.triggered.connect( - partial(self.process_custom_action, action, items)) - - submenu.addAction(action_item) - - menu.addMenu(submenu) - - # go back to flat view - if self._hierarchy_view: - back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR) - back_to_flat_action = QtWidgets.QAction( - back_to_flat_icon, - "Back to Full-View", - menu - ) - back_to_flat_action.triggered.connect(self.leave_hierarchy) - - # send items to hierarchy view - enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8") - enter_hierarchy_action = QtWidgets.QAction( - enter_hierarchy_icon, - "Cherry-Pick (Hierarchy)", - menu - ) - enter_hierarchy_action.triggered.connect( - lambda: self.enter_hierarchy(items)) - - if items: - menu.addAction(enter_hierarchy_action) - - if self._hierarchy_view: - menu.addAction(back_to_flat_action) - - return menu - - def get_custom_actions(self, containers): - """Get the registered Inventory Actions - - Args: - containers(list): collection of containers - - Returns: - list: collection of filter and initialized actions - """ - - def sorter(Plugin): - """Sort based on order attribute of the plugin""" - return Plugin.order - - # Fedd an empty dict if no selection, this will ensure the compat - # lookup always work, so plugin can interact with Scene Inventory - # reversely. - containers = containers or [dict()] - - # Check which action will be available in the menu - Plugins = api.discover(api.InventoryAction) - compatible = [p() for p in Plugins if - any(p.is_compatible(c) for c in containers)] - - return sorted(compatible, key=sorter) - - def process_custom_action(self, action, containers): - """Run action and if results are returned positive update the view - - If the result is list or dict, will select view items by the result. - - Args: - action (InventoryAction): Inventory Action instance - containers (list): Data of currently selected items - - Returns: - None - """ - - result = action.process(containers) - if result: - self.data_changed.emit() - - if isinstance(result, (list, set)): - self.select_items_by_action(result) - - if isinstance(result, dict): - self.select_items_by_action(result["objectNames"], - result["options"]) - - def select_items_by_action(self, object_names, options=None): - """Select view items by the result of action - - Args: - object_names (list or set): A list/set of container object name - options (dict): GUI operation options. - - Returns: - None - - """ - options = options or dict() - - if options.get("clear", True): - self.clearSelection() - - object_names = set(object_names) - if (self._hierarchy_view and - not self._selected.issuperset(object_names)): - # If any container not in current cherry-picked view, update - # view before selecting them. - self._selected.update(object_names) - self.data_changed.emit() - - model = self.model() - selection_model = self.selectionModel() - - select_mode = { - "select": selection_model.Select, - "deselect": selection_model.Deselect, - "toggle": selection_model.Toggle, - }[options.get("mode", "select")] - - for item in tools_lib.iter_model_rows(model, 0): - item = item.data(InventoryModel.ItemRole) - if item.get("isGroupNode"): - continue - - name = item.get("objectName") - if name in object_names: - self.scrollTo(item) # Ensure item is visible - flags = select_mode | selection_model.Rows - selection_model.select(item, flags) - - object_names.remove(name) - - if len(object_names) == 0: - break - - def show_right_mouse_menu(self, pos): - """Display the menu when at the position of the item clicked""" - - globalpos = self.viewport().mapToGlobal(pos) - - if not self.selectionModel().hasSelection(): - print("No selection") - # Build menu without selection, feed an empty list - menu = self.build_item_menu([]) - menu.exec_(globalpos) - return - - active = self.currentIndex() # index under mouse - active = active.sibling(active.row(), 0) # get first column - - # move index under mouse - indices = self.get_indices() - if active in indices: - indices.remove(active) - - indices.append(active) - - # Extend to the sub-items - all_indices = self.extend_to_children(indices) - items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices - if i.parent().isValid()] - - if self._hierarchy_view: - # Ensure no group item - items = [n for n in items if not n.get("isGroupNode")] - - menu = self.build_item_menu(items) - menu.exec_(globalpos) - - def get_indices(self): - """Get the selected rows""" - selection_model = self.selectionModel() - return selection_model.selectedRows() - - def extend_to_children(self, indices): - """Extend the indices to the children indices. - - Top-level indices are extended to its children indices. Sub-items - are kept as is. - - Args: - indices (list): The indices to extend. - - Returns: - list: The children indices - - """ - def get_children(i): - model = i.model() - rows = model.rowCount(parent=i) - for row in range(rows): - child = model.index(row, 0, parent=i) - yield child - - subitems = set() - for i in indices: - valid_parent = i.parent().isValid() - if valid_parent and i not in subitems: - subitems.add(i) - - if self._hierarchy_view: - # Assume this is a group item - for child in get_children(i): - subitems.add(child) - else: - # is top level item - for child in get_children(i): - subitems.add(child) - - return list(subitems) - - def show_version_dialog(self, items): - """Create a dialog with the available versions for the selected file - - Args: - items (list): list of items to run the "set_version" for - - Returns: - None - """ - - active = items[-1] - - # Get available versions for active representation - representation_id = io.ObjectId(active["representation"]) - representation = io.find_one({"_id": representation_id}) - version = io.find_one({ - "_id": representation["parent"] - }) - - versions = list(io.find( - { - "parent": version["parent"], - "type": "version" - }, - sort=[("name", 1)] - )) - - hero_version = io.find_one({ - "parent": version["parent"], - "type": "hero_version" - }) - if hero_version: - _version_id = hero_version["version_id"] - for _version in versions: - if _version["_id"] != _version_id: - continue - - hero_version["name"] = HeroVersionType( - _version["name"] - ) - hero_version["data"] = _version["data"] - break - - # Get index among the listed versions - current_item = None - current_version = active["version"] - if isinstance(current_version, HeroVersionType): - current_item = hero_version - else: - for version in versions: - if version["name"] == current_version: - current_item = version - break - - all_versions = [] - if hero_version: - all_versions.append(hero_version) - all_versions.extend(reversed(versions)) - - if current_item: - index = all_versions.index(current_item) - else: - index = 0 - - versions_by_label = dict() - labels = [] - for version in all_versions: - is_hero = version["type"] == "hero_version" - label = tools_lib.format_version(version["name"], is_hero) - labels.append(label) - versions_by_label[label] = version["name"] - - label, state = QtWidgets.QInputDialog.getItem( - self, - "Set version..", - "Set version number to", - labels, - current=index, - editable=False - ) - if not state: - return - - if label: - version = versions_by_label[label] - for item in items: - try: - api.update(item, version) - except AssertionError: - self._show_version_error_dialog(version, [item]) - log.warning("Update failed", exc_info=True) - # refresh model when done - self.data_changed.emit() - - def show_switch_dialog(self, items): - """Display Switch dialog""" - dialog = SwitchAssetDialog(self, items) - dialog.switched.connect(self.data_changed.emit) - dialog.show() - - def show_remove_warning_dialog(self, items): - """Prompt a dialog to inform the user the action will remove items""" - - accept = QtWidgets.QMessageBox.Ok - buttons = accept | QtWidgets.QMessageBox.Cancel - - message = ("Are you sure you want to remove " - "{} item(s)".format(len(items))) - state = QtWidgets.QMessageBox.question(self, "Are you sure?", - message, - buttons=buttons, - defaultButton=accept) - - if state != accept: - return - - for item in items: - api.remove(item) - self.data_changed.emit() - - def _show_version_error_dialog(self, version, items): - """Shows QMessageBox when version switch doesn't work - - Args: - version: str or int or None - """ - if not version: - version_str = "latest" - elif version == "hero": - version_str = "hero" - elif isinstance(version, int): - version_str = "v{:03d}".format(version) - else: - version_str = version - - dialog = QtWidgets.QMessageBox() - dialog.setIcon(QtWidgets.QMessageBox.Warning) - dialog.setStyleSheet(style.load_stylesheet()) - dialog.setWindowTitle("Update failed") - - switch_btn = dialog.addButton("Switch Asset", - QtWidgets.QMessageBox.ActionRole) - switch_btn.clicked.connect(lambda: self.show_switch_dialog(items)) - - dialog.addButton(QtWidgets.QMessageBox.Cancel) - - msg = "Version update to '{}' ".format(version_str) + \ - "failed as representation doesn't exist.\n\n" \ - "Please update to version with a valid " \ - "representation OR \n use 'Switch Asset' button " \ - "to change asset." - dialog.setText(msg) - dialog.exec_() - class SceneInventoryWindow(QtWidgets.QDialog): """Scene Inventory window""" From 0fcdbabeb112eb8e826ce0314d69d4a9682e7466 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 19:12:14 +0100 Subject: [PATCH 31/60] fixed imports in models --- openpype/tools/sceneinventory/model.py | 12 ++++++------ openpype/tools/sceneinventory/proxy.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 7b4e051b36..59c38ca553 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -2,13 +2,13 @@ import logging from collections import defaultdict -from ... import api, io, style, schema -from ...vendor.Qt import QtCore, QtGui -from ...vendor import qtawesome +from Qt import QtCore, QtGui +from avalon import api, io, style, schema +from avalon.vendor import qtawesome -from .. import lib as tools_lib -from ...lib import HeroVersionType -from ..models import TreeModel, Item +from avalon.tools import lib as tools_lib +from avalon.lib import HeroVersionType +from avalon.tools.models import TreeModel, Item from . import lib diff --git a/openpype/tools/sceneinventory/proxy.py b/openpype/tools/sceneinventory/proxy.py index 307e032eb6..0f92942ad5 100644 --- a/openpype/tools/sceneinventory/proxy.py +++ b/openpype/tools/sceneinventory/proxy.py @@ -1,6 +1,6 @@ import re -from ...vendor.Qt import QtCore +from Qt import QtCore from . import lib From 150eb6a29c090d3cbfac7e52fb71a09109f8ca4a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 19:14:38 +0100 Subject: [PATCH 32/60] use openpype style on main window --- openpype/tools/sceneinventory/window.py | 15 +++++++++++---- openpype/tools/utils/host_tools.py | 7 ++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index e0bbedf297..99e2228bb7 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -3,12 +3,13 @@ import sys from Qt import QtWidgets, QtCore from avalon.vendor import qtawesome -from avalon import io, api, style - +from avalon import io, api from avalon.tools import lib as tools_lib from avalon.tools.delegates import VersionDelegate +from openpype import style + from .proxy import FilterProxyModel from .model import InventoryModel from .view import View @@ -94,7 +95,6 @@ class SceneInventoryWindow(QtWidgets.QDialog): "version": version_delegate } } - # set some nice default widths for the view self.view.setColumnWidth(0, 250) # name self.view.setColumnWidth(1, 55) # version @@ -104,6 +104,14 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.family_config_cache.refresh() + self._first_show = True + + def showEvent(self, event): + super(SceneInventoryWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self.setStyleSheet(style.load_stylesheet()) + def keyPressEvent(self, event): """Custom keyPressEvent. @@ -161,7 +169,6 @@ def show(root=None, debug=False, parent=None, items=None): with tools_lib.application(): window = SceneInventoryWindow(parent) - window.setStyleSheet(style.load_stylesheet()) window.show() window.refresh(items=items) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index d5e4792c94..8011410ce9 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -154,21 +154,18 @@ class HostToolsHelper: def get_scene_inventory_tool(self, parent): """Create, cache and return scene inventory tool window.""" if self._scene_inventory_tool is None: - from avalon.tools.sceneinventory.app import Window + from openpype.tools.sceneinventory import SceneInventoryWindow - scene_inventory_window = Window(parent=parent or self._parent) + scene_inventory_window = SceneInventoryWindow(parent=parent or self._parent) self._scene_inventory_tool = scene_inventory_window return self._scene_inventory_tool def show_scene_inventory(self, parent=None): """Show tool maintain loaded containers.""" - from avalon import style - scene_inventory_tool = self.get_scene_inventory_tool(parent) scene_inventory_tool.show() scene_inventory_tool.refresh() - scene_inventory_tool.setStyleSheet(style.load_stylesheet()) # Pull window to the front. scene_inventory_tool.raise_() From a10fc7e492f67154f21587f365b02a3fae5adc45 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 15 Nov 2021 19:21:56 +0100 Subject: [PATCH 33/60] reorganized initialization --- openpype/tools/sceneinventory/proxy.py | 12 ++++------ openpype/tools/sceneinventory/view.py | 4 ---- openpype/tools/sceneinventory/window.py | 29 ++++++++++++++----------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/openpype/tools/sceneinventory/proxy.py b/openpype/tools/sceneinventory/proxy.py index 0f92942ad5..7d4e6fdb4c 100644 --- a/openpype/tools/sceneinventory/proxy.py +++ b/openpype/tools/sceneinventory/proxy.py @@ -14,11 +14,8 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): self._hierarchy_view = False def filterAcceptsRow(self, row, parent): - model = self.sourceModel() - source_index = model.index(row, - self.filterKeyColumn(), - parent) + source_index = model.index(row, self.filterKeyColumn(), parent) # Always allow bottom entries (individual containers), since their # parent group hidden if it wouldn't have been validated. @@ -97,13 +94,12 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): if is_outdated: return True - elif self._hierarchy_view: + if self._hierarchy_view: for _node in lib.walk_hierarchy(node): if outdated(_node): return True - return False - else: - return False + + return False def _matches(self, row, parent, pattern): """Return whether row matches regex pattern. diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 512c65e143..88914fd0af 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -27,10 +27,6 @@ class View(QtWidgets.QTreeView): def __init__(self, parent=None): super(View, self).__init__(parent=parent) - if not parent: - self.setWindowFlags( - self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint - ) # view settings self.setIndentation(12) self.setAlternatingRowColors(True) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 99e2228bb7..ed2b848481 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -25,6 +25,11 @@ class SceneInventoryWindow(QtWidgets.QDialog): def __init__(self, parent=None): super(SceneInventoryWindow, self).__init__(parent) + if not parent: + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint + ) + self.resize(1100, 480) self.setWindowTitle( "Scene Inventory 1.0 - {}".format( @@ -34,21 +39,19 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.setObjectName("SceneInventory") self.setProperty("saveWindowPref", True) # Maya only property! - layout = QtWidgets.QVBoxLayout(self) - # region control - control_layout = QtWidgets.QHBoxLayout() - filter_label = QtWidgets.QLabel("Search") - text_filter = QtWidgets.QLineEdit() + filter_label = QtWidgets.QLabel("Search", self) + text_filter = QtWidgets.QLineEdit(self) - outdated_only = QtWidgets.QCheckBox("Filter to outdated") + outdated_only = QtWidgets.QCheckBox("Filter to outdated", self) outdated_only.setToolTip("Show outdated files only") outdated_only.setChecked(False) icon = qtawesome.icon("fa.refresh", color="white") - refresh_button = QtWidgets.QPushButton() + refresh_button = QtWidgets.QPushButton(self) refresh_button.setIcon(icon) + control_layout = QtWidgets.QHBoxLayout() control_layout.addWidget(filter_label) control_layout.addWidget(text_filter) control_layout.addWidget(outdated_only) @@ -59,7 +62,11 @@ class SceneInventoryWindow(QtWidgets.QDialog): model = InventoryModel(self.family_config_cache) proxy = FilterProxyModel() - view = View() + proxy.setSourceModel(model) + proxy.setDynamicSortFilter(True) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + + view = View(self) view.setModel(proxy) # apply delegates @@ -67,6 +74,7 @@ class SceneInventoryWindow(QtWidgets.QDialog): column = model.Columns.index("version") view.setItemDelegateForColumn(column, version_delegate) + layout = QtWidgets.QVBoxLayout(self) layout.addLayout(control_layout) layout.addWidget(view) @@ -85,11 +93,6 @@ class SceneInventoryWindow(QtWidgets.QDialog): view.hierarchy_view.connect(self.model.set_hierarchy_view) view.hierarchy_view.connect(self.proxy.set_hierarchy_view) - # proxy settings - proxy.setSourceModel(self.model) - proxy.setDynamicSortFilter(True) - proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - self.data = { "delegates": { "version": version_delegate From 4a57c13958ad839a172aa5ea195de71032a247c9 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 15 Nov 2021 21:17:25 +0100 Subject: [PATCH 34/60] add github token to prerelase calculation --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 60ce608b21..258458e2d4 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -33,7 +33,7 @@ jobs: id: version if: steps.version_type.outputs.type != 'skip' run: | - RESULT=$(python ./tools/ci_tools.py --nightly) + RESULT=$(python ./tools/ci_tools.py --nightly --github_token ${{ secrets.GITHUB_TOKEN }}) echo ::set-output name=next_tag::$RESULT From 023275368b5086d53b566a8b5ad504573eee5687 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Mon, 15 Nov 2021 20:24:30 +0000 Subject: [PATCH 35/60] [Automated] Bump version --- CHANGELOG.md | 66 +++++++++++++++++---------------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 27 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index add7f53ae9..94e093fa4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,29 @@ # Changelog -## [3.6.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.6.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...HEAD) +### 📖 Documentation + +- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) +- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) + **🆕 New features** +- Add validate active site button to sync queue on a project [\#2176](https://github.com/pypeclub/OpenPype/pull/2176) - Maya : Colorspace configuration [\#2170](https://github.com/pypeclub/OpenPype/pull/2170) - Blender: Added support for audio [\#2168](https://github.com/pypeclub/OpenPype/pull/2168) -- Flame: a host basic integration [\#2165](https://github.com/pypeclub/OpenPype/pull/2165) -- Houdini: simple HDA workflow [\#2072](https://github.com/pypeclub/OpenPype/pull/2072) **🚀 Enhancements** +- Tools: Subset manager in OpenPype [\#2243](https://github.com/pypeclub/OpenPype/pull/2243) +- General: Skip module directories without init file [\#2239](https://github.com/pypeclub/OpenPype/pull/2239) +- General: Static interfaces [\#2238](https://github.com/pypeclub/OpenPype/pull/2238) +- Style: Fix transparent image in style [\#2235](https://github.com/pypeclub/OpenPype/pull/2235) +- Add a "following workfile versioning" option on publish [\#2225](https://github.com/pypeclub/OpenPype/pull/2225) +- Modules: Module can add cli commands [\#2224](https://github.com/pypeclub/OpenPype/pull/2224) +- Webpublisher: Separate webpublisher logic [\#2222](https://github.com/pypeclub/OpenPype/pull/2222) - Add both side availability on Site Sync sites to Loader [\#2220](https://github.com/pypeclub/OpenPype/pull/2220) - Tools: Center loader and library loader on show [\#2219](https://github.com/pypeclub/OpenPype/pull/2219) - Maya : Validate shape zero [\#2212](https://github.com/pypeclub/OpenPype/pull/2212) @@ -21,34 +32,31 @@ - Ftrack: Replace Queue with deque in event handlers logic [\#2204](https://github.com/pypeclub/OpenPype/pull/2204) - Tools: New select context dialog [\#2200](https://github.com/pypeclub/OpenPype/pull/2200) - Maya : Validate mesh ngons [\#2199](https://github.com/pypeclub/OpenPype/pull/2199) +- Dirmap in Nuke [\#2198](https://github.com/pypeclub/OpenPype/pull/2198) - Delivery: Check 'frame' key in template for sequence delivery [\#2196](https://github.com/pypeclub/OpenPype/pull/2196) +- Settings: Site sync project settings improvement [\#2193](https://github.com/pypeclub/OpenPype/pull/2193) - Usage of tools code [\#2185](https://github.com/pypeclub/OpenPype/pull/2185) - Settings: Dictionary based on project roots [\#2184](https://github.com/pypeclub/OpenPype/pull/2184) - Subset name: Be able to pass asset document to get subset name [\#2179](https://github.com/pypeclub/OpenPype/pull/2179) -- Tools: Experimental tools [\#2167](https://github.com/pypeclub/OpenPype/pull/2167) - Loader: Refactor and use OpenPype stylesheets [\#2166](https://github.com/pypeclub/OpenPype/pull/2166) -- Add loader for linked smart objects in photoshop [\#2149](https://github.com/pypeclub/OpenPype/pull/2149) **🐛 Bug fixes** +- Ftrack: Sync project ftrack id cache issue [\#2250](https://github.com/pypeclub/OpenPype/pull/2250) +- Ftrack: Session creation and Prepare project [\#2245](https://github.com/pypeclub/OpenPype/pull/2245) +- Added queue for studio processing in PS [\#2237](https://github.com/pypeclub/OpenPype/pull/2237) +- Python 2: Unicode to string conversion [\#2236](https://github.com/pypeclub/OpenPype/pull/2236) +- Fix - enum for color coding in PS [\#2234](https://github.com/pypeclub/OpenPype/pull/2234) +- Pyblish Tool: Fix targets handling [\#2232](https://github.com/pypeclub/OpenPype/pull/2232) +- Ftrack: Base event fix of 'get\_project\_from\_entity' method [\#2214](https://github.com/pypeclub/OpenPype/pull/2214) - Maya : multiple subsets review broken [\#2210](https://github.com/pypeclub/OpenPype/pull/2210) - Fix - different command used for Linux and Mac OS [\#2207](https://github.com/pypeclub/OpenPype/pull/2207) - Tools: Workfiles tool don't use avalon widgets [\#2205](https://github.com/pypeclub/OpenPype/pull/2205) - Ftrack: Fill missing ftrack id on mongo project [\#2203](https://github.com/pypeclub/OpenPype/pull/2203) - Project Manager: Fix copying of tasks [\#2191](https://github.com/pypeclub/OpenPype/pull/2191) -- StandalonePublisher: Source validator don't expect representations [\#2190](https://github.com/pypeclub/OpenPype/pull/2190) - Blender: Fix trying to pack an image when the shader node has no texture [\#2183](https://github.com/pypeclub/OpenPype/pull/2183) -- MacOS: Launching of applications may cause Permissions error [\#2175](https://github.com/pypeclub/OpenPype/pull/2175) +- Maya: review viewport settings [\#2177](https://github.com/pypeclub/OpenPype/pull/2177) - Maya: Aspect ratio [\#2174](https://github.com/pypeclub/OpenPype/pull/2174) -- Blender: Fix 'Deselect All' with object not in 'Object Mode' [\#2163](https://github.com/pypeclub/OpenPype/pull/2163) -- Maya: Fix hotbox broken by scriptsmenu [\#2151](https://github.com/pypeclub/OpenPype/pull/2151) -- Added validator for source files for Standalone Publisher [\#2138](https://github.com/pypeclub/OpenPype/pull/2138) - -**Merged pull requests:** - -- Settings: Site sync project settings improvement [\#2193](https://github.com/pypeclub/OpenPype/pull/2193) -- Add validate active site button to sync queue on a project [\#2176](https://github.com/pypeclub/OpenPype/pull/2176) -- Bump pillow from 8.2.0 to 8.3.2 [\#2162](https://github.com/pypeclub/OpenPype/pull/2162) ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) @@ -63,23 +71,14 @@ - Added project and task into context change message in Maya [\#2131](https://github.com/pypeclub/OpenPype/pull/2131) - Add ExtractBurnin to photoshop review [\#2124](https://github.com/pypeclub/OpenPype/pull/2124) - PYPE-1218 - changed namespace to contain subset name in Maya [\#2114](https://github.com/pypeclub/OpenPype/pull/2114) -- Added running configurable disk mapping command before start of OP [\#2091](https://github.com/pypeclub/OpenPype/pull/2091) -- SFTP provider [\#2073](https://github.com/pypeclub/OpenPype/pull/2073) **🚀 Enhancements** +- Burnins: DNxHD profiles handling [\#2142](https://github.com/pypeclub/OpenPype/pull/2142) - Maya: make rig validators configurable in settings [\#2137](https://github.com/pypeclub/OpenPype/pull/2137) - Settings: Updated readme for entity types in settings [\#2132](https://github.com/pypeclub/OpenPype/pull/2132) - Nuke: unified clip loader [\#2128](https://github.com/pypeclub/OpenPype/pull/2128) - Settings UI: Project model refreshing and sorting [\#2104](https://github.com/pypeclub/OpenPype/pull/2104) -- Create Read From Rendered - Disable Relative paths by default [\#2093](https://github.com/pypeclub/OpenPype/pull/2093) -- Added choosing different dirmap mapping if workfile synched locally [\#2088](https://github.com/pypeclub/OpenPype/pull/2088) -- General: Remove IdleManager module [\#2084](https://github.com/pypeclub/OpenPype/pull/2084) -- Tray UI: Message box about missing settings defaults [\#2080](https://github.com/pypeclub/OpenPype/pull/2080) -- Tray UI: Show menu where first click happened [\#2079](https://github.com/pypeclub/OpenPype/pull/2079) -- Global: add global validators to settings [\#2078](https://github.com/pypeclub/OpenPype/pull/2078) -- Use CRF for burnin when available [\#2070](https://github.com/pypeclub/OpenPype/pull/2070) -- Project manager: Filter first item after selection of project [\#2069](https://github.com/pypeclub/OpenPype/pull/2069) **🐛 Bug fixes** @@ -90,21 +89,6 @@ - Add startup script for Houdini Core. [\#2110](https://github.com/pypeclub/OpenPype/pull/2110) - TVPaint: Behavior name of loop also accept repeat [\#2109](https://github.com/pypeclub/OpenPype/pull/2109) - Ftrack: Project settings save custom attributes skip unknown attributes [\#2103](https://github.com/pypeclub/OpenPype/pull/2103) -- Blender: Fix NoneType error when animation\_data is missing for a rig [\#2101](https://github.com/pypeclub/OpenPype/pull/2101) -- Fix broken import in sftp provider [\#2100](https://github.com/pypeclub/OpenPype/pull/2100) -- Global: Fix docstring on publish plugin extract review [\#2097](https://github.com/pypeclub/OpenPype/pull/2097) -- Delivery Action Files Sequence fix [\#2096](https://github.com/pypeclub/OpenPype/pull/2096) -- General: Cloud mongo ca certificate issue [\#2095](https://github.com/pypeclub/OpenPype/pull/2095) -- TVPaint: Creator use context from workfile [\#2087](https://github.com/pypeclub/OpenPype/pull/2087) -- Blender: fix texture missing when publishing blend files [\#2085](https://github.com/pypeclub/OpenPype/pull/2085) -- General: Startup validations oiio tool path fix on linux [\#2083](https://github.com/pypeclub/OpenPype/pull/2083) -- Deadline: Collect deadline server does not check existence of deadline key [\#2082](https://github.com/pypeclub/OpenPype/pull/2082) -- Blender: fixed Curves with modifiers in Rigs [\#2081](https://github.com/pypeclub/OpenPype/pull/2081) -- Nuke UI scaling [\#2077](https://github.com/pypeclub/OpenPype/pull/2077) - -**Merged pull requests:** - -- Bump pywin32 from 300 to 301 [\#2086](https://github.com/pypeclub/OpenPype/pull/2086) ## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23) diff --git a/openpype/version.py b/openpype/version.py index 7f85931698..ae7c4843e1 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.6.0-nightly.5" +__version__ = "3.6.0-nightly.6" diff --git a/pyproject.toml b/pyproject.toml index 8dd8664eae..fe921dc264 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.6.0-nightly.5" # OpenPype +version = "3.6.0-nightly.6" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From b041a884f3e023b3cd6ee394e42ff45259baa7f9 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Mon, 15 Nov 2021 20:32:28 +0000 Subject: [PATCH 36/60] [Automated] Release --- CHANGELOG.md | 17 ++++++++--------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e093fa4e..fb0ee845ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,8 @@ # Changelog -## [3.6.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.6.0](https://github.com/pypeclub/OpenPype/tree/3.6.0) (2021-11-15) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...HEAD) - -### 📖 Documentation - -- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) -- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...3.6.0) **🆕 New features** @@ -29,6 +24,7 @@ - Maya : Validate shape zero [\#2212](https://github.com/pypeclub/OpenPype/pull/2212) - Maya : validate unique names [\#2211](https://github.com/pypeclub/OpenPype/pull/2211) - Tools: OpenPype stylesheet in workfiles tool [\#2208](https://github.com/pypeclub/OpenPype/pull/2208) +- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) - Ftrack: Replace Queue with deque in event handlers logic [\#2204](https://github.com/pypeclub/OpenPype/pull/2204) - Tools: New select context dialog [\#2200](https://github.com/pypeclub/OpenPype/pull/2200) - Maya : Validate mesh ngons [\#2199](https://github.com/pypeclub/OpenPype/pull/2199) @@ -38,7 +34,6 @@ - Usage of tools code [\#2185](https://github.com/pypeclub/OpenPype/pull/2185) - Settings: Dictionary based on project roots [\#2184](https://github.com/pypeclub/OpenPype/pull/2184) - Subset name: Be able to pass asset document to get subset name [\#2179](https://github.com/pypeclub/OpenPype/pull/2179) -- Loader: Refactor and use OpenPype stylesheets [\#2166](https://github.com/pypeclub/OpenPype/pull/2166) **🐛 Bug fixes** @@ -58,6 +53,10 @@ - Maya: review viewport settings [\#2177](https://github.com/pypeclub/OpenPype/pull/2177) - Maya: Aspect ratio [\#2174](https://github.com/pypeclub/OpenPype/pull/2174) +### 📖 Documentation + +- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) + ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0) @@ -74,7 +73,6 @@ **🚀 Enhancements** -- Burnins: DNxHD profiles handling [\#2142](https://github.com/pypeclub/OpenPype/pull/2142) - Maya: make rig validators configurable in settings [\#2137](https://github.com/pypeclub/OpenPype/pull/2137) - Settings: Updated readme for entity types in settings [\#2132](https://github.com/pypeclub/OpenPype/pull/2132) - Nuke: unified clip loader [\#2128](https://github.com/pypeclub/OpenPype/pull/2128) @@ -82,6 +80,7 @@ **🐛 Bug fixes** +- Maya: Fix hotbox broken by scriptsmenu [\#2151](https://github.com/pypeclub/OpenPype/pull/2151) - Maya: fix model publishing [\#2130](https://github.com/pypeclub/OpenPype/pull/2130) - Fix - oiiotool wasn't recognized even if present [\#2129](https://github.com/pypeclub/OpenPype/pull/2129) - General: Disk mapping group [\#2120](https://github.com/pypeclub/OpenPype/pull/2120) diff --git a/openpype/version.py b/openpype/version.py index ae7c4843e1..122137e6cd 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.6.0-nightly.6" +__version__ = "3.6.0" diff --git a/pyproject.toml b/pyproject.toml index fe921dc264..dfc11b9881 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.6.0-nightly.6" # OpenPype +version = "3.6.0" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 840e3fe4314abba80e45c214aabc2db98433508f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Nov 2021 10:37:48 +0100 Subject: [PATCH 37/60] Fix - added missed argument --- openpype/tools/loader/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index d81fc11cf2..74768dfa25 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -243,9 +243,9 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): # update availability on active site when version changes if self.sync_server.enabled and version: - site = self.active_site query = self._repre_per_version_pipeline([version["_id"]], - site) + self.active_site, + self.remote_site) docs = list(self.dbcon.aggregate(query)) if docs: repre = docs.pop() From f4d070bce00dc3ba9da7c8c678f35ad261875e80 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 10:37:59 +0100 Subject: [PATCH 38/60] minor reorganizations and renaming --- openpype/tools/sceneinventory/model.py | 64 ++++++----- openpype/tools/sceneinventory/proxy.py | 34 +++--- openpype/tools/sceneinventory/view.py | 147 +++++++++++++----------- openpype/tools/sceneinventory/window.py | 40 +++---- 4 files changed, 154 insertions(+), 131 deletions(-) diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 59c38ca553..bf7b296703 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -41,33 +41,36 @@ class InventoryModel(TreeModel): self.active_site = self.remote_site = None self.active_provider = self.remote_provider = None - if self.sync_enabled: - project = io.Session['AVALON_PROJECT'] - active_site = sync_server.get_active_site(project) - remote_site = sync_server.get_remote_site(project) + if not self.sync_enabled: + return - # TODO refactor - active_provider = \ - sync_server.get_provider_for_site(project, - active_site) - if active_site == 'studio': - active_provider = 'studio' # sanitized for icon + project_name = io.Session["AVALON_PROJECT"] + active_site = sync_server.get_active_site(project_name) + remote_site = sync_server.get_remote_site(project_name) - remote_provider = \ - sync_server.get_provider_for_site(project, - remote_site) - if remote_site == 'studio': - remote_provider = 'studio' + active_provider = "studio" + remote_provider = "studio" + if active_site != "studio": + # sanitized for icon + active_provider = sync_server.get_provider_for_site( + project_name, active_site + ) - # self.sync_server = sync_server - self.active_site = active_site - self.active_provider = active_provider - self.remote_site = remote_site - self.remote_provider = remote_provider - self._icons = tools_lib.get_repre_icons() - if 'active_site' not in self.Columns and \ - 'remote_site' not in self.Columns: - self.Columns.extend(['active_site', 'remote_site']) + if remote_site != "studio": + remote_provider = sync_server.get_provider_for_site( + project_name, remote_site + ) + + # self.sync_server = sync_server + self.active_site = active_site + self.active_provider = active_provider + self.remote_site = remote_site + self.remote_provider = remote_provider + self._icons = tools_lib.get_repre_icons() + if "active_site" not in self.Columns: + self.Columns.append("active_site") + if "remote_site" not in self.Columns: + self.Columns.extend("remote_site") def outdated(self, item): value = item.get("version") @@ -79,7 +82,6 @@ class InventoryModel(TreeModel): return True def data(self, index, role): - if not index.isValid(): return @@ -128,10 +130,10 @@ class InventoryModel(TreeModel): color = item.get("color", style.colors.default) if item.get("isGroupNode"): # group-item return qtawesome.icon("fa.folder", color=color) - elif item.get("isNotSet"): + if item.get("isNotSet"): return qtawesome.icon("fa.exclamation-circle", color=color) - else: - return qtawesome.icon("fa.file-o", color=color) + + return qtawesome.icon("fa.file-o", color=color) if index.column() == 3: # Family icon @@ -393,9 +395,9 @@ class InventoryModel(TreeModel): group_node["isGroupNode"] = True if self.sync_enabled: - progress = tools_lib.get_progress_for_repre(representation, - self.active_site, - self.remote_site) + progress = tools_lib.get_progress_for_repre( + representation, self.active_site, self.remote_site + ) group_node["active_site"] = self.active_site group_node["active_site_provider"] = self.active_provider group_node["remote_site"] = self.remote_site diff --git a/openpype/tools/sceneinventory/proxy.py b/openpype/tools/sceneinventory/proxy.py index 7d4e6fdb4c..3c4295c446 100644 --- a/openpype/tools/sceneinventory/proxy.py +++ b/openpype/tools/sceneinventory/proxy.py @@ -123,22 +123,28 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): if re.search(pattern, key, re.IGNORECASE): return True - if not matches(row, parent, pattern): - # Also allow if any of the children matches - source_index = model.index(row, column, parent) - rows = model.rowCount(source_index) + if matches(row, parent, pattern): + return True - if not any(matches(i, source_index, pattern) - for i in range(rows)): + # Also allow if any of the children matches + source_index = model.index(row, column, parent) + rows = model.rowCount(source_index) - if self._hierarchy_view: - for i in range(rows): - child_i = model.index(i, column, source_index) - child_rows = model.rowCount(child_i) - return any(self._matches(ch_i, child_i, pattern) - for ch_i in range(child_rows)) + if any( + matches(idx, source_index, pattern) + for idx in range(rows) + ): + return True - else: - return False + if not self._hierarchy_view: + return False + + for i in range(rows): + child_i = model.index(i, column, source_index) + child_rows = model.rowCount(child_i) + return any( + self._matches(ch_i, child_i, pattern) + for ch_i in range(child_rows) + ) return True diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 88914fd0af..08d5499355 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -20,12 +20,12 @@ DEFAULT_COLOR = "#fb9c15" log = logging.getLogger("SceneInventory") -class View(QtWidgets.QTreeView): +class SceneInvetoryView(QtWidgets.QTreeView): data_changed = QtCore.Signal() hierarchy_view = QtCore.Signal(bool) def __init__(self, parent=None): - super(View, self).__init__(parent=parent) + super(SceneInvetoryView, self).__init__(parent=parent) # view settings self.setIndentation(12) @@ -33,7 +33,7 @@ class View(QtWidgets.QTreeView): self.setSortingEnabled(True) self.setSelectionMode(self.ExtendedSelection) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.show_right_mouse_menu) + self.customContextMenuRequested.connect(self._show_right_mouse_menu) self._hierarchy_view = False self._selected = None @@ -41,7 +41,7 @@ class View(QtWidgets.QTreeView): self.sync_server = manager.modules_by_name["sync_server"] self.sync_enabled = self.sync_server.enabled - def enter_hierarchy(self, items): + def _enter_hierarchy(self, items): self._selected = set(i["objectName"] for i in items) self._hierarchy_view = True self.hierarchy_view.emit(True) @@ -53,13 +53,13 @@ class View(QtWidgets.QTreeView): } """) - def leave_hierarchy(self): + def _leave_hierarchy(self): self._hierarchy_view = False self.hierarchy_view.emit(False) self.data_changed.emit() self.setStyleSheet("QTreeView {}") - def build_item_menu_for_selection(self, items, menu): + def _build_item_menu_for_selection(self, items, menu): if not items: return @@ -267,7 +267,7 @@ class View(QtWidgets.QTreeView): menu ) set_version_action.triggered.connect( - lambda: self.show_version_dialog(items)) + lambda: self._show_version_dialog(items)) # switch asset switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR) @@ -277,13 +277,13 @@ class View(QtWidgets.QTreeView): menu ) switch_asset_action.triggered.connect( - lambda: self.show_switch_dialog(items)) + lambda: self._show_switch_dialog(items)) # remove remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) remove_action.triggered.connect( - lambda: self.show_remove_warning_dialog(items)) + lambda: self._show_remove_warning_dialog(items)) # add the actions if switch_to_versioned: @@ -302,12 +302,9 @@ class View(QtWidgets.QTreeView): menu.addAction(remove_action) - menu.addSeparator() + self._handle_sync_server(menu, repre_ids) - if self.sync_enabled: - menu = self.handle_sync_server(menu, repre_ids) - - def handle_sync_server(self, menu, repre_ids): + def _handle_sync_server(self, menu, repre_ids): """ Adds actions for download/upload when SyncServer is enabled @@ -317,6 +314,11 @@ class View(QtWidgets.QTreeView): Returns: (OptionMenu) """ + if not self.sync_enabled: + return + + menu.addSeparator() + download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR) download_active_action = QtWidgets.QAction( download_icon, @@ -338,8 +340,6 @@ class View(QtWidgets.QTreeView): menu.addAction(download_active_action) menu.addAction(upload_remote_action) - return menu - def _add_sites(self, repre_ids, side): """ (Re)sync all 'repre_ids' to specific site. @@ -351,20 +351,29 @@ class View(QtWidgets.QTreeView): repre_ids (list) side (str): 'active_site'|'remote_site' """ - project = io.Session["AVALON_PROJECT"] - active_site = self.sync_server.get_active_site(project) - remote_site = self.sync_server.get_remote_site(project) + project_name = io.Session["AVALON_PROJECT"] + active_site = self.sync_server.get_active_site(project_name) + remote_site = self.sync_server.get_remote_site(project_name) + repre_docs = io.find({ + "type": "representation", + "_id": {"$in": repre_ids} + }) + repre_docs_by_id = { + repre_doc["_id"]: repre_doc + for repre_doc in repre_docs + } for repre_id in repre_ids: - representation = io.find_one({"type": "representation", - "_id": repre_id}) - if not representation: + repre_doc = repre_docs_by_id.get(repre_id) + if not repre_doc: continue - progress = tools_lib.get_progress_for_repre(representation, - active_site, - remote_site) - if side == 'active_site': + progress = tools_lib.get_progress_for_repre( + repre_doc, + active_site, + remote_site + ) + if side == "active_site": # check opposite from added site, must be 1 or unable to sync check_progress = progress[remote_site] site = active_site @@ -373,17 +382,22 @@ class View(QtWidgets.QTreeView): site = remote_site if check_progress == 1: - self.sync_server.add_site(project, repre_id, site, force=True) + self.sync_server.add_site( + project_name, repre_id, site, force=True + ) self.data_changed.emit() - def build_item_menu(self, items): + def _build_item_menu(self, items=None): """Create menu for the selected items""" + if not items: + items = [] + menu = QtWidgets.QMenu(self) # add the actions - self.build_item_menu_for_selection(items, menu) + self._build_item_menu_for_selection(items, menu) # These two actions should be able to work without selection # expand all items @@ -397,16 +411,15 @@ class View(QtWidgets.QTreeView): menu.addAction(expandall_action) menu.addAction(collapse_action) - custom_actions = self.get_custom_actions(containers=items) + custom_actions = self._get_custom_actions(containers=items) if custom_actions: submenu = QtWidgets.QMenu("Actions", self) for action in custom_actions: - color = action.color or DEFAULT_COLOR icon = qtawesome.icon("fa.%s" % action.icon, color=color) action_item = QtWidgets.QAction(icon, action.label, submenu) action_item.triggered.connect( - partial(self.process_custom_action, action, items)) + partial(self._process_custom_action, action, items)) submenu.addAction(action_item) @@ -420,7 +433,7 @@ class View(QtWidgets.QTreeView): "Back to Full-View", menu ) - back_to_flat_action.triggered.connect(self.leave_hierarchy) + back_to_flat_action.triggered.connect(self._leave_hierarchy) # send items to hierarchy view enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8") @@ -430,7 +443,7 @@ class View(QtWidgets.QTreeView): menu ) enter_hierarchy_action.triggered.connect( - lambda: self.enter_hierarchy(items)) + lambda: self._enter_hierarchy(items)) if items: menu.addAction(enter_hierarchy_action) @@ -440,7 +453,7 @@ class View(QtWidgets.QTreeView): return menu - def get_custom_actions(self, containers): + def _get_custom_actions(self, containers): """Get the registered Inventory Actions Args: @@ -466,7 +479,7 @@ class View(QtWidgets.QTreeView): return sorted(compatible, key=sorter) - def process_custom_action(self, action, containers): + def _process_custom_action(self, action, containers): """Run action and if results are returned positive update the view If the result is list or dict, will select view items by the result. @@ -484,13 +497,14 @@ class View(QtWidgets.QTreeView): self.data_changed.emit() if isinstance(result, (list, set)): - self.select_items_by_action(result) + self._select_items_by_action(result) if isinstance(result, dict): - self.select_items_by_action(result["objectNames"], - result["options"]) + self._select_items_by_action( + result["objectNames"], result["options"] + ) - def select_items_by_action(self, object_names, options=None): + def _select_items_by_action(self, object_names, options=None): """Select view items by the result of action Args: @@ -507,8 +521,10 @@ class View(QtWidgets.QTreeView): self.clearSelection() object_names = set(object_names) - if (self._hierarchy_view and - not self._selected.issuperset(object_names)): + if ( + self._hierarchy_view + and not self._selected.issuperset(object_names) + ): # If any container not in current cherry-picked view, update # view before selecting them. self._selected.update(object_names) @@ -539,7 +555,7 @@ class View(QtWidgets.QTreeView): if len(object_names) == 0: break - def show_right_mouse_menu(self, pos): + def _show_right_mouse_menu(self, pos): """Display the menu when at the position of the item clicked""" globalpos = self.viewport().mapToGlobal(pos) @@ -547,7 +563,7 @@ class View(QtWidgets.QTreeView): if not self.selectionModel().hasSelection(): print("No selection") # Build menu without selection, feed an empty list - menu = self.build_item_menu([]) + menu = self._build_item_menu() menu.exec_(globalpos) return @@ -562,7 +578,7 @@ class View(QtWidgets.QTreeView): indices.append(active) # Extend to the sub-items - all_indices = self.extend_to_children(indices) + all_indices = self._extend_to_children(indices) items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices if i.parent().isValid()] @@ -570,7 +586,7 @@ class View(QtWidgets.QTreeView): # Ensure no group item items = [n for n in items if not n.get("isGroupNode")] - menu = self.build_item_menu(items) + menu = self._build_item_menu(items) menu.exec_(globalpos) def get_indices(self): @@ -578,7 +594,7 @@ class View(QtWidgets.QTreeView): selection_model = self.selectionModel() return selection_model.selectedRows() - def extend_to_children(self, indices): + def _extend_to_children(self, indices): """Extend the indices to the children indices. Top-level indices are extended to its children indices. Sub-items @@ -615,7 +631,7 @@ class View(QtWidgets.QTreeView): return list(subitems) - def show_version_dialog(self, items): + def _show_version_dialog(self, items): """Create a dialog with the available versions for the selected file Args: @@ -709,24 +725,25 @@ class View(QtWidgets.QTreeView): # refresh model when done self.data_changed.emit() - def show_switch_dialog(self, items): + def _show_switch_dialog(self, items): """Display Switch dialog""" dialog = SwitchAssetDialog(self, items) dialog.switched.connect(self.data_changed.emit) dialog.show() - def show_remove_warning_dialog(self, items): + def _show_remove_warning_dialog(self, items): """Prompt a dialog to inform the user the action will remove items""" accept = QtWidgets.QMessageBox.Ok buttons = accept | QtWidgets.QMessageBox.Cancel - message = ("Are you sure you want to remove " - "{} item(s)".format(len(items))) - state = QtWidgets.QMessageBox.question(self, "Are you sure?", - message, - buttons=buttons, - defaultButton=accept) + state = QtWidgets.QMessageBox.question( + self, + "Are you sure?", + "Are you sure you want to remove {} item(s)".format(len(items)), + buttons=buttons, + defaultButton=accept + ) if state != accept: return @@ -755,16 +772,18 @@ class View(QtWidgets.QTreeView): dialog.setStyleSheet(style.load_stylesheet()) dialog.setWindowTitle("Update failed") - switch_btn = dialog.addButton("Switch Asset", - QtWidgets.QMessageBox.ActionRole) - switch_btn.clicked.connect(lambda: self.show_switch_dialog(items)) + switch_btn = dialog.addButton( + "Switch Asset", + QtWidgets.QMessageBox.ActionRole + ) + switch_btn.clicked.connect(lambda: self._show_switch_dialog(items)) dialog.addButton(QtWidgets.QMessageBox.Cancel) - msg = "Version update to '{}' ".format(version_str) + \ - "failed as representation doesn't exist.\n\n" \ - "Please update to version with a valid " \ - "representation OR \n use 'Switch Asset' button " \ - "to change asset." + msg = ( + "Version update to '{}' failed as representation doesn't exist." + "\n\nPlease update to version with a valid representation" + " OR \n use 'Switch Asset' button to change asset." + ).format(version_str) dialog.setText(msg) dialog.exec_() diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index ed2b848481..18d2c971d8 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -12,7 +12,7 @@ from openpype import style from .proxy import FilterProxyModel from .model import InventoryModel -from .view import View +from .view import SceneInvetoryView module = sys.modules[__name__] @@ -66,9 +66,16 @@ class SceneInventoryWindow(QtWidgets.QDialog): proxy.setDynamicSortFilter(True) proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - view = View(self) + view = SceneInvetoryView(self) view.setModel(proxy) + # set some nice default widths for the view + view.setColumnWidth(0, 250) # name + view.setColumnWidth(1, 55) # version + view.setColumnWidth(2, 55) # count + view.setColumnWidth(3, 150) # family + view.setColumnWidth(4, 100) # namespace + # apply delegates version_delegate = VersionDelegate(io, self) column = model.Columns.index("version") @@ -78,32 +85,21 @@ class SceneInventoryWindow(QtWidgets.QDialog): layout.addLayout(control_layout) layout.addWidget(view) + # signals + text_filter.textChanged.connect(proxy.setFilterRegExp) + outdated_only.stateChanged.connect(proxy.set_filter_outdated) + refresh_button.clicked.connect(self.refresh) + view.data_changed.connect(self.refresh) + view.hierarchy_view.connect(model.set_hierarchy_view) + view.hierarchy_view.connect(proxy.set_hierarchy_view) + self.filter = text_filter self.outdated_only = outdated_only self.view = view self.refresh_button = refresh_button self.model = model self.proxy = proxy - - # signals - text_filter.textChanged.connect(self.proxy.setFilterRegExp) - outdated_only.stateChanged.connect(self.proxy.set_filter_outdated) - refresh_button.clicked.connect(self.refresh) - view.data_changed.connect(self.refresh) - view.hierarchy_view.connect(self.model.set_hierarchy_view) - view.hierarchy_view.connect(self.proxy.set_hierarchy_view) - - self.data = { - "delegates": { - "version": version_delegate - } - } - # set some nice default widths for the view - self.view.setColumnWidth(0, 250) # name - self.view.setColumnWidth(1, 55) # version - self.view.setColumnWidth(2, 55) # count - self.view.setColumnWidth(3, 150) # family - self.view.setColumnWidth(4, 100) # namespace + self._version_delegate = version_delegate self.family_config_cache.refresh() From 6c75aa2fd765924e592ebcc28e4c0a8cee9ac35d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 10:38:26 +0100 Subject: [PATCH 39/60] moved sync server lib function to scene inventory lib --- openpype/tools/sceneinventory/lib.py | 74 ++++++++++++++++++++++++++ openpype/tools/sceneinventory/model.py | 23 ++++---- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/openpype/tools/sceneinventory/lib.py b/openpype/tools/sceneinventory/lib.py index 0ac7622d65..7653e1da89 100644 --- a/openpype/tools/sceneinventory/lib.py +++ b/openpype/tools/sceneinventory/lib.py @@ -1,3 +1,9 @@ +import os +from openpype_modules import sync_server + +from Qt import QtGui + + def walk_hierarchy(node): """Recursively yield group node.""" for child in node.children(): @@ -6,3 +12,71 @@ def walk_hierarchy(node): for _child in walk_hierarchy(child): yield _child + + +def get_site_icons(): + resource_path = os.path.join( + os.path.dirname(sync_server.sync_server_module.__file__), + "providers", + "resources" + ) + icons = {} + # TODO get from sync module + for provider in ["studio", "local_drive", "gdrive"]: + pix_url = "{}/{}.png".format(resource_path, provider) + icons[provider] = QtGui.QIcon(pix_url) + + return icons + + +def get_progress_for_repre(repre_doc, active_site, remote_site): + """ + Calculates average progress for representation. + + If site has created_dt >> fully available >> progress == 1 + + Could be calculated in aggregate if it would be too slow + Args: + repre_doc(dict): representation dict + Returns: + (dict) with active and remote sites progress + {'studio': 1.0, 'gdrive': -1} - gdrive site is not present + -1 is used to highlight the site should be added + {'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not + uploaded yet + """ + progress = {active_site: -1, remote_site: -1} + if not repre_doc: + return progress + + files = {active_site: 0, remote_site: 0} + doc_files = repre_doc.get("files") or [] + for doc_file in doc_files: + if not isinstance(doc_file, dict): + continue + + sites = doc_file.get("sites") or [] + for site in sites: + if ( + # Pype 2 compatibility + not isinstance(site, dict) + # Check if site name is one of progress sites + or site["name"] not in progress + ): + continue + + files[site["name"]] += 1 + norm_progress = max(progress[site["name"]], 0) + if site.get("created_dt"): + progress[site["name"]] = norm_progress + 1 + elif site.get("progress"): + progress[site["name"]] = norm_progress + site["progress"] + else: # site exists, might be failed, do not add again + progress[site["name"]] = 0 + + # for example 13 fully avail. files out of 26 >> 13/26 = 0.5 + avg_progress = { + active_site: progress[active_site] / max(files[active_site], 1), + remote_site: progress[remote_site] / max(files[remote_site], 1) + } + return avg_progress diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index bf7b296703..5962802c30 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -6,11 +6,14 @@ from Qt import QtCore, QtGui from avalon import api, io, style, schema from avalon.vendor import qtawesome -from avalon.tools import lib as tools_lib from avalon.lib import HeroVersionType from avalon.tools.models import TreeModel, Item -from . import lib +from .lib import ( + get_site_icons, + walk_hierarchy, + get_progress_for_repre +) from openpype.modules import ModulesManager @@ -37,7 +40,7 @@ class InventoryModel(TreeModel): manager = ModulesManager() sync_server = manager.modules_by_name["sync_server"] self.sync_enabled = sync_server.enabled - self._icons = {} + self._site_icons = {} self.active_site = self.remote_site = None self.active_provider = self.remote_provider = None @@ -66,11 +69,11 @@ class InventoryModel(TreeModel): self.active_provider = active_provider self.remote_site = remote_site self.remote_provider = remote_provider - self._icons = tools_lib.get_repre_icons() + self._site_icons = get_site_icons() if "active_site" not in self.Columns: self.Columns.append("active_site") if "remote_site" not in self.Columns: - self.Columns.extend("remote_site") + self.Columns.append("remote_site") def outdated(self, item): value = item.get("version") @@ -106,7 +109,7 @@ class InventoryModel(TreeModel): if self._hierarchy_view: # If current group is not outdated, check if any # outdated children. - for _node in lib.walk_hierarchy(item): + for _node in walk_hierarchy(item): if self.outdated(_node): return self.CHILD_OUTDATED_COLOR else: @@ -114,7 +117,7 @@ class InventoryModel(TreeModel): if self._hierarchy_view: # Although this is not a group item, we still need # to distinguish which one contain outdated child. - for _node in lib.walk_hierarchy(item): + for _node in walk_hierarchy(item): if self.outdated(_node): return self.CHILD_OUTDATED_COLOR.darker(150) @@ -142,9 +145,9 @@ class InventoryModel(TreeModel): if item.get("isGroupNode"): column_name = self.Columns[index.column()] if column_name == 'active_site': - return self._icons.get(item.get('active_site_provider')) + return self._site_icons.get(item.get('active_site_provider')) if column_name == 'remote_site': - return self._icons.get(item.get('remote_site_provider')) + return self._site_icons.get(item.get('remote_site_provider')) if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"): column_name = self.Columns[index.column()] @@ -395,7 +398,7 @@ class InventoryModel(TreeModel): group_node["isGroupNode"] = True if self.sync_enabled: - progress = tools_lib.get_progress_for_repre( + progress = get_progress_for_repre( representation, self.active_site, self.remote_site ) group_node["active_site"] = self.active_site From 0c7a0a04c40333db08f531ae56639dd8c7d38075 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 10:48:07 +0100 Subject: [PATCH 40/60] removed avalon tools import --- openpype/tools/sceneinventory/window.py | 31 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 18d2c971d8..3583624a4a 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -12,6 +12,11 @@ from openpype import style from .proxy import FilterProxyModel from .model import InventoryModel +from openpype.tools.utils.lib import ( + qt_app_context, + preserve_expanded_rows, + preserve_selection +) from .view import SceneInvetoryView @@ -95,7 +100,7 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.filter = text_filter self.outdated_only = outdated_only - self.view = view + self._view = view self.refresh_button = refresh_button self.model = model self.proxy = proxy @@ -122,16 +127,20 @@ class SceneInventoryWindow(QtWidgets.QDialog): """ def refresh(self, items=None): - with tools_lib.preserve_expanded_rows(tree_view=self.view, - role=self.model.UniqueRole): - with tools_lib.preserve_selection(tree_view=self.view, - role=self.model.UniqueRole, - current_index=False): + with preserve_expanded_rows( + tree_view=self._view, + role=self.model.UniqueRole + ): + with preserve_selection( + tree_view=self._view, + role=self.model.UniqueRole, + current_index=False + ): + kwargs = {"items": items} if self.view._hierarchy_view: - self.model.refresh(selected=self.view._selected, - items=items) - else: - self.model.refresh(items=items) + # TODO do not touch view's inner attribute + kwargs["selected"] = self.view._selected + self.model.refresh(**kwargs) def show(root=None, debug=False, parent=None, items=None): @@ -166,7 +175,7 @@ def show(root=None, debug=False, parent=None, items=None): else: api.Session["AVALON_PROJECT"] = os.environ.get("AVALON_PROJECT") - with tools_lib.application(): + with qt_app_context(): window = SceneInventoryWindow(parent) window.show() window.refresh(items=items) From fadfcacc364de5fd1048689596deed900c38bd14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 10:48:18 +0100 Subject: [PATCH 41/60] renamed checkbox variable --- openpype/tools/sceneinventory/window.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 3583624a4a..e9cbfa6670 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -48,9 +48,11 @@ class SceneInventoryWindow(QtWidgets.QDialog): filter_label = QtWidgets.QLabel("Search", self) text_filter = QtWidgets.QLineEdit(self) - outdated_only = QtWidgets.QCheckBox("Filter to outdated", self) - outdated_only.setToolTip("Show outdated files only") - outdated_only.setChecked(False) + outdated_only_checkbox = QtWidgets.QCheckBox( + "Filter to outdated", self + ) + outdated_only_checkbox.setToolTip("Show outdated files only") + outdated_only_checkbox.setChecked(False) icon = qtawesome.icon("fa.refresh", color="white") refresh_button = QtWidgets.QPushButton(self) @@ -59,7 +61,7 @@ class SceneInventoryWindow(QtWidgets.QDialog): control_layout = QtWidgets.QHBoxLayout() control_layout.addWidget(filter_label) control_layout.addWidget(text_filter) - control_layout.addWidget(outdated_only) + control_layout.addWidget(outdated_only_checkbox) control_layout.addWidget(refresh_button) # endregion control @@ -92,14 +94,12 @@ class SceneInventoryWindow(QtWidgets.QDialog): # signals text_filter.textChanged.connect(proxy.setFilterRegExp) - outdated_only.stateChanged.connect(proxy.set_filter_outdated) + outdated_only_checkbox.stateChanged.connect(proxy.set_filter_outdated) refresh_button.clicked.connect(self.refresh) view.data_changed.connect(self.refresh) view.hierarchy_view.connect(model.set_hierarchy_view) view.hierarchy_view.connect(proxy.set_hierarchy_view) - self.filter = text_filter - self.outdated_only = outdated_only self._view = view self.refresh_button = refresh_button self.model = model From 3a3e83e58976be38a5625f5277a426a3027d3642 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 10:48:32 +0100 Subject: [PATCH 42/60] moved proxy into model.py --- openpype/tools/sceneinventory/model.py | 148 ++++++++++++++++++++++- openpype/tools/sceneinventory/proxy.py | 150 ------------------------ openpype/tools/sceneinventory/window.py | 8 +- 3 files changed, 151 insertions(+), 155 deletions(-) delete mode 100644 openpype/tools/sceneinventory/proxy.py diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 5962802c30..3a4e5d5a4b 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -1,3 +1,4 @@ +import re import logging from collections import defaultdict @@ -346,10 +347,10 @@ class InventoryModel(TreeModel): self.add_child(group_node, parent=parent) - for items in group_items: + for _group_items in group_items: item_node = Item() item_node["Name"] = ", ".join( - [item["objectName"] for item in items] + [item["objectName"] for item in _group_items] ) self.add_child(item_node, parent=group_node) @@ -427,3 +428,146 @@ class InventoryModel(TreeModel): self.endResetModel() return self._root_item + + +class FilterProxyModel(QtCore.QSortFilterProxyModel): + """Filter model to where key column's value is in the filtered tags""" + + def __init__(self, *args, **kwargs): + super(FilterProxyModel, self).__init__(*args, **kwargs) + self._filter_outdated = False + self._hierarchy_view = False + + def filterAcceptsRow(self, row, parent): + model = self.sourceModel() + source_index = model.index(row, self.filterKeyColumn(), parent) + + # Always allow bottom entries (individual containers), since their + # parent group hidden if it wouldn't have been validated. + rows = model.rowCount(source_index) + if not rows: + return True + + # Filter by regex + if not self.filterRegExp().isEmpty(): + pattern = re.escape(self.filterRegExp().pattern()) + + if not self._matches(row, parent, pattern): + return False + + if self._filter_outdated: + # When filtering to outdated we filter the up to date entries + # thus we "allow" them when they are outdated + if not self._is_outdated(row, parent): + return False + + return True + + def set_filter_outdated(self, state): + """Set whether to show the outdated entries only.""" + state = bool(state) + + if state != self._filter_outdated: + self._filter_outdated = bool(state) + self.invalidateFilter() + + def set_hierarchy_view(self, state): + state = bool(state) + + if state != self._hierarchy_view: + self._hierarchy_view = state + + def _is_outdated(self, row, parent): + """Return whether row is outdated. + + A row is considered outdated if it has "version" and "highest_version" + data and in the internal data structure, and they are not of an + equal value. + + """ + def outdated(node): + version = node.get("version", None) + highest = node.get("highest_version", None) + + # Always allow indices that have no version data at all + if version is None and highest is None: + return True + + # If either a version or highest is present but not the other + # consider the item invalid. + if not self._hierarchy_view: + # Skip this check if in hierarchy view, or the child item + # node will be hidden even it's actually outdated. + if version is None or highest is None: + return False + return version != highest + + index = self.sourceModel().index(row, self.filterKeyColumn(), parent) + + # The scene contents are grouped by "representation", e.g. the same + # "representation" loaded twice is grouped under the same header. + # Since the version check filters these parent groups we skip that + # check for the individual children. + has_parent = index.parent().isValid() + if has_parent and not self._hierarchy_view: + return True + + # Filter to those that have the different version numbers + node = index.internalPointer() + if outdated(node): + return True + + if self._hierarchy_view: + for _node in walk_hierarchy(node): + if outdated(_node): + return True + + return False + + def _matches(self, row, parent, pattern): + """Return whether row matches regex pattern. + + Args: + row (int): row number in model + parent (QtCore.QModelIndex): parent index + pattern (regex.pattern): pattern to check for in key + + Returns: + bool + + """ + model = self.sourceModel() + column = self.filterKeyColumn() + role = self.filterRole() + + def matches(row, parent, pattern): + index = model.index(row, column, parent) + key = model.data(index, role) + if re.search(pattern, key, re.IGNORECASE): + return True + + if matches(row, parent, pattern): + return True + + # Also allow if any of the children matches + source_index = model.index(row, column, parent) + rows = model.rowCount(source_index) + + if any( + matches(idx, source_index, pattern) + for idx in range(rows) + ): + return True + + if not self._hierarchy_view: + return False + + for idx in range(rows): + child_index = model.index(idx, column, source_index) + child_rows = model.rowCount(child_index) + return any( + self._matches(child_idx, child_index, pattern) + for child_idx in range(child_rows) + ) + + return True diff --git a/openpype/tools/sceneinventory/proxy.py b/openpype/tools/sceneinventory/proxy.py deleted file mode 100644 index 3c4295c446..0000000000 --- a/openpype/tools/sceneinventory/proxy.py +++ /dev/null @@ -1,150 +0,0 @@ -import re - -from Qt import QtCore - -from . import lib - - -class FilterProxyModel(QtCore.QSortFilterProxyModel): - """Filter model to where key column's value is in the filtered tags""" - - def __init__(self, *args, **kwargs): - super(FilterProxyModel, self).__init__(*args, **kwargs) - self._filter_outdated = False - self._hierarchy_view = False - - def filterAcceptsRow(self, row, parent): - model = self.sourceModel() - source_index = model.index(row, self.filterKeyColumn(), parent) - - # Always allow bottom entries (individual containers), since their - # parent group hidden if it wouldn't have been validated. - rows = model.rowCount(source_index) - if not rows: - return True - - # Filter by regex - if not self.filterRegExp().isEmpty(): - pattern = re.escape(self.filterRegExp().pattern()) - - if not self._matches(row, parent, pattern): - return False - - if self._filter_outdated: - # When filtering to outdated we filter the up to date entries - # thus we "allow" them when they are outdated - if not self._is_outdated(row, parent): - return False - - return True - - def set_filter_outdated(self, state): - """Set whether to show the outdated entries only.""" - state = bool(state) - - if state != self._filter_outdated: - self._filter_outdated = bool(state) - self.invalidateFilter() - - def set_hierarchy_view(self, state): - state = bool(state) - - if state != self._hierarchy_view: - self._hierarchy_view = state - - def _is_outdated(self, row, parent): - """Return whether row is outdated. - - A row is considered outdated if it has "version" and "highest_version" - data and in the internal data structure, and they are not of an - equal value. - - """ - def outdated(node): - version = node.get("version", None) - highest = node.get("highest_version", None) - - # Always allow indices that have no version data at all - if version is None and highest is None: - return True - - # If either a version or highest is present but not the other - # consider the item invalid. - if not self._hierarchy_view: - # Skip this check if in hierarchy view, or the child item - # node will be hidden even it's actually outdated. - if version is None or highest is None: - return False - return version != highest - - index = self.sourceModel().index(row, self.filterKeyColumn(), parent) - - # The scene contents are grouped by "representation", e.g. the same - # "representation" loaded twice is grouped under the same header. - # Since the version check filters these parent groups we skip that - # check for the individual children. - has_parent = index.parent().isValid() - if has_parent and not self._hierarchy_view: - return True - - # Filter to those that have the different version numbers - node = index.internalPointer() - is_outdated = outdated(node) - - if is_outdated: - return True - - if self._hierarchy_view: - for _node in lib.walk_hierarchy(node): - if outdated(_node): - return True - - return False - - def _matches(self, row, parent, pattern): - """Return whether row matches regex pattern. - - Args: - row (int): row number in model - parent (QtCore.QModelIndex): parent index - pattern (regex.pattern): pattern to check for in key - - Returns: - bool - - """ - model = self.sourceModel() - column = self.filterKeyColumn() - role = self.filterRole() - - def matches(row, parent, pattern): - index = model.index(row, column, parent) - key = model.data(index, role) - if re.search(pattern, key, re.IGNORECASE): - return True - - if matches(row, parent, pattern): - return True - - # Also allow if any of the children matches - source_index = model.index(row, column, parent) - rows = model.rowCount(source_index) - - if any( - matches(idx, source_index, pattern) - for idx in range(rows) - ): - return True - - if not self._hierarchy_view: - return False - - for i in range(rows): - child_i = model.index(i, column, source_index) - child_rows = model.rowCount(child_i) - return any( - self._matches(ch_i, child_i, pattern) - for ch_i in range(child_rows) - ) - - return True diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index e9cbfa6670..35ff2b5a55 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -9,14 +9,16 @@ from avalon.tools import lib as tools_lib from avalon.tools.delegates import VersionDelegate from openpype import style - -from .proxy import FilterProxyModel -from .model import InventoryModel from openpype.tools.utils.lib import ( qt_app_context, preserve_expanded_rows, preserve_selection ) + +from .model import ( + InventoryModel, + FilterProxyModel +) from .view import SceneInvetoryView From 280560652d4e9cae6e9fb2985da5d8bf5651e1aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 11:07:33 +0100 Subject: [PATCH 43/60] more cleanup in code and imports --- .../tools/sceneinventory/switch_dialog.py | 24 +++---- openpype/tools/sceneinventory/view.py | 19 +++-- openpype/tools/sceneinventory/window.py | 72 +++++++++++-------- 3 files changed, 67 insertions(+), 48 deletions(-) diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index 37659b2370..f539294ded 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -1,11 +1,14 @@ import collections +import logging from Qt import QtWidgets, QtCore -from avalon import io, api, style +from avalon import io, api from avalon.vendor import qtawesome from .widgets import SearchComboBox +log = logging.getLogger("SwitchAssetDialog") + class ValidationState: def __init__(self): @@ -969,19 +972,16 @@ class SwitchAssetDialog(QtWidgets.QDialog): try: api.switch(container, repre_doc) except Exception: - log.warning( - ( - "Couldn't switch asset." - "See traceback for more information." - ), - exc_info=True + msg = ( + "Couldn't switch asset." + "See traceback for more information." ) - dialog = QtWidgets.QMessageBox() - dialog.setStyleSheet(style.load_stylesheet()) + log.warning(msg, exc_info=True) + dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle("Switch asset failed") - msg = "Switch asset failed. "\ - "Search console log for more details" - dialog.setText(msg) + dialog.setText( + "Switch asset failed. Search console log for more details" + ) dialog.exec_() self.switched.emit() diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 08d5499355..80f26a881d 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -22,7 +22,7 @@ log = logging.getLogger("SceneInventory") class SceneInvetoryView(QtWidgets.QTreeView): data_changed = QtCore.Signal() - hierarchy_view = QtCore.Signal(bool) + hierarchy_view_changed = QtCore.Signal(bool) def __init__(self, parent=None): super(SceneInvetoryView, self).__init__(parent=parent) @@ -41,10 +41,15 @@ class SceneInvetoryView(QtWidgets.QTreeView): self.sync_server = manager.modules_by_name["sync_server"] self.sync_enabled = self.sync_server.enabled + def _set_hierarchy_view(self, enabled): + if enabled == self._hierarchy_view: + return + self._hierarchy_view = enabled + self.hierarchy_view_changed.emit(enabled) + def _enter_hierarchy(self, items): self._selected = set(i["objectName"] for i in items) - self._hierarchy_view = True - self.hierarchy_view.emit(True) + self._set_hierarchy_view(True) self.data_changed.emit() self.expandToDepth(1) self.setStyleSheet(""" @@ -54,8 +59,7 @@ class SceneInvetoryView(QtWidgets.QTreeView): """) def _leave_hierarchy(self): - self._hierarchy_view = False - self.hierarchy_view.emit(False) + self._set_hierarchy_view(False) self.data_changed.emit() self.setStyleSheet("QTreeView {}") @@ -189,8 +193,9 @@ class SceneInvetoryView(QtWidgets.QTreeView): try: api.update(item, version_name) except AssertionError: - self._show_version_error_dialog(version_name, - [item]) + self._show_version_error_dialog( + version_name, [item] + ) log.warning("Update failed", exc_info=True) self.data_changed.emit() diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 35ff2b5a55..e71af6a93d 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -5,14 +5,13 @@ from Qt import QtWidgets, QtCore from avalon.vendor import qtawesome from avalon import io, api -from avalon.tools import lib as tools_lib -from avalon.tools.delegates import VersionDelegate - from openpype import style +from openpype.tools.utils.delegates import VersionDelegate from openpype.tools.utils.lib import ( qt_app_context, preserve_expanded_rows, - preserve_selection + preserve_selection, + FamilyConfigCache ) from .model import ( @@ -37,14 +36,13 @@ class SceneInventoryWindow(QtWidgets.QDialog): self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint ) - self.resize(1100, 480) - self.setWindowTitle( - "Scene Inventory 1.0 - {}".format( - os.getenv("AVALON_PROJECT") or "" - ) - ) + project_name = os.getenv("AVALON_PROJECT") or "" + self.setWindowTitle("Scene Inventory 1.0 - {}".format(project_name)) self.setObjectName("SceneInventory") - self.setProperty("saveWindowPref", True) # Maya only property! + # Maya only property + self.setProperty("saveWindowPref", True) + + self.resize(1100, 480) # region control filter_label = QtWidgets.QLabel("Search", self) @@ -67,9 +65,9 @@ class SceneInventoryWindow(QtWidgets.QDialog): control_layout.addWidget(refresh_button) # endregion control - self.family_config_cache = tools_lib.global_family_cache() + family_config_cache = FamilyConfigCache(io) - model = InventoryModel(self.family_config_cache) + model = InventoryModel(family_config_cache) proxy = FilterProxyModel() proxy.setSourceModel(model) proxy.setDynamicSortFilter(True) @@ -95,23 +93,27 @@ class SceneInventoryWindow(QtWidgets.QDialog): layout.addWidget(view) # signals - text_filter.textChanged.connect(proxy.setFilterRegExp) - outdated_only_checkbox.stateChanged.connect(proxy.set_filter_outdated) - refresh_button.clicked.connect(self.refresh) + text_filter.textChanged.connect(self._on_text_filter_change) + outdated_only_checkbox.stateChanged.connect( + self._on_outdated_state_change + ) + view.hierarchy_view_changed.connect( + self._on_hiearchy_view_change + ) view.data_changed.connect(self.refresh) - view.hierarchy_view.connect(model.set_hierarchy_view) - view.hierarchy_view.connect(proxy.set_hierarchy_view) + refresh_button.clicked.connect(self.refresh) + self._outdated_only_checkbox = outdated_only_checkbox self._view = view - self.refresh_button = refresh_button - self.model = model - self.proxy = proxy + self._model = model + self._proxy = proxy self._version_delegate = version_delegate - - self.family_config_cache.refresh() + self._family_config_cache = family_config_cache self._first_show = True + family_config_cache.refresh() + def showEvent(self, event): super(SceneInventoryWindow, self).showEvent(event) if self._first_show: @@ -131,18 +133,30 @@ class SceneInventoryWindow(QtWidgets.QDialog): def refresh(self, items=None): with preserve_expanded_rows( tree_view=self._view, - role=self.model.UniqueRole + role=self._model.UniqueRole ): with preserve_selection( tree_view=self._view, - role=self.model.UniqueRole, + role=self._model.UniqueRole, current_index=False ): kwargs = {"items": items} - if self.view._hierarchy_view: - # TODO do not touch view's inner attribute - kwargs["selected"] = self.view._selected - self.model.refresh(**kwargs) + # TODO do not touch view's inner attribute + if self._view._hierarchy_view: + kwargs["selected"] = self._view._selected + self._model.refresh(**kwargs) + + def _on_hiearchy_view_change(self, enabled): + self._proxy.set_hierarchy_view(enabled) + self._model.set_hierarchy_view(enabled) + + def _on_text_filter_change(self, text_filter): + self._proxy.setFilterRegExp(text_filter) + + def _on_outdated_state_change(self): + self._proxy.set_filter_outdated( + self._outdated_only_checkbox.isChecked() + ) def show(root=None, debug=False, parent=None, items=None): From 424876f0cb8238c2523f302aa5d67e28fea57aa9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Nov 2021 11:24:07 +0100 Subject: [PATCH 44/60] Refactor - better layout of aggregate command --- openpype/tools/loader/model.py | 72 +++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 74768dfa25..96a52fce97 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -801,47 +801,63 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): {"$unwind": "$files"}, {'$addFields': { 'order_local': { - '$filter': {'input': '$files.sites', 'as': 'p', - 'cond': {'$eq': ['$$p.name', active_site]} - }} + '$filter': { + 'input': '$files.sites', 'as': 'p', + 'cond': {'$eq': ['$$p.name', active_site]} + } + } }}, {'$addFields': { 'order_remote': { - '$filter': {'input': '$files.sites', 'as': 'p', - 'cond': {'$eq': ['$$p.name', remote_site]} - }} + '$filter': { + 'input': '$files.sites', 'as': 'p', + 'cond': {'$eq': ['$$p.name', remote_site]} + } + } }}, {'$addFields': { 'progress_local': {"$arrayElemAt": [{ - '$cond': [{'$size': "$order_local.progress"}, - "$order_local.progress", - # if exists created_dt count is as available - {'$cond': [ - {'$size': "$order_local.created_dt"}, - [1], - [0] - ]} - ]}, 0]} + '$cond': [ + {'$size': "$order_local.progress"}, + "$order_local.progress", + # if exists created_dt count is as available + {'$cond': [ + {'$size': "$order_local.created_dt"}, + [1], + [0] + ]} + ]}, + 0 + ]} }}, {'$addFields': { 'progress_remote': {"$arrayElemAt": [{ - '$cond': [{'$size': "$order_remote.progress"}, - "$order_remote.progress", - # if exists created_dt count is as available - {'$cond': [ - {'$size': "$order_remote.created_dt"}, - [1], - [0] - ]} - ]}, 0]} + '$cond': [ + {'$size': "$order_remote.progress"}, + "$order_remote.progress", + # if exists created_dt count is as available + {'$cond': [ + {'$size': "$order_remote.created_dt"}, + [1], + [0] + ]} + ]}, + 0 + ]} }}, {'$group': { # first group by repre '_id': '$_id', 'parent': {'$first': '$parent'}, - 'avail_ratio_local': {'$first': { - '$divide': [{'$sum': "$progress_local"}, {'$sum': 1}]}}, - 'avail_ratio_remote': {'$first': { - '$divide': [{'$sum': "$progress_remote"}, {'$sum': 1}]}} + 'avail_ratio_local': { + '$first': { + '$divide': [{'$sum': "$progress_local"}, {'$sum': 1}] + } + }, + 'avail_ratio_remote': { + '$first': { + '$divide': [{'$sum': "$progress_remote"}, {'$sum': 1}] + } + } }}, {'$group': { # second group by parent, eg version_id '_id': '$parent', From c30e7e083848048435bb540390a03b51aa660f6d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 16 Nov 2021 11:30:56 +0100 Subject: [PATCH 45/60] OP-2003 - added 0.0 when batch is in progress FE is expecting 'progress' key --- openpype/lib/remote_publish.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index f7d7955b79..d7db4d1ab9 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -32,7 +32,8 @@ def start_webpublish_log(dbcon, batch_id, user): "batch_id": batch_id, "start_date": datetime.now(), "user": user, - "status": "in_progress" + "status": "in_progress", + "progress": 0.0 }).inserted_id From 797d5d2d9af88b40002c726e6d3855cd47cf2d35 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 11:53:37 +0100 Subject: [PATCH 46/60] set stretch of switch dialog layout --- openpype/tools/sceneinventory/switch_dialog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index f539294ded..ecad8eac0a 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -71,6 +71,10 @@ class SwitchAssetDialog(QtWidgets.QDialog): main_layout.addWidget(repre_label, 2, 2) # Btn column main_layout.addWidget(accept_btn, 1, 3) + main_layout.setColumnStretch(0, 1) + main_layout.setColumnStretch(1, 1) + main_layout.setColumnStretch(2, 1) + main_layout.setColumnStretch(3, 0) assets_combox.currentIndexChanged.connect( self._combobox_value_changed From 215bfd7c47d62504f0fe9b0d26cdecab7b338133 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 12:03:59 +0100 Subject: [PATCH 47/60] fixed too long line --- openpype/tools/sceneinventory/model.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 3a4e5d5a4b..d2b7f8b70f 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -145,10 +145,13 @@ class InventoryModel(TreeModel): if item.get("isGroupNode"): column_name = self.Columns[index.column()] - if column_name == 'active_site': - return self._site_icons.get(item.get('active_site_provider')) - if column_name == 'remote_site': - return self._site_icons.get(item.get('remote_site_provider')) + if column_name == "active_site": + provider = item.get("active_site_provider") + return self._site_icons.get(provider) + + if column_name == "remote_site": + provider = item.get("remote_site_provider") + return self._site_icons.get(provider) if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"): column_name = self.Columns[index.column()] From d6c608199d871fe48f8c3820b2ea8e0cd7dc57e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 12:04:59 +0100 Subject: [PATCH 48/60] fixed too long line --- openpype/tools/utils/host_tools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 8011410ce9..e87da7f0b4 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -156,7 +156,9 @@ class HostToolsHelper: if self._scene_inventory_tool is None: from openpype.tools.sceneinventory import SceneInventoryWindow - scene_inventory_window = SceneInventoryWindow(parent=parent or self._parent) + scene_inventory_window = SceneInventoryWindow( + parent=parent or self._parent + ) self._scene_inventory_tool = scene_inventory_window return self._scene_inventory_tool From 59de91596f547824348161d5b149eec6db80cac2 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 16 Nov 2021 11:21:47 +0000 Subject: [PATCH 49/60] [Automated] Bump version --- CHANGELOG.md | 21 ++++++++++++++------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb0ee845ca..deadd59945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ # Changelog +## [3.6.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.0...HEAD) + +**🐛 Bug fixes** + +- Loader doesn't allow changing of version before loading [\#2254](https://github.com/pypeclub/OpenPype/pull/2254) + ## [3.6.0](https://github.com/pypeclub/OpenPype/tree/3.6.0) (2021-11-15) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...3.6.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.0-nightly.6...3.6.0) + +### 📖 Documentation + +- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) +- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) **🆕 New features** @@ -24,7 +37,6 @@ - Maya : Validate shape zero [\#2212](https://github.com/pypeclub/OpenPype/pull/2212) - Maya : validate unique names [\#2211](https://github.com/pypeclub/OpenPype/pull/2211) - Tools: OpenPype stylesheet in workfiles tool [\#2208](https://github.com/pypeclub/OpenPype/pull/2208) -- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) - Ftrack: Replace Queue with deque in event handlers logic [\#2204](https://github.com/pypeclub/OpenPype/pull/2204) - Tools: New select context dialog [\#2200](https://github.com/pypeclub/OpenPype/pull/2200) - Maya : Validate mesh ngons [\#2199](https://github.com/pypeclub/OpenPype/pull/2199) @@ -53,10 +65,6 @@ - Maya: review viewport settings [\#2177](https://github.com/pypeclub/OpenPype/pull/2177) - Maya: Aspect ratio [\#2174](https://github.com/pypeclub/OpenPype/pull/2174) -### 📖 Documentation - -- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) - ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0) @@ -80,7 +88,6 @@ **🐛 Bug fixes** -- Maya: Fix hotbox broken by scriptsmenu [\#2151](https://github.com/pypeclub/OpenPype/pull/2151) - Maya: fix model publishing [\#2130](https://github.com/pypeclub/OpenPype/pull/2130) - Fix - oiiotool wasn't recognized even if present [\#2129](https://github.com/pypeclub/OpenPype/pull/2129) - General: Disk mapping group [\#2120](https://github.com/pypeclub/OpenPype/pull/2120) diff --git a/openpype/version.py b/openpype/version.py index 122137e6cd..f414424cde 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.6.0" +__version__ = "3.6.1-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index dfc11b9881..7d3e3a7e19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.6.0" # OpenPype +version = "3.6.1-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 06b04f29efeaf133da384524d82f64349a2e9cc3 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 16 Nov 2021 13:04:11 +0000 Subject: [PATCH 50/60] [Automated] Release --- CHANGELOG.md | 22 ++++++++-------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index deadd59945..2ed4b10a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.6.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.0...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.0...3.6.1) **🐛 Bug fixes** @@ -12,11 +12,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.0-nightly.6...3.6.0) -### 📖 Documentation - -- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) -- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) - **🆕 New features** - Add validate active site button to sync queue on a project [\#2176](https://github.com/pypeclub/OpenPype/pull/2176) @@ -37,6 +32,7 @@ - Maya : Validate shape zero [\#2212](https://github.com/pypeclub/OpenPype/pull/2212) - Maya : validate unique names [\#2211](https://github.com/pypeclub/OpenPype/pull/2211) - Tools: OpenPype stylesheet in workfiles tool [\#2208](https://github.com/pypeclub/OpenPype/pull/2208) +- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) - Ftrack: Replace Queue with deque in event handlers logic [\#2204](https://github.com/pypeclub/OpenPype/pull/2204) - Tools: New select context dialog [\#2200](https://github.com/pypeclub/OpenPype/pull/2200) - Maya : Validate mesh ngons [\#2199](https://github.com/pypeclub/OpenPype/pull/2199) @@ -46,6 +42,7 @@ - Usage of tools code [\#2185](https://github.com/pypeclub/OpenPype/pull/2185) - Settings: Dictionary based on project roots [\#2184](https://github.com/pypeclub/OpenPype/pull/2184) - Subset name: Be able to pass asset document to get subset name [\#2179](https://github.com/pypeclub/OpenPype/pull/2179) +- Loader: Refactor and use OpenPype stylesheets [\#2166](https://github.com/pypeclub/OpenPype/pull/2166) **🐛 Bug fixes** @@ -65,14 +62,14 @@ - Maya: review viewport settings [\#2177](https://github.com/pypeclub/OpenPype/pull/2177) - Maya: Aspect ratio [\#2174](https://github.com/pypeclub/OpenPype/pull/2174) +### 📖 Documentation + +- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) + ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0) -**Deprecated:** - -- Maya: Change mayaAscii family to mayaScene [\#2106](https://github.com/pypeclub/OpenPype/pull/2106) - **🆕 New features** - Added project and task into context change message in Maya [\#2131](https://github.com/pypeclub/OpenPype/pull/2131) @@ -84,7 +81,6 @@ - Maya: make rig validators configurable in settings [\#2137](https://github.com/pypeclub/OpenPype/pull/2137) - Settings: Updated readme for entity types in settings [\#2132](https://github.com/pypeclub/OpenPype/pull/2132) - Nuke: unified clip loader [\#2128](https://github.com/pypeclub/OpenPype/pull/2128) -- Settings UI: Project model refreshing and sorting [\#2104](https://github.com/pypeclub/OpenPype/pull/2104) **🐛 Bug fixes** @@ -93,8 +89,6 @@ - General: Disk mapping group [\#2120](https://github.com/pypeclub/OpenPype/pull/2120) - Hiero: publishing effect first time makes wrong resources path [\#2115](https://github.com/pypeclub/OpenPype/pull/2115) - Add startup script for Houdini Core. [\#2110](https://github.com/pypeclub/OpenPype/pull/2110) -- TVPaint: Behavior name of loop also accept repeat [\#2109](https://github.com/pypeclub/OpenPype/pull/2109) -- Ftrack: Project settings save custom attributes skip unknown attributes [\#2103](https://github.com/pypeclub/OpenPype/pull/2103) ## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23) diff --git a/openpype/version.py b/openpype/version.py index f414424cde..9c6070eca5 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.6.1-nightly.1" +__version__ = "3.6.1" diff --git a/pyproject.toml b/pyproject.toml index 7d3e3a7e19..264aebe988 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.6.1-nightly.1" # OpenPype +version = "3.6.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 4b0e92cfbf3417f66bc3b67c028ac70ae66da626 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 17:41:35 +0100 Subject: [PATCH 51/60] python interpreter does not have corner wiget with + but "Add tab" button --- .../window/widgets.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py index 0e8dd2fb9b..bbc304d680 100644 --- a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py +++ b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py @@ -176,6 +176,7 @@ class PythonCodeEditor(QtWidgets.QPlainTextEdit): class PythonTabWidget(QtWidgets.QWidget): + add_tab_requested = QtCore.Signal() before_execute = QtCore.Signal(str) def __init__(self, parent): @@ -185,11 +186,15 @@ class PythonTabWidget(QtWidgets.QWidget): self.setFocusProxy(code_input) + add_tab_btn = QtWidgets.QPushButton("Add tab", self) + add_tab_btn.setToolTip("Add new tab") + execute_btn = QtWidgets.QPushButton("Execute", self) execute_btn.setToolTip("Execute command (Ctrl + Enter)") btns_layout = QtWidgets.QHBoxLayout() btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(add_tab_btn) btns_layout.addStretch(1) btns_layout.addWidget(execute_btn) @@ -198,12 +203,16 @@ class PythonTabWidget(QtWidgets.QWidget): layout.addWidget(code_input, 1) layout.addLayout(btns_layout, 0) + add_tab_btn.clicked.connect(self._on_add_tab_clicked) execute_btn.clicked.connect(self._on_execute_clicked) code_input.execute_requested.connect(self.execute) self._code_input = code_input self._interpreter = InteractiveInterpreter() + def _on_add_tab_clicked(self): + self.add_tab_requested.emit() + def _on_execute_clicked(self): self.execute() @@ -352,9 +361,6 @@ class PythonInterpreterWidget(QtWidgets.QWidget): tab_widget.setTabsClosable(False) tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - add_tab_btn = QtWidgets.QPushButton("+", tab_widget) - tab_widget.setCornerWidget(add_tab_btn, QtCore.Qt.TopLeftCorner) - widgets_splitter = QtWidgets.QSplitter(self) widgets_splitter.setOrientation(QtCore.Qt.Vertical) widgets_splitter.addWidget(output_widget) @@ -371,14 +377,12 @@ class PythonInterpreterWidget(QtWidgets.QWidget): line_check_timer.setInterval(200) line_check_timer.timeout.connect(self._on_timer_timeout) - add_tab_btn.clicked.connect(self._on_add_clicked) tab_bar.right_clicked.connect(self._on_tab_right_click) tab_bar.double_clicked.connect(self._on_tab_double_click) tab_bar.mid_clicked.connect(self._on_tab_mid_click) tab_widget.tabCloseRequested.connect(self._on_tab_close_req) self._widgets_splitter = widgets_splitter - self._add_tab_btn = add_tab_btn self._output_widget = output_widget self._tab_widget = tab_widget self._line_check_timer = line_check_timer @@ -525,7 +529,7 @@ class PythonInterpreterWidget(QtWidgets.QWidget): lines.append(self.ansi_escape.sub("", line)) self._append_lines(lines) - def _on_add_clicked(self): + def _on_add_requested(self): dialog = TabNameDialog(self) dialog.exec_() tab_name = dialog.result() @@ -562,6 +566,7 @@ class PythonInterpreterWidget(QtWidgets.QWidget): def add_tab(self, tab_name, index=None): widget = PythonTabWidget(self) widget.before_execute.connect(self._on_before_execute) + widget.add_tab_requested.connect(self._on_add_requested) if index is None: if self._tab_widget.count() > 0: index = self._tab_widget.currentIndex() + 1 From c85247fb3a0f20740a1cb2826f268210d91a0d1a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 17:41:52 +0100 Subject: [PATCH 52/60] modified qtab widget style --- openpype/style/data.json | 12 ++++++++-- openpype/style/style.css | 47 ++++++++++++++++------------------------ 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index b92ee61764..977de50be2 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -18,7 +18,6 @@ "green-light": "hsl(155, 80%, 80%)" }, "color": { - "font": "#D3D8DE", "font-hover": "#F0F2F5", "font-disabled": "#99A3B2", @@ -50,7 +49,16 @@ "border": "#373D48", "border-hover": "rgba(168, 175, 189, .3)", - "border-focus": "hsl(200, 60%, 60%)", + "border-focus": "rgb(92, 173, 214)", + + "tab-widget": { + "bg": "#21252B", + "bg-selected": "#434a56", + "bg-hover": "#373D48", + "color": "#99A3B2", + "color-selected": "#F0F2F5", + "color-hover": "#F0F2F5" + }, "loader": { "asset-view": { diff --git a/openpype/style/style.css b/openpype/style/style.css index 89458fd117..519adbbed3 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -325,47 +325,38 @@ QTabWidget::pane { /* move to the right to not mess with borders of widget underneath */ QTabWidget::tab-bar { - left: 2px; + alignment: left; } QTabBar::tab { - padding: 5px; - border-left: 3px solid transparent; border-top: 1px solid {color:border}; + border-left: 1px solid {color:border}; border-right: 1px solid {color:border}; - /* must be single like because of Nuke*/ - background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0,stop: 0.5 {color:bg}, stop: 1.0 {color:bg-inputs}); + padding: 5px; + background: {color:tab-widget:bg}; + color: {color:tab-widget:color}; } QTabBar::tab:selected { - background: {color:grey-lighter}; - border-left: 3px solid {color:border-focus}; - /* must be single like because of Nuke*/ - background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0,stop: 0.5 {color:bg}, stop: 1.0 {color:border}); -} - -QTabBar::tab:!selected { - background: {color:grey-light}; + border-left-color: {color:tab-widget:bg-selected}; + border-right-color: {color:tab-widget:bg-selected}; + border-top-color: {color:border-focus}; + background: {color:tab-widget:bg-selected}; + color: {color:tab-widget:color-selected}; } +QTabBar::tab:!selected {} QTabBar::tab:!selected:hover { - background: {color:grey-lighter}; + background: {color:tab-widget:bg-hover}; + color: {color:tab-widget:color-hover}; } -QTabBar::tab:first { - border-left: 1px solid {color:border}; -} -QTabBar::tab:first:selected { - margin-left: 0; - border-left: 3px solid {color:border-focus}; -} - -QTabBar::tab:last:selected { - margin-right: 0; -} - -QTabBar::tab:only-one { - margin: 0; +QTabBar::tab:first {} +QTabBar::tab:first:selected {} +QTabBar::tab:last:!selected { + border-right: 1px solid {color:border}; } +QTabBar::tab:last:selected {} +QTabBar::tab:only-one {} QHeaderView { border: 0px solid {color:border}; From dd0dd627bf3d549c224b451747c55de0f18a102d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 17:45:29 +0100 Subject: [PATCH 53/60] modified button label --- .../python_console_interpreter/window/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py index bbc304d680..8fea91dd20 100644 --- a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py +++ b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py @@ -186,7 +186,7 @@ class PythonTabWidget(QtWidgets.QWidget): self.setFocusProxy(code_input) - add_tab_btn = QtWidgets.QPushButton("Add tab", self) + add_tab_btn = QtWidgets.QPushButton("Add tab...", self) add_tab_btn.setToolTip("Add new tab") execute_btn = QtWidgets.QPushButton("Execute", self) From b19ba91bea6d14bfef3a07ff6efbb8f26edbfad7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 17:59:41 +0100 Subject: [PATCH 54/60] added more actions to tab context menu --- .../window/widgets.py | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py index 8fea91dd20..1999854ba1 100644 --- a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py +++ b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py @@ -463,14 +463,33 @@ class PythonInterpreterWidget(QtWidgets.QWidget): return menu = QtWidgets.QMenu(self._tab_widget) - menu.addAction("Rename") + add_tab_action = QtWidgets.QAction("Add tab...", menu) + rename_tab_action = QtWidgets.QAction("Rename...", menu) + duplicate_tab_action = QtWidgets.QAction("Duplicate...", menu) + menu.addAction(add_tab_action) + menu.addAction(rename_tab_action) + menu.addAction(duplicate_tab_action) + + close_tab_action = None + if self._tab_widget.tabsClosable(): + close_tab_action = QtWidgets.QAction("Close", menu) + menu.addAction(close_tab_action) result = menu.exec_(global_point) if result is None: return - if result.text() == "Rename": + if result is rename_tab_action: self._rename_tab_req(tab_idx) + elif result is add_tab_action: + self._on_add_requested() + + elif result is duplicate_tab_action: + self._duplicate_requested(tab_idx) + + elif result is close_tab_action: + self._on_tab_close_req(tab_idx) + def _rename_tab_req(self, tab_idx): dialog = TabNameDialog(self) dialog.set_tab_name(self._tab_widget.tabText(tab_idx)) @@ -479,6 +498,16 @@ class PythonInterpreterWidget(QtWidgets.QWidget): if tab_name: self._tab_widget.setTabText(tab_idx, tab_name) + def _duplicate_requested(self, tab_idx=None): + if tab_idx is None: + tab_idx = self._tab_widget.currentIndex() + + src_widget = self._tab_widget.widget(tab_idx) + dst_widget = self._add_tab() + if dst_widget is None: + return + dst_widget.set_code(src_widget.get_code()) + def _on_tab_mid_click(self, global_point): point = self._tab_widget.mapFromGlobal(global_point) tab_bar = self._tab_widget.tabBar() @@ -530,11 +559,16 @@ class PythonInterpreterWidget(QtWidgets.QWidget): self._append_lines(lines) def _on_add_requested(self): + self._add_tab() + + def _add_tab(self): dialog = TabNameDialog(self) dialog.exec_() tab_name = dialog.result() if tab_name: - self.add_tab(tab_name) + return self.add_tab(tab_name) + + return None def _on_before_execute(self, code_text): at_max = self._output_widget.vertical_scroll_at_max() From e090b640001ae4a071f84fdbb92426674e77b67b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 18:07:06 +0100 Subject: [PATCH 55/60] always show close action --- .../python_console_interpreter/window/widgets.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py index 1999854ba1..ecf41eaf3e 100644 --- a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py +++ b/openpype/modules/default_modules/python_console_interpreter/window/widgets.py @@ -463,17 +463,25 @@ class PythonInterpreterWidget(QtWidgets.QWidget): return menu = QtWidgets.QMenu(self._tab_widget) + add_tab_action = QtWidgets.QAction("Add tab...", menu) + add_tab_action.setToolTip("Add new tab") + rename_tab_action = QtWidgets.QAction("Rename...", menu) + rename_tab_action.setToolTip("Rename tab") + duplicate_tab_action = QtWidgets.QAction("Duplicate...", menu) + duplicate_tab_action.setToolTip("Duplicate code to new tab") + + close_tab_action = QtWidgets.QAction("Close", menu) + close_tab_action.setToolTip("Close tab and lose content") + close_tab_action.setEnabled(self._tab_widget.tabsClosable()) + menu.addAction(add_tab_action) menu.addAction(rename_tab_action) menu.addAction(duplicate_tab_action) + menu.addAction(close_tab_action) - close_tab_action = None - if self._tab_widget.tabsClosable(): - close_tab_action = QtWidgets.QAction("Close", menu) - menu.addAction(close_tab_action) result = menu.exec_(global_point) if result is None: return From a2e96fee55b70ece8d73b9ac89dc5bd9135d4075 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Nov 2021 18:07:16 +0100 Subject: [PATCH 56/60] removed border radius from console widget --- openpype/style/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index 519adbbed3..2a2f4e572e 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -765,6 +765,7 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { /* Python console interpreter */ #PythonInterpreterOutput, #PythonCodeEditor { font-family: "Roboto Mono"; + border-radius: 0px; } #SubsetView::item, #RepresentationView:item { From ed64c8e9f6142a5b0e82e3aa01724d0e05d3d938 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 17 Nov 2021 03:40:31 +0000 Subject: [PATCH 57/60] [Automated] Bump version --- CHANGELOG.md | 28 ++++++++++++++++++++-------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed4b10a1c..9a3571eca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,22 @@ # Changelog +## [3.6.2-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.1...HEAD) + +**🚀 Enhancements** + +- Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255) +- Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251) +- Added endpoint for configured extensions [\#2221](https://github.com/pypeclub/OpenPype/pull/2221) + +**🐛 Bug fixes** + +- Burnins: Support mxf metadata [\#2247](https://github.com/pypeclub/OpenPype/pull/2247) + ## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.0...3.6.1) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.1-nightly.1...3.6.1) **🐛 Bug fixes** @@ -12,6 +26,11 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.0-nightly.6...3.6.0) +### 📖 Documentation + +- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) +- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) + **🆕 New features** - Add validate active site button to sync queue on a project [\#2176](https://github.com/pypeclub/OpenPype/pull/2176) @@ -32,7 +51,6 @@ - Maya : Validate shape zero [\#2212](https://github.com/pypeclub/OpenPype/pull/2212) - Maya : validate unique names [\#2211](https://github.com/pypeclub/OpenPype/pull/2211) - Tools: OpenPype stylesheet in workfiles tool [\#2208](https://github.com/pypeclub/OpenPype/pull/2208) -- Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) - Ftrack: Replace Queue with deque in event handlers logic [\#2204](https://github.com/pypeclub/OpenPype/pull/2204) - Tools: New select context dialog [\#2200](https://github.com/pypeclub/OpenPype/pull/2200) - Maya : Validate mesh ngons [\#2199](https://github.com/pypeclub/OpenPype/pull/2199) @@ -42,7 +60,6 @@ - Usage of tools code [\#2185](https://github.com/pypeclub/OpenPype/pull/2185) - Settings: Dictionary based on project roots [\#2184](https://github.com/pypeclub/OpenPype/pull/2184) - Subset name: Be able to pass asset document to get subset name [\#2179](https://github.com/pypeclub/OpenPype/pull/2179) -- Loader: Refactor and use OpenPype stylesheets [\#2166](https://github.com/pypeclub/OpenPype/pull/2166) **🐛 Bug fixes** @@ -62,10 +79,6 @@ - Maya: review viewport settings [\#2177](https://github.com/pypeclub/OpenPype/pull/2177) - Maya: Aspect ratio [\#2174](https://github.com/pypeclub/OpenPype/pull/2174) -### 📖 Documentation - -- Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) - ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0) @@ -88,7 +101,6 @@ - Fix - oiiotool wasn't recognized even if present [\#2129](https://github.com/pypeclub/OpenPype/pull/2129) - General: Disk mapping group [\#2120](https://github.com/pypeclub/OpenPype/pull/2120) - Hiero: publishing effect first time makes wrong resources path [\#2115](https://github.com/pypeclub/OpenPype/pull/2115) -- Add startup script for Houdini Core. [\#2110](https://github.com/pypeclub/OpenPype/pull/2110) ## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23) diff --git a/openpype/version.py b/openpype/version.py index 9c6070eca5..ef4bbe505b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.6.1" +__version__ = "3.6.2-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 264aebe988..cfe7422d49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.6.1" # OpenPype +version = "3.6.2-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 1c59ba5707b12b782fde335ff80bba7f4c86c9e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Nov 2021 16:21:06 +0100 Subject: [PATCH 58/60] fix maya look assigner garbage collection --- openpype/tools/mayalookassigner/app.py | 44 +++++++++++++++----------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/openpype/tools/mayalookassigner/app.py b/openpype/tools/mayalookassigner/app.py index d723387f2d..fb99333f87 100644 --- a/openpype/tools/mayalookassigner/app.py +++ b/openpype/tools/mayalookassigner/app.py @@ -38,6 +38,7 @@ class App(QtWidgets.QWidget): # Store callback references self._callbacks = [] + self._connections_set_up = False filename = get_workfile() @@ -46,17 +47,10 @@ class App(QtWidgets.QWidget): self.setWindowFlags(QtCore.Qt.Window) self.setParent(parent) - # Force to delete the window on close so it triggers - # closeEvent only once. Otherwise it's retriggered when - # the widget gets garbage collected. - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.resize(750, 500) self.setup_ui() - self.setup_connections() - # Force refresh check on initialization self._on_renderlayer_switch() @@ -111,6 +105,16 @@ class App(QtWidgets.QWidget): asset_outliner.view.setColumnWidth(0, 200) look_outliner.view.setColumnWidth(0, 150) + asset_outliner.selection_changed.connect( + self.on_asset_selection_changed) + + asset_outliner.refreshed.connect( + lambda: self.echo("Loaded assets..") + ) + + look_outliner.menu_apply_action.connect(self.on_process_selected) + remove_unused_btn.clicked.connect(remove_unused_looks) + # Open widgets self.asset_outliner = asset_outliner self.look_outliner = look_outliner @@ -123,15 +127,8 @@ class App(QtWidgets.QWidget): def setup_connections(self): """Connect interactive widgets with actions""" - - self.asset_outliner.selection_changed.connect( - self.on_asset_selection_changed) - - self.asset_outliner.refreshed.connect( - lambda: self.echo("Loaded assets..")) - - self.look_outliner.menu_apply_action.connect(self.on_process_selected) - self.remove_unused.clicked.connect(remove_unused_looks) + if self._connections_set_up: + return # Maya renderlayer switch callback callback = om.MEventMessage.addEventCallback( @@ -139,14 +136,23 @@ class App(QtWidgets.QWidget): self._on_renderlayer_switch ) self._callbacks.append(callback) + self._connections_set_up = True - def closeEvent(self, event): - + def remove_connection(self): # Delete callbacks for callback in self._callbacks: om.MMessage.removeCallback(callback) - return super(App, self).closeEvent(event) + self._callbacks = [] + self._connections_set_up = False + + def showEvent(self, event): + self.setup_connections() + super(App, self).showEvent(event) + + def closeEvent(self, event): + self.remove_connection() + super(App, self).closeEvent(event) def _on_renderlayer_switch(self, *args): """Callback that updates on Maya renderlayer switch""" From 2425b4a993c7170c1d44375bef78f312780d9bc8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Nov 2021 20:07:00 +0100 Subject: [PATCH 59/60] fix attribute acces in signal registration --- openpype/tools/context_dialog/window.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py index 3e7c8c7065..7f3ac75445 100644 --- a/openpype/tools/context_dialog/window.py +++ b/openpype/tools/context_dialog/window.py @@ -113,9 +113,7 @@ class ContextDialog(QtWidgets.QDialog): assets_widget.selection_changed.connect(self._on_asset_change) assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger) assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished) - tasks_widget.task_changed.selectionChanged.connect( - self._on_task_change - ) + tasks_widget.task_changed.connect(self._on_task_change) ok_btn.clicked.connect(self._on_ok_click) self._dbcon = dbcon From 980d1eb499a5c411e3b316bf86bc8ea00b6cf4aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Nov 2021 21:23:22 +0100 Subject: [PATCH 60/60] fix method name --- openpype/tools/workfiles/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index edea7bb1e0..aa98e67158 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -991,7 +991,7 @@ class Window(QtWidgets.QMainWindow): workdir, filename = os.path.split(filepath) asset_docs = self.assets_widget.get_selected_assets() asset_doc = asset_docs[0] - task_name = self.tasks_widget.get_current_task_name() + task_name = self.tasks_widget.get_selected_task_name() create_workfile_doc(asset_doc, task_name, filename, workdir, io) def set_context(self, context):