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 }}
diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py
index 50e5f995f4..e917a28046 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_end - frame_start + 1) + (handle_start + handle_end)
)
temporary_nodes = []
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
diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py b/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py
index a73bed8edd..08f09f8097 100644
--- a/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py
+++ b/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py
@@ -34,9 +34,9 @@ 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 +74,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
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",
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."
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]
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
diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py
index bec1f75425..0b139a73e4 100644
--- a/openpype/plugins/publish/extract_review.py
+++ b/openpype/plugins/publish/extract_review.py
@@ -1159,12 +1159,26 @@ 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"])
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((
@@ -1173,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"
diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py
index 7002168cdb..5442cf2211 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(
@@ -347,8 +372,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:
diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py
index f9ab46b6fd..e8dab089af 100644
--- a/openpype/plugins/publish/integrate_new.py
+++ b/openpype/plugins/publish/integrate_new.py
@@ -194,11 +194,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")
@@ -818,8 +822,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"],
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/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
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/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py
index 5ebcd7291d..27ce97955a 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
@@ -103,71 +103,98 @@ 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 ---
icon_widget = IconValuePixmapLabel(None, self)
icon_widget.setObjectName("FamilyIconLabel")
- family_label = QtWidgets.QLabel("family")
+ # --- Short description inputs ---
+ short_desc_input_widget = QtWidgets.QWidget(self)
+
+ 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)
- detail_description_widget.setObjectName("InfoText")
- detail_description_widget.setTextInteractionFlags(
- QtCore.Qt.TextBrowserInteraction
+ 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)
+ # --------------------------------
- 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)
-
- layout = QtWidgets.QVBoxLayout(self)
+ layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
- layout.addLayout(top_layout, 0)
- layout.addWidget(detail_description_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._icon_widget = icon_widget
+ self._family_label = family_label
+ self._description_label = description_label
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("")
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)
+
+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)
+ 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):
@@ -215,13 +242,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 ---
creators_view = QtWidgets.QListView(self)
creators_model = QtGui.QStandardItemModel()
creators_view.setModel(creators_model)
@@ -260,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(pre_create_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)
@@ -295,12 +357,9 @@ 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
- self.creator_description_widget = creator_description_widget
self.subset_name_input = subset_name_input
@@ -313,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
@@ -506,10 +570,61 @@ 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
@@ -563,10 +678,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
@@ -576,9 +690,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)
@@ -663,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:
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
diff --git a/openpype/tools/publisher/widgets/tasks_widget.py b/openpype/tools/publisher/widgets/tasks_widget.py
index a0b3a340ae..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):
@@ -17,9 +18,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 +72,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 +100,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
@@ -109,9 +119,13 @@ 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)
- 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 9a9fe3193e..5ced469b59 100644
--- a/openpype/tools/publisher/widgets/widgets.py
+++ b/openpype/tools/publisher/widgets/widgets.py
@@ -6,6 +6,7 @@ import collections
from Qt import QtWidgets, QtCore, QtGui
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
@@ -470,6 +471,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.
@@ -489,13 +512,16 @@ class TasksCombobox(QtWidgets.QComboBox):
delegate = QtWidgets.QStyledItemDelegate()
self.setItemDelegate(delegate)
- model = TasksModel(controller)
- self.setModel(model)
+ model = TasksModel(controller, True)
+ proxy_model = TasksComboboxProxy()
+ 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 = []
@@ -506,6 +532,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
@@ -595,6 +629,8 @@ class TasksCombobox(QtWidgets.QComboBox):
self._ignore_index_change = True
self._model.set_asset_names(asset_names)
+ self._proxy_model.set_filter_empty(False)
+ self._proxy_model.sort(0)
self._ignore_index_change = False
@@ -640,6 +676,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 = []
@@ -931,7 +970,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)
@@ -943,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)
@@ -997,7 +1036,33 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
project_name = self.controller.project_name
subset_names = set()
+ invalid_tasks = False
for instance in self._current_instances:
+ 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]
+
+ try:
+ new_subset_name = instance.creator.get_subset_name(
+ 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)
if variant_value is not None:
instance["variant"] = variant_value
@@ -1006,25 +1071,18 @@ 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)
- new_variant_value = instance.get("variant")
- new_asset_name = instance.get("asset")
- new_task_name = instance.get("task")
-
- 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
- )
- subset_names.add(new_subset_name)
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()
@@ -1097,7 +1155,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") or 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)
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..042ceaab88 100644
--- a/openpype/tools/utils/lib.py
+++ b/openpype/tools/utils/lib.py
@@ -98,6 +98,58 @@ 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 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"):