From bde7d988a21fdf1df8ac95ff863089f25dc31612 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Mon, 7 Mar 2022 16:59:50 +0100 Subject: [PATCH 01/28] Fix family test in validate_write_legacy to work with stillImage --- openpype/hosts/nuke/plugins/publish/validate_write_legacy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py b/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py index a73bed8edd..91e7dacc6e 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py @@ -34,9 +34,8 @@ class ValidateWriteLegacy(pyblish.api.InstancePlugin): # test if render in family test knob # and only one item should be available assert len(family_test) == 1, msg + " > More avalon attributes" - assert "render" in node[family_test[0]].value(), msg + \ + assert "render" in node[family_test[0]].value() or "still" in node[family_test[0]].value(), msg + \ " > Not correct family" - # test if `file` knob in node, this way old # non-group-node write could be detected assert "file" not in node.knobs(), msg + \ @@ -74,6 +73,8 @@ class ValidateWriteLegacy(pyblish.api.InstancePlugin): Create_name = "CreateWriteRender" elif family == "prerender": Create_name = "CreateWritePrerender" + elif family == "still": + Create_name = "CreateWriteStill" # get appropriate plugin class creator_plugin = None From 9f39bba2d3bdc97f0005ed3793134f66ecad834b Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Mon, 7 Mar 2022 17:06:23 +0100 Subject: [PATCH 02/28] fix hound --- openpype/hosts/nuke/plugins/publish/validate_write_legacy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py b/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py index 91e7dacc6e..08f09f8097 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py @@ -34,7 +34,8 @@ class ValidateWriteLegacy(pyblish.api.InstancePlugin): # test if render in family test knob # and only one item should be available assert len(family_test) == 1, msg + " > More avalon attributes" - assert "render" in node[family_test[0]].value() or "still" in node[family_test[0]].value(), msg + \ + assert "render" in node[family_test[0]].value() \ + or "still" in node[family_test[0]].value(), msg + \ " > Not correct family" # test if `file` knob in node, this way old # non-group-node write could be detected From e1d07b2b13d7af7c604c5f5293aea2b8c7639c2e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Mar 2022 17:11:13 +0100 Subject: [PATCH 03/28] nuke: adding input resolution of input video file --- .../plugins/publish/extract_review_slate.py | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 7002168cdb..948cee0639 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -14,7 +14,7 @@ class ExtractReviewSlate(openpype.api.Extractor): families = ["slate", "review"] match = pyblish.api.Subset - hosts = ["nuke", "maya", "shell"] + hosts = ["nuke", "shell"] optional = True def process(self, instance): @@ -59,13 +59,44 @@ class ExtractReviewSlate(openpype.api.Extractor): if "slate-frame" not in p_tags: continue + # get repre file + stagingdir = repre["stagingDir"] + input_file = "{0}".format(repre["files"]) + input_path = os.path.join( + os.path.normpath(stagingdir), repre["files"]) + self.log.debug("__ input_path: {}".format(input_path)) + + video_streams = openpype.lib.ffprobe_streams( + input_path, self.log + ) + + # Try to find first stream with defined 'width' and 'height' + # - this is to avoid order of streams where audio can be as first + # - there may be a better way (checking `codec_type`?) + input_width = None + input_height = None + for stream in video_streams: + if "width" in stream and "height" in stream: + input_width = int(stream["width"]) + input_height = int(stream["height"]) + break + + # Raise exception of any stream didn't define input resolution + if input_width is None: + raise AssertionError(( + "FFprobe couldn't read resolution from input file: \"{}\"" + ).format(input_path)) + # values are set in ExtractReview if use_legacy_code: to_width = inst_data["reviewToWidth"] to_height = inst_data["reviewToHeight"] else: - to_width = repre["resolutionWidth"] - to_height = repre["resolutionHeight"] + to_width = input_width + to_height = input_height + + self.log.debug("to_width: `{}`".format(to_width)) + self.log.debug("to_height: `{}`".format(to_height)) # defining image ratios resolution_ratio = ( @@ -94,15 +125,9 @@ class ExtractReviewSlate(openpype.api.Extractor): _remove_at_end = [] - stagingdir = repre["stagingDir"] - input_file = "{0}".format(repre["files"]) - ext = os.path.splitext(input_file)[1] output_file = input_file.replace(ext, "") + suffix + ext - input_path = os.path.join( - os.path.normpath(stagingdir), repre["files"]) - self.log.debug("__ input_path: {}".format(input_path)) _remove_at_end.append(input_path) output_path = os.path.join( From ee71d3b6580f363d95404b13283339ea055a183d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 10:13:51 +0100 Subject: [PATCH 04/28] fix getattr clalback on dynamic module --- openpype/modules/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index c7078475df..175957ae39 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -61,6 +61,7 @@ class _ModuleClass(object): def __init__(self, name): # Call setattr on super class super(_ModuleClass, self).__setattr__("name", name) + super(_ModuleClass, self).__setattr__("__name__", name) # Where modules and interfaces are stored super(_ModuleClass, self).__setattr__("__attributes__", dict()) @@ -72,7 +73,7 @@ class _ModuleClass(object): if attr_name not in self.__attributes__: if attr_name in ("__path__", "__file__"): return None - raise ImportError("No module named {}.{}".format( + raise AttributeError("'{}' has not attribute '{}'".format( self.name, attr_name )) return self.__attributes__[attr_name] From 9280eacf40d821bad8fa85c8db78ac551728b957 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 14:38:22 +0100 Subject: [PATCH 05/28] missing task does not make invalid asset --- openpype/pipeline/create/context.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 706279fd72..c2757a4502 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1005,12 +1005,14 @@ class CreateContext: if not instances: return - task_names_by_asset_name = collections.defaultdict(set) + task_names_by_asset_name = {} for instance in instances: task_name = instance.get("task") asset_name = instance.get("asset") - if asset_name and task_name: - task_names_by_asset_name[asset_name].add(task_name) + if asset_name: + task_names_by_asset_name[asset_name] = set() + if task_name: + task_names_by_asset_name[asset_name].add(task_name) asset_names = [ asset_name From acb7546e3896c6ec66c9c80a9e6fb68fa2a6468f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 16:25:03 +0100 Subject: [PATCH 06/28] handle 'TaskNotSetError' in create dialog --- .../tools/publisher/widgets/create_dialog.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index c5b77eca8b..607060da7e 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -8,7 +8,7 @@ try: except Exception: commonmark = None from Qt import QtWidgets, QtCore, QtGui - +from openpype.lib import TaskNotSetError from openpype.pipeline.create import ( CreatorError, SUBSET_NAME_ALLOWED_SYMBOLS @@ -566,10 +566,9 @@ class CreateDialog(QtWidgets.QDialog): if variant_value is None: variant_value = self.variant_input.text() - match = self._compiled_name_pattern.match(variant_value) - valid = bool(match) - self.create_btn.setEnabled(valid) - if not valid: + self.create_btn.setEnabled(True) + if not self._compiled_name_pattern.match(variant_value): + self.create_btn.setEnabled(False) self._set_variant_state_property("invalid") self.subset_name_input.setText("< Invalid variant >") return @@ -579,9 +578,16 @@ class CreateDialog(QtWidgets.QDialog): asset_doc = copy.deepcopy(self._asset_doc) # Calculate subset name with Creator plugin - subset_name = self._selected_creator.get_subset_name( - variant_value, task_name, asset_doc, project_name - ) + try: + subset_name = self._selected_creator.get_subset_name( + variant_value, task_name, asset_doc, project_name + ) + except TaskNotSetError: + self.create_btn.setEnabled(False) + self._set_variant_state_property("invalid") + self.subset_name_input.setText("< Missing task >") + return + self.subset_name_input.setText(subset_name) self._validate_subset_name(subset_name, variant_value) From 09dbeffe7122d9ad3ee37aab4354637bbbb16e6c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Mar 2022 16:42:49 +0100 Subject: [PATCH 07/28] global: slate could not be created when prores 4444 --- .../plugins/publish/extract_review_slate.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 7002168cdb..f9ed3cfab6 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -347,8 +347,21 @@ class ExtractReviewSlate(openpype.api.Extractor): profile_name = no_audio_stream.get("profile") if profile_name: - profile_name = profile_name.replace(" ", "_").lower() - codec_args.append("-profile:v {}".format(profile_name)) + # Rest of arguments is prores_kw specific + if codec_name == "prores_ks": + codec_tag_to_profile_map = { + "apco": "proxy", + "apcs": "lt", + "apcn": "standard", + "apch": "hq", + "ap4h": "4444", + "ap4x": "4444xq" + } + codec_tag_str = no_audio_stream.get("codec_tag_string") + if codec_tag_str: + profile = codec_tag_to_profile_map.get(codec_tag_str) + if profile: + codec_args.extend(["-profile:v", profile]) pix_fmt = no_audio_stream.get("pix_fmt") if pix_fmt: From a0b2995f15c2f40b81b73d0f8356a75eaa81bc32 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 17:39:08 +0100 Subject: [PATCH 08/28] tasks model has ability to have empty task --- .../tools/publisher/widgets/tasks_widget.py | 19 +++++++--- openpype/tools/publisher/widgets/widgets.py | 35 +++++++++++++++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/openpype/tools/publisher/widgets/tasks_widget.py b/openpype/tools/publisher/widgets/tasks_widget.py index a0b3a340ae..2d1cc017af 100644 --- a/openpype/tools/publisher/widgets/tasks_widget.py +++ b/openpype/tools/publisher/widgets/tasks_widget.py @@ -17,9 +17,10 @@ class TasksModel(QtGui.QStandardItemModel): controller (PublisherController): Controller which handles creation and publishing. """ - def __init__(self, controller): + def __init__(self, controller, allow_empty_task=False): super(TasksModel, self).__init__() + self._allow_empty_task = allow_empty_task self._controller = controller self._items_by_name = {} self._asset_names = [] @@ -70,8 +71,14 @@ class TasksModel(QtGui.QStandardItemModel): task_name (str): Name of task which should be available in asset's tasks. """ - task_names = self._task_names_by_asset_name.get(asset_name) - if task_names and task_name in task_names: + if asset_name not in self._task_names_by_asset_name: + return False + + if self._allow_empty_task and not task_name: + return True + + task_names = self._task_names_by_asset_name[asset_name] + if task_name in task_names: return True return False @@ -92,6 +99,8 @@ class TasksModel(QtGui.QStandardItemModel): new_task_names = self.get_intersection_of_tasks( task_names_by_asset_name ) + if self._allow_empty_task: + new_task_names.add("") old_task_names = set(self._items_by_name.keys()) if new_task_names == old_task_names: return @@ -111,7 +120,9 @@ class TasksModel(QtGui.QStandardItemModel): item.setData(task_name, TASK_NAME_ROLE) self._items_by_name[task_name] = item new_items.append(item) - root_item.appendRows(new_items) + + if new_items: + root_item.appendRows(new_items) def headerData(self, section, orientation, role=None): if role is None: diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index fb1f0e54aa..3f913d7e52 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -7,6 +7,7 @@ from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome +from openpype.lib import TaskNotSetError from openpype.widgets.attribute_defs import create_widget_for_attr_def from openpype.tools import resources from openpype.tools.flickcharm import FlickCharm @@ -490,13 +491,16 @@ class TasksCombobox(QtWidgets.QComboBox): delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(delegate) - model = TasksModel(controller) - self.setModel(model) + model = TasksModel(controller, True) + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setSourceModel(model) + self.setModel(proxy_model) self.currentIndexChanged.connect(self._on_index_change) self._delegate = delegate self._model = model + self._proxy_model = proxy_model self._origin_value = [] self._origin_selection = [] self._selected_items = [] @@ -596,6 +600,7 @@ class TasksCombobox(QtWidgets.QComboBox): self._ignore_index_change = True self._model.set_asset_names(asset_names) + self._proxy_model.invalidate() self._ignore_index_change = False @@ -1016,10 +1021,26 @@ class GlobalAttrsWidget(QtWidgets.QWidget): asset_doc = asset_docs_by_name[new_asset_name] - new_subset_name = instance.creator.get_subset_name( - new_variant_value, new_task_name, asset_doc, project_name - ) + try: + new_subset_name = instance.creator.get_subset_name( + new_variant_value, new_task_name, asset_doc, project_name + ) + except TaskNotSetError: + instance.set_task_invalid(True) + continue + subset_names.add(new_subset_name) + if variant_value is not None: + instance["variant"] = variant_value + + if asset_name is not None: + instance["asset"] = asset_name + instance.set_asset_invalid(False) + + if task_name is not None: + instance["task"] = task_name + instance.set_task_invalid(False) + instance["subset"] = new_subset_name self.subset_value_widget.set_value(subset_names) @@ -1098,7 +1119,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget): variants.add(instance.get("variant") or self.unknown_value) families.add(instance.get("family") or self.unknown_value) asset_name = instance.get("asset") or self.unknown_value - task_name = instance.get("task") or self.unknown_value + task_name = instance.get("task") + if task_name is None: + task_name = self.unknown_value asset_names.add(asset_name) asset_task_combinations.append((asset_name, task_name)) subset_names.add(instance.get("subset") or self.unknown_value) From 8efbe92ccb3b90981e8a17ba1ef4c1a9d81e22b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 17:39:41 +0100 Subject: [PATCH 09/28] changed "Sumbit" to "Confirm" --- openpype/tools/publisher/widgets/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 3f913d7e52..a5528e52a6 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -937,7 +937,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): family_value_widget.set_value() subset_value_widget.set_value() - submit_btn = QtWidgets.QPushButton("Submit", self) + submit_btn = QtWidgets.QPushButton("Confirm", self) cancel_btn = QtWidgets.QPushButton("Cancel", self) submit_btn.setEnabled(False) cancel_btn.setEnabled(False) From db1f2ce8c415f2fdf99c201f90602e18f00e69be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 17:44:33 +0100 Subject: [PATCH 10/28] fix method name --- openpype/hosts/testhost/plugins/publish/collect_context.py | 2 +- openpype/hosts/testhost/plugins/publish/collect_instance_1.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/testhost/plugins/publish/collect_context.py b/openpype/hosts/testhost/plugins/publish/collect_context.py index bbb8477cdf..0ab98fb84b 100644 --- a/openpype/hosts/testhost/plugins/publish/collect_context.py +++ b/openpype/hosts/testhost/plugins/publish/collect_context.py @@ -19,7 +19,7 @@ class CollectContextDataTestHost( hosts = ["testhost"] @classmethod - def get_instance_attr_defs(cls): + def get_attribute_defs(cls): return [ attribute_definitions.BoolDef( "test_bool", diff --git a/openpype/hosts/testhost/plugins/publish/collect_instance_1.py b/openpype/hosts/testhost/plugins/publish/collect_instance_1.py index 979ab83f11..3c035eccb6 100644 --- a/openpype/hosts/testhost/plugins/publish/collect_instance_1.py +++ b/openpype/hosts/testhost/plugins/publish/collect_instance_1.py @@ -20,7 +20,7 @@ class CollectInstanceOneTestHost( hosts = ["testhost"] @classmethod - def get_instance_attr_defs(cls): + def get_attribute_defs(cls): return [ attribute_definitions.NumberDef( "version", From c1304f4e691fec99de2d639992dee04a5957bc7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 18:45:05 +0100 Subject: [PATCH 11/28] it is possible to catch invalid tasks on confirm submit --- openpype/tools/publisher/widgets/widgets.py | 65 ++++++++++++++++----- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index a5528e52a6..ace7b4e02d 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -472,6 +472,28 @@ class AssetsField(BaseClickableFrame): self.set_selected_items(self._origin_value) +class TasksComboboxProxy(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(TasksComboboxProxy, self).__init__(*args, **kwargs) + self._filter_empty = False + + def set_filter_empty(self, filter_empty): + if self._filter_empty is filter_empty: + return + self._filter_empty = filter_empty + self.invalidate() + + def filterAcceptsRow(self, source_row, parent_index): + if self._filter_empty: + model = self.sourceModel() + source_index = model.index( + source_row, self.filterKeyColumn(), parent_index + ) + if not source_index.data(QtCore.Qt.DisplayRole): + return False + return True + + class TasksCombobox(QtWidgets.QComboBox): """Combobox to show tasks for selected instances. @@ -492,7 +514,7 @@ class TasksCombobox(QtWidgets.QComboBox): self.setItemDelegate(delegate) model = TasksModel(controller, True) - proxy_model = QtCore.QSortFilterProxyModel() + proxy_model = TasksComboboxProxy() proxy_model.setSourceModel(model) self.setModel(proxy_model) @@ -511,6 +533,14 @@ class TasksCombobox(QtWidgets.QComboBox): self._text = None + def set_invalid_empty_task(self, invalid=True): + self._proxy_model.set_filter_empty(invalid) + if invalid: + self._set_is_valid(False) + self.set_text("< One or more subsets require Task selected >") + else: + self.set_text(None) + def set_multiselection_text(self, text): """Change text shown when multiple different tasks are in context.""" self._multiselection_text = text @@ -600,7 +630,8 @@ class TasksCombobox(QtWidgets.QComboBox): self._ignore_index_change = True self._model.set_asset_names(asset_names) - self._proxy_model.invalidate() + self._proxy_model.set_filter_empty(False) + self._proxy_model.sort(0) self._ignore_index_change = False @@ -646,6 +677,9 @@ class TasksCombobox(QtWidgets.QComboBox): asset_task_combinations (list): List of tuples. Each item in the list contain asset name and task name. """ + self._proxy_model.set_filter_empty(False) + self._proxy_model.sort(0) + if asset_task_combinations is None: asset_task_combinations = [] @@ -1003,21 +1037,19 @@ class GlobalAttrsWidget(QtWidgets.QWidget): project_name = self.controller.project_name subset_names = set() + invalid_tasks = False for instance in self._current_instances: - if variant_value is not None: - instance["variant"] = variant_value - - if asset_name is not None: - instance["asset"] = asset_name - instance.set_asset_invalid(False) - - if task_name is not None: - instance["task"] = task_name - instance.set_task_invalid(False) - new_variant_value = instance.get("variant") new_asset_name = instance.get("asset") new_task_name = instance.get("task") + if variant_value is not None: + new_variant_value = variant_value + + if asset_name is not None: + new_asset_name = asset_name + + if task_name is not None: + new_task_name = task_name asset_doc = asset_docs_by_name[new_asset_name] @@ -1026,7 +1058,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget): new_variant_value, new_task_name, asset_doc, project_name ) except TaskNotSetError: + invalid_tasks = True instance.set_task_invalid(True) + subset_names.add(instance["subset"]) continue subset_names.add(new_subset_name) @@ -1043,10 +1077,13 @@ class GlobalAttrsWidget(QtWidgets.QWidget): instance["subset"] = new_subset_name + if invalid_tasks: + self.task_value_widget.set_invalid_empty_task() + self.subset_value_widget.set_value(subset_names) self._set_btns_enabled(False) - self._set_btns_visible(False) + self._set_btns_visible(invalid_tasks) self.instance_context_changed.emit() From 41bee1d2fac264cac48d3d5f6d2edd5139c5e2b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 18:49:57 +0100 Subject: [PATCH 12/28] change checkstate of goups on key press events --- .../tools/publisher/widgets/list_view_widgets.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 23a86cd070..6bddaf66c8 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -467,12 +467,22 @@ class InstanceListView(AbstractInstanceView): else: active = False + group_names = set() for instance_id in selected_instance_ids: widget = self._widgets_by_id.get(instance_id) - if widget is not None: - widget.set_active(active) + if widget is None: + continue + + widget.set_active(active) + group_name = self._group_by_instance_id.get(instance_id) + if group_name is not None: + group_names.add(group_name) + + for group_name in group_names: + self._update_group_checkstate(group_name) def _update_group_checkstate(self, group_name): + """Update checkstate of one group.""" widget = self._group_widgets.get(group_name) if widget is None: return From 5a2f917c2aec35286efe030f5b4f0c28c3fb293d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 18:54:54 +0100 Subject: [PATCH 13/28] empty task is stored as None --- openpype/tools/publisher/widgets/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index ace7b4e02d..200e85ba5c 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1072,7 +1072,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): instance.set_asset_invalid(False) if task_name is not None: - instance["task"] = task_name + instance["task"] = task_name or None instance.set_task_invalid(False) instance["subset"] = new_subset_name From 86ddbf8c5cf5455249cfe8d11d58593333190a5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 18:56:35 +0100 Subject: [PATCH 14/28] task as none will use empty string --- openpype/tools/publisher/widgets/widgets.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 200e85ba5c..8a950feb8b 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1156,9 +1156,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): variants.add(instance.get("variant") or self.unknown_value) families.add(instance.get("family") or self.unknown_value) asset_name = instance.get("asset") or self.unknown_value - task_name = instance.get("task") - if task_name is None: - task_name = self.unknown_value + task_name = instance.get("task") or "" asset_names.add(asset_name) asset_task_combinations.append((asset_name, task_name)) subset_names.add(instance.get("subset") or self.unknown_value) From a7b8cf26260f60c69d212dae7fb611c46503da72 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 19:20:04 +0100 Subject: [PATCH 15/28] add plugin to report before it's processing --- openpype/tools/publisher/control.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 5a84b1d8ca..6707feac9c 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -873,8 +873,6 @@ class PublisherController: """ for idx, plugin in enumerate(self.publish_plugins): self._publish_progress = idx - # Add plugin to publish report - self._publish_report.add_plugin_iter(plugin, self._publish_context) # Reset current plugin validations error self._publish_current_plugin_validation_errors = None @@ -902,6 +900,9 @@ class PublisherController: ): yield MainThreadItem(self.stop_publish) + # Add plugin to publish report + self._publish_report.add_plugin_iter(plugin, self._publish_context) + # Trigger callback that new plugin is going to be processed self._trigger_callbacks( self._publish_plugin_changed_callback_refs, plugin From 23b5f3828a4d248590f1035875eb05d0f674fea4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 19:20:23 +0100 Subject: [PATCH 16/28] don't use task if is not available in intergrate new --- openpype/plugins/publish/integrate_new.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6e0940d459..33d365fe42 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -192,11 +192,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "short": task_code } - else: + elif "task" in anatomy_data: # Just set 'task_name' variable to context task task_name = anatomy_data["task"]["name"] task_type = anatomy_data["task"]["type"] + else: + task_name = None + task_type = None + # Fill family in anatomy data anatomy_data["family"] = instance.data.get("family") @@ -816,8 +820,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # - is there a chance that task name is not filled in anatomy # data? # - should we use context task in that case? - task_name = instance.data["anatomyData"]["task"]["name"] - task_type = instance.data["anatomyData"]["task"]["type"] + anatomy_data = instance.data["anatomyData"] + task_name = None + task_type = None + if "task" in anatomy_data: + task_name = anatomy_data["task"]["name"] + task_type = anatomy_data["task"]["type"] filtering_criteria = { "families": instance.data["family"], "hosts": instance.context.data["hostName"], From ca94cf247aceac8a3c61d38465625c9c82c85aed Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 10 Mar 2022 09:37:42 +0100 Subject: [PATCH 17/28] add refactor sections to CI --- .github/workflows/prerelease.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 258458e2d4..d9b4d8089c 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -43,7 +43,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false sinceTag: "3.0.0" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f85525c26..917e6c884c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]},"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]}}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false sinceTag: "3.0.0" @@ -81,7 +81,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false sinceTag: ${{ steps.version.outputs.last_release }} From e61c8d992bc3a2308293b9077bc5ae85fa82ba05 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Mar 2022 10:57:01 +0100 Subject: [PATCH 18/28] fix hardlink for windows --- openpype/lib/path_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index 3a9f835272..851bc872fb 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -43,6 +43,7 @@ def create_hard_link(src_path, dst_path): res = CreateHardLink(dst_path, src_path, None) if res == 0: raise ctypes.WinError() + return # Raises not implemented error if gets here raise NotImplementedError( "Implementation of hardlink for current environment is missing." From 58aaa25c5536cdc61d1852e132c3e88e313dc587 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Mar 2022 11:56:06 +0100 Subject: [PATCH 19/28] added short description widget --- .../tools/publisher/widgets/create_dialog.py | 101 +++++++++++------- 1 file changed, 63 insertions(+), 38 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 6396b77901..83418b8bef 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -107,67 +107,100 @@ class CreatorDescriptionWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(CreatorDescriptionWidget, self).__init__(parent=parent) - icon_widget = IconValuePixmapLabel(None, self) + # --- Short description widget --- + short_desc_widget = QtWidgets.QWidget(self) + + icon_widget = IconValuePixmapLabel(None, short_desc_widget) icon_widget.setObjectName("FamilyIconLabel") - family_label = QtWidgets.QLabel("family") + # --- Short description inputs --- + short_desc_input_widget = QtWidgets.QWidget(short_desc_widget) + + family_label = QtWidgets.QLabel(short_desc_input_widget) family_label.setAlignment( QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft ) - description_label = QtWidgets.QLabel("description") + description_label = QtWidgets.QLabel(short_desc_input_widget) description_label.setAlignment( QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft ) - detail_description_widget = QtWidgets.QTextEdit(self) + short_desc_input_layout = QtWidgets.QVBoxLayout( + short_desc_input_widget + ) + short_desc_input_layout.setSpacing(0) + short_desc_input_layout.addWidget(family_label) + short_desc_input_layout.addWidget(description_label) + # -------------------------------- + + short_desc_layout = QtWidgets.QHBoxLayout(short_desc_widget) + short_desc_layout.setContentsMargins(0, 0, 0, 0) + short_desc_layout.addWidget(icon_widget, 0) + short_desc_layout.addWidget(short_desc_input_widget, 1) + # -------------------------------- + + separator_widget = QtWidgets.QWidget(self) + separator_widget.setObjectName("Separator") + separator_widget.setMinimumHeight(2) + separator_widget.setMaximumHeight(2) + + # --- Bottom part ---------------- + bottom_widget = QtWidgets.QWidget(self) + + # Precreate attributes widget + pre_create_widget = PreCreateWidget(bottom_widget) + + # Detailed description of creator + detail_description_widget = QtWidgets.QTextEdit(bottom_widget) detail_description_widget.setObjectName("InfoText") detail_description_widget.setTextInteractionFlags( QtCore.Qt.TextBrowserInteraction ) + # TODO add HELP button + detail_description_widget.setVisible(False) - label_layout = QtWidgets.QVBoxLayout() - label_layout.setSpacing(0) - label_layout.addWidget(family_label) - label_layout.addWidget(description_label) - - top_layout = QtWidgets.QHBoxLayout() - top_layout.setContentsMargins(0, 0, 0, 0) - top_layout.addWidget(icon_widget, 0) - top_layout.addLayout(label_layout, 1) + bottom_layout = QtWidgets.QHBoxLayout(bottom_widget) + bottom_layout.setContentsMargins(0, 0, 0, 0) + bottom_layout.addWidget(pre_create_widget, 1) + bottom_layout.addWidget(detail_description_widget, 1) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(top_layout, 0) - layout.addWidget(detail_description_widget, 1) + layout.addWidget(short_desc_widget, 0) + layout.addWidget(separator_widget, 0) + layout.addWidget(bottom_widget, 1) - self.icon_widget = icon_widget - self.family_label = family_label - self.description_label = description_label - self.detail_description_widget = detail_description_widget + self._icon_widget = icon_widget + self._family_label = family_label + self._description_label = description_label + self._detail_description_widget = detail_description_widget + self._pre_create_widget = pre_create_widget def set_plugin(self, plugin=None): if not plugin: - self.icon_widget.set_icon_def(None) - self.family_label.setText("") - self.description_label.setText("") - self.detail_description_widget.setPlainText("") + self._icon_widget.set_icon_def(None) + self._family_label.setText("") + self._description_label.setText("") + self._detail_description_widget.setPlainText("") + self._pre_create_widget.set_plugin(plugin) return plugin_icon = plugin.get_icon() description = plugin.get_description() or "" detailed_description = plugin.get_detail_description() or "" - self.icon_widget.set_icon_def(plugin_icon) - self.family_label.setText("{}".format(plugin.family)) - self.family_label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self.description_label.setText(description) + self._icon_widget.set_icon_def(plugin_icon) + self._family_label.setText("{}".format(plugin.family)) + self._family_label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self._description_label.setText(description) if commonmark: html = commonmark.commonmark(detailed_description) - self.detail_description_widget.setHtml(html) + self._detail_description_widget.setHtml(html) else: - self.detail_description_widget.setMarkdown(detailed_description) + self._detail_description_widget.setMarkdown(detailed_description) + self._pre_create_widget.set_plugin(plugin) class CreateDialog(QtWidgets.QDialog): @@ -215,12 +248,7 @@ class CreateDialog(QtWidgets.QDialog): context_layout.addWidget(assets_widget, 2) context_layout.addWidget(tasks_widget, 1) - # Precreate attributes widgets - pre_create_widget = PreCreateWidget(self) - - # TODO add HELP button creator_description_widget = CreatorDescriptionWidget(self) - creator_description_widget.setVisible(False) creators_view = QtWidgets.QListView(self) creators_model = QtGui.QStandardItemModel() @@ -264,7 +292,7 @@ class CreateDialog(QtWidgets.QDialog): splitter_widget = QtWidgets.QSplitter(self) splitter_widget.addWidget(context_widget) splitter_widget.addWidget(mid_widget) - splitter_widget.addWidget(pre_create_widget) + splitter_widget.addWidget(creator_description_widget) splitter_widget.setStretchFactor(0, 1) splitter_widget.setStretchFactor(1, 1) splitter_widget.setStretchFactor(2, 1) @@ -295,8 +323,6 @@ class CreateDialog(QtWidgets.QDialog): self._splitter_widget = splitter_widget - self._pre_create_widget = pre_create_widget - self._context_widget = context_widget self._assets_widget = assets_widget self._tasks_widget = tasks_widget @@ -510,7 +536,6 @@ class CreateDialog(QtWidgets.QDialog): creator = self.controller.manual_creators.get(identifier) self.creator_description_widget.set_plugin(creator) - self._pre_create_widget.set_plugin(creator) self._selected_creator = creator From 2bce15f106611e9e3e6572b3d6c6b7cb77509f67 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Mar 2022 11:57:21 +0100 Subject: [PATCH 20/28] changed Name to Variant --- openpype/tools/publisher/widgets/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 4e55e86491..5ced469b59 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -982,7 +982,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): btns_layout.addWidget(cancel_btn) main_layout = QtWidgets.QFormLayout(self) - main_layout.addRow("Name", variant_input) + main_layout.addRow("Variant", variant_input) main_layout.addRow("Asset", asset_value_widget) main_layout.addRow("Task", task_value_widget) main_layout.addRow("Family", family_value_widget) From 752af659bee065658e76d7a5ecb0996864de8069 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Thu, 10 Mar 2022 13:01:52 +0100 Subject: [PATCH 21/28] families is None for group/gizmo --- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index 97ddef0a59..29c706f302 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -80,7 +80,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): # Add all nodes in group instances. if node.Class() == "Group": # only alter families for render family - if "write" in families_ak.lower(): + if families_ak and "write" in families_ak.lower(): target = node["render"].value() if target == "Use existing frames": # Local rendering From 76b630aaac31e03ac867fa6ea4affa6a2c7b82b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Mar 2022 14:44:07 +0100 Subject: [PATCH 22/28] added description widget and detailed info widget --- openpype/style/style.css | 13 ++ .../tools/publisher/widgets/create_dialog.py | 208 +++++++++++++----- 2 files changed, 165 insertions(+), 56 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 5586cf766d..df83600973 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -836,6 +836,19 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { } /* New Create/Publish UI */ +#CreateDialogHelpButton { + background: rgba(255, 255, 255, 31); + border-top-right-radius: 0; + border-bottom-right-radius: 0; + font-size: 10pt; + font-weight: bold; + padding: 3px 3px 3px 3px; +} + +#CreateDialogHelpButton:hover { + background: rgba(255, 255, 255, 63); +} + #PublishLogConsole { font-family: "Noto Sans Mono"; } diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 83418b8bef..27ce97955a 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -103,18 +103,16 @@ class CreateErrorMessageBox(ErrorMessageBox): # TODO add creator identifier/label to details -class CreatorDescriptionWidget(QtWidgets.QWidget): +class CreatorShortDescWidget(QtWidgets.QWidget): def __init__(self, parent=None): - super(CreatorDescriptionWidget, self).__init__(parent=parent) + super(CreatorShortDescWidget, self).__init__(parent=parent) # --- Short description widget --- - short_desc_widget = QtWidgets.QWidget(self) - - icon_widget = IconValuePixmapLabel(None, short_desc_widget) + icon_widget = IconValuePixmapLabel(None, self) icon_widget.setObjectName("FamilyIconLabel") # --- Short description inputs --- - short_desc_input_widget = QtWidgets.QWidget(short_desc_widget) + short_desc_input_widget = QtWidgets.QWidget(self) family_label = QtWidgets.QLabel(short_desc_input_widget) family_label.setAlignment( @@ -134,73 +132,69 @@ class CreatorDescriptionWidget(QtWidgets.QWidget): short_desc_input_layout.addWidget(description_label) # -------------------------------- - short_desc_layout = QtWidgets.QHBoxLayout(short_desc_widget) - short_desc_layout.setContentsMargins(0, 0, 0, 0) - short_desc_layout.addWidget(icon_widget, 0) - short_desc_layout.addWidget(short_desc_input_widget, 1) - # -------------------------------- - - separator_widget = QtWidgets.QWidget(self) - separator_widget.setObjectName("Separator") - separator_widget.setMinimumHeight(2) - separator_widget.setMaximumHeight(2) - - # --- Bottom part ---------------- - bottom_widget = QtWidgets.QWidget(self) - - # Precreate attributes widget - pre_create_widget = PreCreateWidget(bottom_widget) - - # Detailed description of creator - detail_description_widget = QtWidgets.QTextEdit(bottom_widget) - detail_description_widget.setObjectName("InfoText") - detail_description_widget.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - # TODO add HELP button - detail_description_widget.setVisible(False) - - bottom_layout = QtWidgets.QHBoxLayout(bottom_widget) - bottom_layout.setContentsMargins(0, 0, 0, 0) - bottom_layout.addWidget(pre_create_widget, 1) - bottom_layout.addWidget(detail_description_widget, 1) - - layout = QtWidgets.QVBoxLayout(self) + layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(short_desc_widget, 0) - layout.addWidget(separator_widget, 0) - layout.addWidget(bottom_widget, 1) + layout.addWidget(icon_widget, 0) + layout.addWidget(short_desc_input_widget, 1) + # -------------------------------- self._icon_widget = icon_widget self._family_label = family_label self._description_label = description_label - self._detail_description_widget = detail_description_widget - self._pre_create_widget = pre_create_widget def set_plugin(self, plugin=None): if not plugin: self._icon_widget.set_icon_def(None) self._family_label.setText("") self._description_label.setText("") - self._detail_description_widget.setPlainText("") - self._pre_create_widget.set_plugin(plugin) return plugin_icon = plugin.get_icon() description = plugin.get_description() or "" - detailed_description = plugin.get_detail_description() or "" self._icon_widget.set_icon_def(plugin_icon) self._family_label.setText("{}".format(plugin.family)) self._family_label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) self._description_label.setText(description) - if commonmark: - html = commonmark.commonmark(detailed_description) - self._detail_description_widget.setHtml(html) + +class HelpButton(QtWidgets.QPushButton): + resized = QtCore.Signal() + + def __init__(self, *args, **kwargs): + super(HelpButton, self).__init__(*args, **kwargs) + self.setObjectName("CreateDialogHelpButton") + + self._expanded = None + self.set_expanded() + + def set_expanded(self, expanded=None): + if self._expanded is expanded: + if expanded is not None: + return + expanded = False + self._expanded = expanded + if expanded: + text = "<" else: - self._detail_description_widget.setMarkdown(detailed_description) - self._pre_create_widget.set_plugin(plugin) + text = "?" + self.setText(text) + + self._update_size() + + def _update_size(self): + new_size = self.minimumSizeHint() + if self.size() != new_size: + self.resize(new_size) + self.resized.emit() + + def showEvent(self, event): + super(HelpButton, self).showEvent(event) + self._update_size() + + def resizeEvent(self, event): + super(HelpButton, self).resizeEvent(event) + self._update_size() class CreateDialog(QtWidgets.QDialog): @@ -248,8 +242,7 @@ class CreateDialog(QtWidgets.QDialog): context_layout.addWidget(assets_widget, 2) context_layout.addWidget(tasks_widget, 1) - creator_description_widget = CreatorDescriptionWidget(self) - + # --- Creators view --- creators_view = QtWidgets.QListView(self) creators_model = QtGui.QStandardItemModel() creators_view.setModel(creators_model) @@ -288,24 +281,65 @@ class CreateDialog(QtWidgets.QDialog): mid_layout.addWidget(creators_view, 1) mid_layout.addLayout(form_layout, 0) mid_layout.addWidget(create_btn, 0) + # ------------ + + # --- Creator short info and attr defs --- + creator_attrs_widget = QtWidgets.QWidget(self) + + creator_short_desc_widget = CreatorShortDescWidget( + creator_attrs_widget + ) + + separator_widget = QtWidgets.QWidget(self) + separator_widget.setObjectName("Separator") + separator_widget.setMinimumHeight(2) + separator_widget.setMaximumHeight(2) + + # Precreate attributes widget + pre_create_widget = PreCreateWidget(creator_attrs_widget) + + creator_attrs_layout = QtWidgets.QVBoxLayout(creator_attrs_widget) + creator_attrs_layout.setContentsMargins(0, 0, 0, 0) + creator_attrs_layout.addWidget(creator_short_desc_widget, 0) + creator_attrs_layout.addWidget(separator_widget, 0) + creator_attrs_layout.addWidget(pre_create_widget, 1) + # ------------------------------------- + + # --- Detailed information about creator --- + # Detailed description of creator + detail_description_widget = QtWidgets.QTextEdit(self) + detail_description_widget.setObjectName("InfoText") + detail_description_widget.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + detail_description_widget.setVisible(False) + # ------------------------------------------- splitter_widget = QtWidgets.QSplitter(self) splitter_widget.addWidget(context_widget) splitter_widget.addWidget(mid_widget) - splitter_widget.addWidget(creator_description_widget) + splitter_widget.addWidget(creator_attrs_widget) + splitter_widget.addWidget(detail_description_widget) splitter_widget.setStretchFactor(0, 1) splitter_widget.setStretchFactor(1, 1) splitter_widget.setStretchFactor(2, 1) + splitter_widget.setStretchFactor(3, 1) layout = QtWidgets.QHBoxLayout(self) layout.addWidget(splitter_widget, 1) + # Floating help button + help_btn = HelpButton(self) + prereq_timer = QtCore.QTimer() prereq_timer.setInterval(50) prereq_timer.setSingleShot(True) prereq_timer.timeout.connect(self._on_prereq_timer) + help_btn.clicked.connect(self._on_help_btn) + help_btn.resized.connect(self._on_help_btn_resize) + create_btn.clicked.connect(self._on_create) variant_input.returnPressed.connect(self._on_create) variant_input.textChanged.connect(self._on_variant_change) @@ -326,7 +360,6 @@ class CreateDialog(QtWidgets.QDialog): self._context_widget = context_widget self._assets_widget = assets_widget self._tasks_widget = tasks_widget - self.creator_description_widget = creator_description_widget self.subset_name_input = subset_name_input @@ -339,6 +372,11 @@ class CreateDialog(QtWidgets.QDialog): self.creators_view = creators_view self.create_btn = create_btn + self._creator_short_desc_widget = creator_short_desc_widget + self._pre_create_widget = pre_create_widget + self._detail_description_widget = detail_description_widget + self._help_btn = help_btn + self._prereq_timer = prereq_timer self._first_show = True @@ -532,10 +570,62 @@ class CreateDialog(QtWidgets.QDialog): identifier = new_index.data(CREATOR_IDENTIFIER_ROLE) self._set_creator(identifier) + def _update_help_btn(self): + pos_x = self.width() - self._help_btn.width() + point = self._creator_short_desc_widget.rect().topRight() + mapped_point = self._creator_short_desc_widget.mapTo(self, point) + pos_y = mapped_point.y() + self._help_btn.move(max(0, pos_x), max(0, pos_y)) + + def _on_help_btn_resize(self): + self._update_help_btn() + + def _on_help_btn(self): + final_size = self.size() + cur_sizes = self._splitter_widget.sizes() + spacing = self._splitter_widget.handleWidth() + + sizes = [] + for idx, value in enumerate(cur_sizes): + if idx < 3: + sizes.append(value) + + now_visible = self._detail_description_widget.isVisible() + if now_visible: + width = final_size.width() - ( + spacing + self._detail_description_widget.width() + ) + + else: + last_size = self._detail_description_widget.sizeHint().width() + width = final_size.width() + spacing + last_size + sizes.append(last_size) + + final_size.setWidth(width) + + self._detail_description_widget.setVisible(not now_visible) + self._splitter_widget.setSizes(sizes) + self.resize(final_size) + + self._help_btn.set_expanded(not now_visible) + + def _set_creator_detailed_text(self, creator): + if not creator: + self._detail_description_widget.setPlainText("") + return + detailed_description = creator.get_detail_description() or "" + if commonmark: + html = commonmark.commonmark(detailed_description) + self._detail_description_widget.setHtml(html) + else: + self._detail_description_widget.setMarkdown(detailed_description) + def _set_creator(self, identifier): creator = self.controller.manual_creators.get(identifier) - self.creator_description_widget.set_plugin(creator) + self._creator_short_desc_widget.set_plugin(creator) + self._set_creator_detailed_text(creator) + self._pre_create_widget.set_plugin(creator) self._selected_creator = creator @@ -694,8 +784,14 @@ class CreateDialog(QtWidgets.QDialog): if self._last_pos is not None: self.move(self._last_pos) + self._update_help_btn() + self.refresh() + def resizeEvent(self, event): + super(CreateDialog, self).resizeEvent(event) + self._update_help_btn() + def _on_create(self): indexes = self.creators_view.selectedIndexes() if not indexes or len(indexes) > 1: From be19709107bc7c33781175c6948fa43b38891cd1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Mar 2022 15:56:02 +0100 Subject: [PATCH 23/28] nuke: fix slate check for frame length --- openpype/hosts/nuke/plugins/publish/extract_slate_frame.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index 50e5f995f4..a91181c81b 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -48,8 +48,13 @@ class ExtractSlateFrame(openpype.api.Extractor): self.log.info( "StagingDir `{0}`...".format(instance.data["stagingDir"])) + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + frame_length = int( - instance.data["frameEnd"] - instance.data["frameStart"] + 1 + (frame_start - frame_end + 1) + (handle_start + handle_end) ) temporary_nodes = [] From 32923208687a221fb11f2f85cb43680292bc3981 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Mar 2022 15:56:39 +0100 Subject: [PATCH 24/28] implemented get asset icon function --- openpype/tools/utils/__init__.py | 1 + openpype/tools/utils/assets_widget.py | 27 +++++------------ openpype/tools/utils/lib.py | 42 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index c15e9f8139..6ab9e75b52 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -16,6 +16,7 @@ from .lib import ( set_style_property, DynamicQThread, qt_app_context, + get_asset_icon, ) from .models import ( diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index d410b0f1c3..4c77b81c0e 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -16,7 +16,10 @@ from .views import ( ) from .widgets import PlaceholderLineEdit from .models import RecursiveSortFilterProxyModel -from .lib import DynamicQThread +from .lib import ( + DynamicQThread, + get_asset_icon +) if Qt.__binding__ == "PySide": from PySide.QtGui import QStyleOptionViewItemV4 @@ -508,25 +511,9 @@ class AssetModel(QtGui.QStandardItemModel): item.setData(asset_label, QtCore.Qt.DisplayRole) item.setData(asset_label, ASSET_LABEL_ROLE) - icon_color = asset_data.get("color") or style.colors.default - icon_name = asset_data.get("icon") - if not icon_name: - # Use default icons if no custom one is specified. - # If it has children show a full folder, otherwise - # show an open folder - if item.rowCount() > 0: - icon_name = "folder" - else: - icon_name = "folder-o" - - try: - # font-awesome key - full_icon_name = "fa.{0}".format(icon_name) - icon = qtawesome.icon(full_icon_name, color=icon_color) - item.setData(icon, QtCore.Qt.DecorationRole) - - except Exception: - pass + has_children = item.rowCount() > 0 + icon = get_asset_icon(asset_data, has_children) + item.setData(icon, QtCore.Qt.DecorationRole) def _threaded_fetch(self): asset_docs = self._fetch_asset_docs() diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 1cbc632804..d57b44728d 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -98,6 +98,48 @@ application = qt_app_context class SharedObjects: jobs = {} + icons = {} + + +def get_qta_icon_by_name_and_color(icon_name, icon_color): + if not icon_name or not icon_color: + return None + + full_icon_name = "{0}-{1}".format(icon_name, icon_color) + if full_icon_name in SharedObjects.icons: + return SharedObjects.icons[full_icon_name] + + variants = [icon_name] + qta_instance = qtawesome._instance() + for key in qta_instance.charmap.keys(): + variants.append("{0}.{1}".format(key, icon_name)) + + icon = None + for variant in variants: + try: + icon = qtawesome.icon(variant, color=icon_color) + break + except Exception: + pass + + SharedObjects.icons[full_icon_name] = icon + return icon + + +def get_asset_icon(asset_doc, has_children=False): + asset_data = asset_doc.get("data") or {} + icon_color = asset_data.get("color") or style.colors.default + icon_name = asset_data.get("icon") + if not icon_name: + # Use default icons if no custom one is specified. + # If it has children show a full folder, otherwise + # show an open folder + if has_children: + icon_name = "folder" + else: + icon_name = "folder-o" + + return get_qta_icon_by_name_and_color(icon_name, icon_color) def schedule(func, time, channel="default"): From ff440612a2bc00b84ebd9275192f12025732bb62 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Mar 2022 15:56:50 +0100 Subject: [PATCH 25/28] added icons into publisher UI --- openpype/tools/publisher/widgets/assets_widget.py | 7 ++++++- openpype/tools/publisher/widgets/tasks_widget.py | 3 +++ openpype/tools/utils/lib.py | 10 ++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index b8696a2665..984da59c77 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -3,7 +3,8 @@ import collections from Qt import QtWidgets, QtCore, QtGui from openpype.tools.utils import ( PlaceholderLineEdit, - RecursiveSortFilterProxyModel + RecursiveSortFilterProxyModel, + get_asset_icon, ) from openpype.tools.utils.assets_widget import ( SingleSelectAssetsWidget, @@ -102,11 +103,15 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel): for name in sorted(children_by_name.keys()): child = children_by_name[name] child_id = child["_id"] + has_children = bool(assets_by_parent_id.get(child_id)) + icon = get_asset_icon(child, has_children) + item = QtGui.QStandardItem(name) item.setFlags( QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable ) + item.setData(icon, QtCore.Qt.DecorationRole) item.setData(child_id, ASSET_ID_ROLE) item.setData(name, ASSET_NAME_ROLE) diff --git a/openpype/tools/publisher/widgets/tasks_widget.py b/openpype/tools/publisher/widgets/tasks_widget.py index 2d1cc017af..8a913b7114 100644 --- a/openpype/tools/publisher/widgets/tasks_widget.py +++ b/openpype/tools/publisher/widgets/tasks_widget.py @@ -1,6 +1,7 @@ from Qt import QtCore, QtGui from openpype.tools.utils.tasks_widget import TasksWidget, TASK_NAME_ROLE +from openpype.tools.utils.lib import get_task_icon class TasksModel(QtGui.QStandardItemModel): @@ -118,6 +119,8 @@ class TasksModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem(task_name) item.setData(task_name, TASK_NAME_ROLE) + if task_name: + item.setData(get_task_icon(), QtCore.Qt.DecorationRole) self._items_by_name[task_name] = item new_items.append(item) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index d57b44728d..042ceaab88 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -142,6 +142,16 @@ def get_asset_icon(asset_doc, has_children=False): return get_qta_icon_by_name_and_color(icon_name, icon_color) +def get_task_icon(): + """Get icon for a task. + + TODO: Get task icon based on data in database. + + Icon should be defined by task type which is stored on project. + """ + return get_qta_icon_by_name_and_color("fa.male", style.colors.default) + + def schedule(func, time, channel="default"): """Run `func` at a later `time` in a dedicated `channel` From 87719ed878a55ec096a9cf57f0e3493577b3b3ce Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Mar 2022 15:59:28 +0100 Subject: [PATCH 26/28] nuke: wrong expression --- openpype/hosts/nuke/plugins/publish/extract_slate_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index a91181c81b..e917a28046 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -54,7 +54,7 @@ class ExtractSlateFrame(openpype.api.Extractor): handle_end = instance.data["handleEnd"] frame_length = int( - (frame_start - frame_end + 1) + (handle_start + handle_end) + (frame_end - frame_start + 1) + (handle_start + handle_end) ) temporary_nodes = [] From 0b36fc2c65356b9b787cb79e58cf3822c4bd1e81 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Mar 2022 16:08:12 +0100 Subject: [PATCH 27/28] fixing reformat in extract review when slate reformate --- openpype/plugins/publish/extract_review.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index bec1f75425..f9a02f58bb 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1165,6 +1165,18 @@ class ExtractReview(pyblish.api.InstancePlugin): input_height = int(stream["height"]) break + # Get instance data + pixel_aspect = temp_data["pixel_aspect"] + + if reformat_in_baking: + self.log.debug(( + "Using resolution from input. It is already " + "reformated from upstream process" + )) + pixel_aspect = 1 + output_width = input_width + output_height = input_height + # Raise exception of any stream didn't define input resolution if input_width is None: raise AssertionError(( From 99d7912495b5a57eba48f5d614aac6cd08e7d1a3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Mar 2022 16:12:11 +0100 Subject: [PATCH 28/28] defining default none values for Output resolution --- openpype/plugins/publish/extract_review.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index f9a02f58bb..0b139a73e4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1159,6 +1159,8 @@ class ExtractReview(pyblish.api.InstancePlugin): # - there may be a better way (checking `codec_type`?) input_width = None input_height = None + output_width = None + output_height = None for stream in streams: if "width" in stream and "height" in stream: input_width = int(stream["width"]) @@ -1185,8 +1187,8 @@ class ExtractReview(pyblish.api.InstancePlugin): # NOTE Setting only one of `width` or `heigth` is not allowed # - settings value can't have None but has value of 0 - output_width = output_def.get("width") or None - output_height = output_def.get("height") or None + output_width = output_width or output_def.get("width") or None + output_height = output_height or output_def.get("height") or None # Overscal color overscan_color_value = "black"