[Automated] Merged develop into main

This commit is contained in:
pypebot 2022-10-19 14:04:53 +02:00 committed by GitHub
commit 966fa92d8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 6890 additions and 816 deletions

View file

@ -37,27 +37,27 @@ jobs:
echo ::set-output name=next_tag::$RESULT
- name: "✏️ Generate full changelog"
if: steps.version_type.outputs.type != 'skip'
id: generate-full-changelog
uses: heinrichreimer/github-changelog-generator-action@v2.3
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"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}'
issues: false
issuesWoLabels: false
sinceTag: "3.12.0"
maxIssues: 100
pullRequests: true
prWoLabels: false
author: false
unreleased: true
compareLink: true
stripGeneratorNotice: true
verbose: true
unreleasedLabel: ${{ steps.version.outputs.next_tag }}
excludeTagsRegex: "CI/.+"
releaseBranch: "main"
# - name: "✏️ Generate full changelog"
# if: steps.version_type.outputs.type != 'skip'
# id: generate-full-changelog
# uses: heinrichreimer/github-changelog-generator-action@v2.3
# 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"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}'
# issues: false
# issuesWoLabels: false
# sinceTag: "3.12.0"
# maxIssues: 100
# pullRequests: true
# prWoLabels: false
# author: false
# unreleased: true
# compareLink: true
# stripGeneratorNotice: true
# verbose: true
# unreleasedLabel: ${{ steps.version.outputs.next_tag }}
# excludeTagsRegex: "CI/.+"
# releaseBranch: "main"
- name: "🖨️ Print changelog to console"
if: steps.version_type.outputs.type != 'skip'
@ -85,7 +85,7 @@ jobs:
tags: true
unprotect_reviews: true
- name: 🔨 Merge main back to develop
- name: 🔨 Merge main back to develop
uses: everlytic/branch-merge@1.1.0
if: steps.version_type.outputs.type != 'skip'
with:

View file

@ -2,7 +2,7 @@ name: Stable Release
on:
release:
types:
types:
- prereleased
jobs:
@ -13,7 +13,7 @@ jobs:
steps:
- name: 🚛 Checkout Code
uses: actions/checkout@v2
with:
with:
fetch-depth: 0
- name: Set up Python
@ -33,27 +33,27 @@ jobs:
echo ::set-output name=last_release::$LASTRELEASE
echo ::set-output name=release_tag::$RESULT
- name: "✏️ Generate full changelog"
if: steps.version.outputs.release_tag != 'skip'
id: generate-full-changelog
uses: heinrichreimer/github-changelog-generator-action@v2.3
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"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}'
issues: false
issuesWoLabels: false
sinceTag: "3.12.0"
maxIssues: 100
pullRequests: true
prWoLabels: false
author: false
unreleased: true
compareLink: true
stripGeneratorNotice: true
verbose: true
futureRelease: ${{ steps.version.outputs.release_tag }}
excludeTagsRegex: "CI/.+"
releaseBranch: "main"
# - name: "✏️ Generate full changelog"
# if: steps.version.outputs.release_tag != 'skip'
# id: generate-full-changelog
# uses: heinrichreimer/github-changelog-generator-action@v2.3
# 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"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}'
# issues: false
# issuesWoLabels: false
# sinceTag: "3.12.0"
# maxIssues: 100
# pullRequests: true
# prWoLabels: false
# author: false
# unreleased: true
# compareLink: true
# stripGeneratorNotice: true
# verbose: true
# futureRelease: ${{ steps.version.outputs.release_tag }}
# excludeTagsRegex: "CI/.+"
# releaseBranch: "main"
- name: 💾 Commit and Tag
id: git_commit
@ -73,8 +73,8 @@ jobs:
token: ${{ secrets.ADMIN_TOKEN }}
branch: main
tags: true
unprotect_reviews: true
unprotect_reviews: true
- name: "✏️ Generate last changelog"
if: steps.version.outputs.release_tag != 'skip'
id: generate-last-changelog
@ -114,7 +114,7 @@ jobs:
with:
tag: "${{ steps.version.outputs.current_version }}"
- name: 🔁 Merge main back to develop
- name: 🔁 Merge main back to develop
if: steps.version.outputs.release_tag != 'skip'
uses: everlytic/branch-merge@1.1.0
with:

2
.gitignore vendored
View file

@ -110,3 +110,5 @@ tools/run_eventserver.*
# Developer tools
tools/dev_*
.github_changelog_generator

File diff suppressed because it is too large Load diff

1818
HISTORY.md

File diff suppressed because it is too large Load diff

View file

@ -260,20 +260,20 @@ class ARenderProducts:
"""
try:
file_prefix_attr = IMAGE_PREFIXES[self.renderer]
prefix_attr = IMAGE_PREFIXES[self.renderer]
except KeyError:
raise UnsupportedRendererException(
"Unsupported renderer {}".format(self.renderer)
)
file_prefix = self._get_attr(file_prefix_attr)
prefix = self._get_attr(prefix_attr)
if not file_prefix:
if not prefix:
# Fall back to scene name by default
log.debug("Image prefix not set, using <Scene>")
file_prefix = "<Scene>"
return file_prefix
return prefix
def get_render_attribute(self, attribute):
"""Get attribute from render options.
@ -730,13 +730,16 @@ class RenderProductsVray(ARenderProducts):
"""Get image prefix for V-Ray.
This overrides :func:`ARenderProducts.get_renderer_prefix()` as
we must add `<aov>` token manually.
we must add `<aov>` token manually. This is done only for
non-multipart outputs, where `<aov>` token doesn't make sense.
See also:
:func:`ARenderProducts.get_renderer_prefix()`
"""
prefix = super(RenderProductsVray, self).get_renderer_prefix()
if self.multipart:
return prefix
aov_separator = self._get_aov_separator()
prefix = "{}{}<aov>".format(prefix, aov_separator)
return prefix
@ -974,15 +977,18 @@ class RenderProductsRedshift(ARenderProducts):
"""Get image prefix for Redshift.
This overrides :func:`ARenderProducts.get_renderer_prefix()` as
we must add `<aov>` token manually.
we must add `<aov>` token manually. This is done only for
non-multipart outputs, where `<aov>` token doesn't make sense.
See also:
:func:`ARenderProducts.get_renderer_prefix()`
"""
file_prefix = super(RenderProductsRedshift, self).get_renderer_prefix()
separator = self.extract_separator(file_prefix)
prefix = "{}{}<aov>".format(file_prefix, separator or "_")
prefix = super(RenderProductsRedshift, self).get_renderer_prefix()
if self.multipart:
return prefix
separator = self.extract_separator(prefix)
prefix = "{}{}<aov>".format(prefix, separator or "_")
return prefix
def get_render_products(self):

View file

@ -12,6 +12,7 @@ class CreateAnimation(plugin.Creator):
family = "animation"
icon = "male"
write_color_sets = False
write_face_sets = False
def __init__(self, *args, **kwargs):
super(CreateAnimation, self).__init__(*args, **kwargs)
@ -24,7 +25,7 @@ class CreateAnimation(plugin.Creator):
# Write vertex colors with the geometry.
self.data["writeColorSets"] = self.write_color_sets
self.data["writeFaceSets"] = False
self.data["writeFaceSets"] = self.write_face_sets
# Include only renderable visible shapes.
# Skips locators and empty transforms

View file

@ -9,13 +9,14 @@ class CreateModel(plugin.Creator):
family = "model"
icon = "cube"
defaults = ["Main", "Proxy", "_MD", "_HD", "_LD"]
write_color_sets = False
write_face_sets = False
def __init__(self, *args, **kwargs):
super(CreateModel, self).__init__(*args, **kwargs)
# Vertex colors with the geometry
self.data["writeColorSets"] = False
self.data["writeFaceSets"] = False
self.data["writeColorSets"] = self.write_color_sets
self.data["writeFaceSets"] = self.write_face_sets
# Include attributes by attribute name or prefix
self.data["attr"] = ""

View file

@ -12,6 +12,7 @@ class CreatePointCache(plugin.Creator):
family = "pointcache"
icon = "gears"
write_color_sets = False
write_face_sets = False
def __init__(self, *args, **kwargs):
super(CreatePointCache, self).__init__(*args, **kwargs)
@ -21,7 +22,8 @@ class CreatePointCache(plugin.Creator):
# Vertex colors with the geometry.
self.data["writeColorSets"] = self.write_color_sets
self.data["writeFaceSets"] = False # Vertex colors with the geometry.
# Vertex colors with the geometry.
self.data["writeFaceSets"] = self.write_face_sets
self.data["renderableOnly"] = False # Only renderable visible shapes
self.data["visibleOnly"] = False # only nodes that are visible
self.data["includeParentHierarchy"] = False # Include parent groups

View file

@ -3,11 +3,33 @@ import re
import collections
import uuid
import json
from abc import ABCMeta, abstractmethod
from abc import ABCMeta, abstractmethod, abstractproperty
import six
import clique
# Global variable which store attribude definitions by type
# - default types are registered on import
_attr_defs_by_type = {}
def register_attr_def_class(cls):
"""Register attribute definition.
Currently are registered definitions used to deserialize data to objects.
Attrs:
cls (AbtractAttrDef): Non-abstract class to be registered with unique
'type' attribute.
Raises:
KeyError: When type was already registered.
"""
if cls.type in _attr_defs_by_type:
raise KeyError("Type \"{}\" was already registered".format(cls.type))
_attr_defs_by_type[cls.type] = cls
def get_attributes_keys(attribute_definitions):
"""Collect keys from list of attribute definitions.
@ -90,6 +112,8 @@ class AbtractAttrDef:
next to value input or ahead.
"""
type_attributes = []
is_value_def = True
def __init__(
@ -115,6 +139,16 @@ class AbtractAttrDef:
return False
return self.key == other.key
@abstractproperty
def type(self):
"""Attribute definition type also used as identifier of class.
Returns:
str: Type of attribute definition.
"""
pass
@abstractmethod
def convert_value(self, value):
"""Convert value to a valid one.
@ -125,6 +159,35 @@ class AbtractAttrDef:
pass
def serialize(self):
"""Serialize object to data so it's possible to recreate it.
Returns:
Dict[str, Any]: Serialized object that can be passed to
'deserialize' method.
"""
data = {
"type": self.type,
"key": self.key,
"label": self.label,
"tooltip": self.tooltip,
"default": self.default,
"is_label_horizontal": self.is_label_horizontal
}
for attr in self.type_attributes:
data[attr] = getattr(self, attr)
return data
@classmethod
def deserialize(cls, data):
"""Recreate object from data.
Data can be received using 'serialize' method.
"""
return cls(**data)
# -----------------------------------------
# UI attribute definitoins won't hold value
@ -141,10 +204,12 @@ class UIDef(AbtractAttrDef):
class UISeparatorDef(UIDef):
pass
type = "separator"
class UILabelDef(UIDef):
type = "label"
def __init__(self, label):
super(UILabelDef, self).__init__(label=label)
@ -160,6 +225,8 @@ class UnknownDef(AbtractAttrDef):
have known definition of type.
"""
type = "unknown"
def __init__(self, key, default=None, **kwargs):
kwargs["default"] = default
super(UnknownDef, self).__init__(key, **kwargs)
@ -181,6 +248,13 @@ class NumberDef(AbtractAttrDef):
default(int, float): Default value for conversion.
"""
type = "number"
type_attributes = [
"minimum",
"maximum",
"decimals"
]
def __init__(
self, key, minimum=None, maximum=None, decimals=None, default=None,
**kwargs
@ -252,6 +326,12 @@ class TextDef(AbtractAttrDef):
default(str, None): Default value. Empty string used when not defined.
"""
type = "text"
type_attributes = [
"multiline",
"placeholder",
]
def __init__(
self, key, multiline=None, regex=None, placeholder=None, default=None,
**kwargs
@ -290,6 +370,11 @@ class TextDef(AbtractAttrDef):
return value
return self.default
def serialize(self):
data = super(TextDef, self).serialize()
data["regex"] = self.regex.pattern
return data
class EnumDef(AbtractAttrDef):
"""Enumeration of single item from items.
@ -301,6 +386,8 @@ class EnumDef(AbtractAttrDef):
default: Default value. Must be one key(value) from passed items.
"""
type = "enum"
def __init__(self, key, items, default=None, **kwargs):
if not items:
raise ValueError((
@ -335,6 +422,11 @@ class EnumDef(AbtractAttrDef):
return value
return self.default
def serialize(self):
data = super(TextDef, self).serialize()
data["items"] = list(self.items)
return data
class BoolDef(AbtractAttrDef):
"""Boolean representation.
@ -343,6 +435,8 @@ class BoolDef(AbtractAttrDef):
default(bool): Default value. Set to `False` if not defined.
"""
type = "bool"
def __init__(self, key, default=None, **kwargs):
if default is None:
default = False
@ -585,6 +679,15 @@ class FileDef(AbtractAttrDef):
default(str, List[str]): Default value.
"""
type = "path"
type_attributes = [
"single_item",
"folders",
"extensions",
"allow_sequences",
"extensions_label",
]
def __init__(
self, key, single_item=True, folders=None, extensions=None,
allow_sequences=True, extensions_label=None, default=None, **kwargs
@ -675,3 +778,71 @@ class FileDef(AbtractAttrDef):
if self.single_item:
return FileDefItem.create_empty_item().to_dict()
return []
def serialize_attr_def(attr_def):
"""Serialize attribute definition to data.
Args:
attr_def (AbtractAttrDef): Attribute definition to serialize.
Returns:
Dict[str, Any]: Serialized data.
"""
return attr_def.serialize()
def serialize_attr_defs(attr_defs):
"""Serialize attribute definitions to data.
Args:
attr_defs (List[AbtractAttrDef]): Attribute definitions to serialize.
Returns:
List[Dict[str, Any]]: Serialized data.
"""
return [
serialize_attr_def(attr_def)
for attr_def in attr_defs
]
def deserialize_attr_def(attr_def_data):
"""Deserialize attribute definition from data.
Args:
attr_def (Dict[str, Any]): Attribute definition data to deserialize.
"""
attr_type = attr_def_data.pop("type")
cls = _attr_defs_by_type[attr_type]
return cls.deserialize(attr_def_data)
def deserialize_attr_defs(attr_defs_data):
"""Deserialize attribute definitions.
Args:
List[Dict[str, Any]]: List of attribute definitions.
"""
return [
deserialize_attr_def(attr_def_data)
for attr_def_data in attr_defs_data
]
# Register attribute definitions
for _attr_class in (
UISeparatorDef,
UILabelDef,
UnknownDef,
NumberDef,
TextDef,
EnumDef,
BoolDef,
FileDef
):
register_attr_def_class(_attr_class)

View file

@ -1,6 +1,7 @@
"""Events holding data about specific event."""
import os
import re
import copy
import inspect
import logging
import weakref
@ -207,6 +208,12 @@ class Event(object):
@property
def source(self):
"""Event's source used for triggering callbacks.
Returns:
Union[str, None]: Source string or None. Source is optional.
"""
return self._source
@property
@ -215,6 +222,12 @@ class Event(object):
@property
def topic(self):
"""Event's topic used for triggering callbacks.
Returns:
str: Topic string.
"""
return self._topic
def emit(self):
@ -227,6 +240,42 @@ class Event(object):
)
self._event_system.emit_event(self)
def to_data(self):
"""Convert Event object to data.
Returns:
Dict[str, Any]: Event data.
"""
return {
"id": self.id,
"topic": self.topic,
"source": self.source,
"data": copy.deepcopy(self.data)
}
@classmethod
def from_data(cls, event_data, event_system=None):
"""Create event from data.
Args:
event_data (Dict[str, Any]): Event data with defined keys. Can be
created using 'to_data' method.
event_system (EventSystem): System to which the event belongs.
Returns:
Event: Event with attributes from passed data.
"""
obj = cls(
event_data["topic"],
event_data["data"],
event_data["source"],
event_system
)
obj._id = event_data["id"]
return obj
class EventSystem(object):
"""Encapsulate event handling into an object.

View file

@ -32,6 +32,9 @@ from maya import cmds
from openpype.pipeline import legacy_io
from openpype.hosts.maya.api.lib_rendersettings import RenderSettings
from openpype.hosts.maya.api.lib import get_attr_in_layer
from openpype_modules.deadline import abstract_submit_deadline
from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
@ -498,9 +501,10 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline):
job_info.AssetDependency += self.scene_path
# Get layer prefix
render_products = self._instance.data["renderProducts"]
layer_metadata = render_products.layer_data
layer_prefix = layer_metadata.filePrefix
renderlayer = self._instance.data["setMembers"]
renderer = self._instance.data["renderer"]
layer_prefix_attr = RenderSettings.get_image_prefix_attr(renderer)
layer_prefix = get_attr_in_layer(layer_prefix_attr, layer=renderlayer)
plugin_info = copy.deepcopy(self.plugin_info)
plugin_info.update({

View file

@ -6,8 +6,17 @@ import collections
import numbers
import six
import time
from openpype.settings.lib import get_anatomy_settings
from openpype.settings.lib import (
get_project_settings,
get_local_settings,
)
from openpype.settings.constants import (
DEFAULT_PROJECT_KEY
)
from openpype.client import get_project
from openpype.lib.path_templates import (
TemplateUnsolved,
TemplateResult,
@ -39,34 +48,23 @@ class RootCombinationError(Exception):
super(RootCombinationError, self).__init__(msg)
class Anatomy:
class BaseAnatomy(object):
"""Anatomy module helps to keep project settings.
Wraps key project specifications, AnatomyTemplates and Roots.
Args:
project_name (str): Project name to look on overrides.
"""
root_key_regex = re.compile(r"{(root?[^}]+)}")
root_name_regex = re.compile(r"root\[([^]]+)\]")
def __init__(self, project_name=None, site_name=None):
if not project_name:
project_name = os.environ.get("AVALON_PROJECT")
if not project_name:
raise ProjectNotSet((
"Implementation bug: Project name is not set. Anatomy requires"
" to load data for specific project."
))
def __init__(self, project_doc, local_settings, site_name):
project_name = project_doc["name"]
self.project_name = project_name
self._data = self._prepare_anatomy_data(
get_anatomy_settings(project_name, site_name)
)
self._site_name = site_name
self._data = self._prepare_anatomy_data(
project_doc, local_settings, site_name
)
self._templates_obj = AnatomyTemplates(self)
self._roots_obj = Roots(self)
@ -87,12 +85,14 @@ class Anatomy:
def items(self):
return copy.deepcopy(self._data).items()
@staticmethod
def _prepare_anatomy_data(anatomy_data):
def _prepare_anatomy_data(self, project_doc, local_settings, site_name):
"""Prepare anatomy data for further processing.
Method added to replace `{task}` with `{task[name]}` in templates.
"""
project_name = project_doc["name"]
anatomy_data = self._project_doc_to_anatomy_data(project_doc)
templates_data = anatomy_data.get("templates")
if templates_data:
# Replace `{task}` with `{task[name]}` in templates
@ -103,23 +103,13 @@ class Anatomy:
if not isinstance(item, dict):
continue
for key in tuple(item.keys()):
value = item[key]
if isinstance(value, dict):
value_queue.append(value)
self._apply_local_settings_on_anatomy_data(anatomy_data,
local_settings,
project_name,
site_name)
elif isinstance(value, six.string_types):
item[key] = value.replace("{task}", "{task[name]}")
return anatomy_data
def reset(self):
"""Reset values of cached data in templates and roots objects."""
self._data = self._prepare_anatomy_data(
get_anatomy_settings(self.project_name, self._site_name)
)
self.templates_obj.reset()
self.roots_obj.reset()
@property
def templates(self):
"""Wrap property `templates` of Anatomy's AnatomyTemplates instance."""
@ -338,6 +328,161 @@ class Anatomy:
data = self.root_environmets_fill_data(template)
return rootless_path.format(**data)
def _project_doc_to_anatomy_data(self, project_doc):
"""Convert project document to anatomy data.
Probably should fill missing keys and values.
"""
output = copy.deepcopy(project_doc["config"])
output["attributes"] = copy.deepcopy(project_doc["data"])
return output
def _apply_local_settings_on_anatomy_data(
self, anatomy_data, local_settings, project_name, site_name
):
"""Apply local settings on anatomy data.
ATM local settings can modify project roots. Project name is required
as local settings have data stored data by project's name.
Local settings override root values in this order:
1.) Check if local settings contain overrides for default project and
apply it's values on roots if there are any.
2.) If passed `project_name` is not None then check project specific
overrides in local settings for the project and apply it's value on
roots if there are any.
NOTE: Root values of default project from local settings are always
applied if are set.
Args:
anatomy_data (dict): Data for anatomy.
local_settings (dict): Data of local settings.
project_name (str): Name of project for which anatomy data are.
"""
if not local_settings:
return
local_project_settings = local_settings.get("projects") or {}
# Check for roots existence in local settings first
roots_project_locals = (
local_project_settings
.get(project_name, {})
)
roots_default_locals = (
local_project_settings
.get(DEFAULT_PROJECT_KEY, {})
)
# Skip rest of processing if roots are not set
if not roots_project_locals and not roots_default_locals:
return
# Combine roots from local settings
roots_locals = roots_default_locals.get(site_name) or {}
roots_locals.update(roots_project_locals.get(site_name) or {})
# Skip processing if roots for current active site are not available in
# local settings
if not roots_locals:
return
current_platform = platform.system().lower()
root_data = anatomy_data["roots"]
for root_name, path in roots_locals.items():
if root_name not in root_data:
continue
anatomy_data["roots"][root_name][current_platform] = (
path
)
class Anatomy(BaseAnatomy):
_project_cache = {}
_site_cache = {}
def __init__(self, project_name=None, site_name=None):
if not project_name:
project_name = os.environ.get("AVALON_PROJECT")
if not project_name:
raise ProjectNotSet((
"Implementation bug: Project name is not set. Anatomy requires"
" to load data for specific project."
))
project_doc = self.get_project_doc_from_cache(project_name)
local_settings = get_local_settings()
if not site_name:
site_name = self.get_site_name_from_cache(
project_name, local_settings
)
super(Anatomy, self).__init__(
project_doc,
local_settings,
site_name
)
@classmethod
def get_project_doc_from_cache(cls, project_name):
project_cache = cls._project_cache.get(project_name)
if project_cache is not None:
if time.time() - project_cache["start"] > 10:
cls._project_cache.pop(project_name)
project_cache = None
if project_cache is None:
project_cache = {
"project_doc": get_project(project_name),
"start": time.time()
}
cls._project_cache[project_name] = project_cache
return copy.deepcopy(
cls._project_cache[project_name]["project_doc"]
)
@classmethod
def get_site_name_from_cache(cls, project_name, local_settings):
site_cache = cls._site_cache.get(project_name)
if site_cache is not None:
if time.time() - site_cache["start"] > 10:
cls._site_cache.pop(project_name)
site_cache = None
if site_cache:
return site_cache["site_name"]
local_project_settings = local_settings.get("projects")
if not local_project_settings:
return
project_locals = local_project_settings.get(project_name) or {}
default_locals = local_project_settings.get(DEFAULT_PROJECT_KEY) or {}
active_site = (
project_locals.get("active_site")
or default_locals.get("active_site")
)
if not active_site:
project_settings = get_project_settings(project_name)
active_site = (
project_settings
["global"]
["sync_server"]
["config"]
["active_site"]
)
cls._site_cache[project_name] = {
"site_name": active_site,
"start": time.time()
}
return active_site
class AnatomyTemplateUnsolved(TemplateUnsolved):
"""Exception for unsolved template when strict is set to True."""

View file

@ -200,6 +200,16 @@ class AttributeValues:
def changes(self):
return self.calculate_changes(self._data, self._origin_data)
def apply_changes(self, changes):
for key, item in changes.items():
old_value, new_value = item
if new_value is None:
if key in self:
self.pop(key)
elif self.get(key) != new_value:
self[key] = new_value
class CreatorAttributeValues(AttributeValues):
"""Creator specific attribute values of an instance.
@ -333,6 +343,21 @@ class PublishAttributes:
changes[key] = (value, None)
return changes
def apply_changes(self, changes):
for key, item in changes.items():
if isinstance(item, dict):
self._data[key].apply_changes(item)
continue
old_value, new_value = item
if new_value is not None:
raise ValueError(
"Unexpected type \"{}\" expected None".format(
str(type(new_value))
)
)
self.pop(key)
def set_publish_plugins(self, attr_plugins):
"""Set publish plugins attribute definitions."""
@ -730,6 +755,97 @@ class CreatedInstance:
if member not in self._members:
self._members.append(member)
def serialize_for_remote(self):
return {
"data": self.data_to_store(),
"orig_data": copy.deepcopy(self._orig_data)
}
@classmethod
def deserialize_on_remote(cls, serialized_data, creator_items):
"""Convert instance data to CreatedInstance.
This is fake instance in remote process e.g. in UI process. The creator
is not a full creator and should not be used for calling methods when
instance is created from this method (matters on implementation).
Args:
serialized_data (Dict[str, Any]): Serialized data for remote
recreating. Should contain 'data' and 'orig_data'.
creator_items (Dict[str, Any]): Mapping of creator identifier and
objects that behave like a creator for most of attribute
access.
"""
instance_data = copy.deepcopy(serialized_data["data"])
creator_identifier = instance_data["creator_identifier"]
creator_item = creator_items[creator_identifier]
family = instance_data.get("family", None)
if family is None:
family = creator_item.family
subset_name = instance_data.get("subset", None)
obj = cls(
family, subset_name, instance_data, creator_item, new=False
)
obj._orig_data = serialized_data["orig_data"]
return obj
def remote_changes(self):
"""Prepare serializable changes on remote side.
Returns:
Dict[str, Any]: Prepared changes that can be send to client side.
"""
return {
"changes": self.changes(),
"asset_is_valid": self._asset_is_valid,
"task_is_valid": self._task_is_valid,
}
def update_from_remote(self, remote_changes):
"""Apply changes from remote side on client side.
Args:
remote_changes (Dict[str, Any]): Changes created on remote side.
"""
self._asset_is_valid = remote_changes["asset_is_valid"]
self._task_is_valid = remote_changes["task_is_valid"]
changes = remote_changes["changes"]
creator_attributes = changes.pop("creator_attributes", None) or {}
publish_attributes = changes.pop("publish_attributes", None) or {}
if changes:
self.apply_changes(changes)
if creator_attributes:
self.creator_attributes.apply_changes(creator_attributes)
if publish_attributes:
self.publish_attributes.apply_changes(publish_attributes)
def apply_changes(self, changes):
"""Apply changes created via 'changes'.
Args:
Dict[str, Tuple[Any, Any]]: Instance changes to apply. Same values
are kept untouched.
"""
for key, item in changes.items():
old_value, new_value = item
if new_value is None:
if key in self:
self.pop(key)
else:
current_value = self.get(key)
if current_value != new_value:
self[key] = new_value
class CreateContext:
"""Context of instance creation.
@ -817,6 +933,10 @@ class CreateContext:
def instances(self):
return self._instances_by_id.values()
@property
def instances_by_id(self):
return self._instances_by_id
@property
def publish_attributes(self):
"""Access to global publish attributes."""
@ -976,7 +1096,8 @@ class CreateContext:
and creator_class.host_name != self.host_name
):
self.log.info((
"Creator's host name is not supported for current host {}"
"Creator's host name \"{}\""
" is not supported for current host \"{}\""
).format(creator_class.host_name, self.host_name))
continue

View file

@ -126,6 +126,7 @@
"CreateAnimation": {
"enabled": true,
"write_color_sets": false,
"write_face_sets": false,
"defaults": [
"Main"
]
@ -133,6 +134,7 @@
"CreatePointCache": {
"enabled": true,
"write_color_sets": false,
"write_face_sets": false,
"defaults": [
"Main"
]
@ -187,6 +189,8 @@
},
"CreateModel": {
"enabled": true,
"write_color_sets": false,
"write_face_sets": false,
"defaults": [
"Main",
"Proxy",

View file

@ -127,6 +127,41 @@
"key": "write_color_sets",
"label": "Write Color Sets"
},
{
"type": "boolean",
"key": "write_face_sets",
"label": "Write Face Sets"
},
{
"type": "list",
"key": "defaults",
"label": "Default Subsets",
"object_type": "text"
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "CreateModel",
"label": "Create Model",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "write_color_sets",
"label": "Write Color Sets"
},
{
"type": "boolean",
"key": "write_face_sets",
"label": "Write Face Sets"
},
{
"type": "list",
"key": "defaults",
@ -152,6 +187,11 @@
"key": "write_color_sets",
"label": "Write Color Sets"
},
{
"type": "boolean",
"key": "write_face_sets",
"label": "Write Face Sets"
},
{
"type": "list",
"key": "defaults",
@ -160,7 +200,7 @@
}
]
},
{
"type": "schema_template",
"name": "template_create_plugin",
@ -197,10 +237,6 @@
"key": "CreateMayaScene",
"label": "Create Maya Scene"
},
{
"key": "CreateModel",
"label": "Create Model"
},
{
"key": "CreateRenderSetup",
"label": "Create Render Setup"

View file

@ -973,23 +973,22 @@ VariantInputsWidget QToolButton {
background: {color:bg};
border-radius: 0.3em;
}
#PublishInfoFrame[state="-1"] {
background: rgb(194, 226, 236);
}
#PublishInfoFrame[state="0"] {
background: {color:publisher:crash};
background: {color:publisher:success};
}
#PublishInfoFrame[state="1"] {
background: {color:publisher:success};
background: {color:publisher:crash};
}
#PublishInfoFrame[state="2"] {
background: {color:publisher:warning};
}
#PublishInfoFrame[state="3"], #PublishInfoFrame[state="4"] {
background: rgb(194, 226, 236);
}
#PublishInfoFrame QLabel {
color: black;
font-style: bold;
@ -1086,7 +1085,7 @@ ValidationArtistMessage QLabel {
border-color: {color:publisher:error};
}
#PublishProgressBar[state="0"]::chunk {
#PublishProgressBar[state="1"]::chunk, #PublishProgressBar[state="4"]::chunk {
background: {color:bg-buttons};
}

View file

@ -1,7 +0,0 @@
from .app import show
from .window import PublisherWindow
__all__ = (
"show",
"PublisherWindow"
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,405 @@
import collections
from abc import abstractmethod, abstractproperty
from Qt import QtCore
from openpype.lib.events import Event
from openpype.pipeline.create import CreatedInstance
from .control import (
MainThreadItem,
PublisherController,
BasePublisherController,
)
class MainThreadProcess(QtCore.QObject):
"""Qt based main thread process executor.
Has timer which controls each 50ms if there is new item to process.
This approach gives ability to update UI meanwhile plugin is in progress.
"""
count_timeout = 2
def __init__(self):
super(MainThreadProcess, self).__init__()
self._items_to_process = collections.deque()
timer = QtCore.QTimer()
timer.setInterval(0)
timer.timeout.connect(self._execute)
self._timer = timer
self._switch_counter = self.count_timeout
def process(self, func, *args, **kwargs):
item = MainThreadItem(func, *args, **kwargs)
self.add_item(item)
def add_item(self, item):
self._items_to_process.append(item)
def _execute(self):
if not self._items_to_process:
return
if self._switch_counter > 0:
self._switch_counter -= 1
return
self._switch_counter = self.count_timeout
item = self._items_to_process.popleft()
item.process()
def start(self):
if not self._timer.isActive():
self._timer.start()
def stop(self):
if self._timer.isActive():
self._timer.stop()
def clear(self):
if self._timer.isActive():
self._timer.stop()
self._items_to_process = collections.deque()
class QtPublisherController(PublisherController):
def __init__(self, *args, **kwargs):
self._main_thread_processor = MainThreadProcess()
super(QtPublisherController, self).__init__(*args, **kwargs)
self.event_system.add_callback(
"publish.process.started", self._qt_on_publish_start
)
self.event_system.add_callback(
"publish.process.stopped", self._qt_on_publish_stop
)
def _reset_publish(self):
super(QtPublisherController, self)._reset_publish()
self._main_thread_processor.clear()
def _process_main_thread_item(self, item):
self._main_thread_processor.add_item(item)
def _qt_on_publish_start(self):
self._main_thread_processor.start()
def _qt_on_publish_stop(self):
self._main_thread_processor.stop()
class QtRemotePublishController(BasePublisherController):
"""Abstract Remote controller for Qt UI.
This controller should be used in process where UI is running and should
listen and ask for data on a client side.
All objects that are used during UI processing should be able to convert
on client side to json serializable data and then recreated here. Keep in
mind that all changes made here should be send back to client controller
before critical actions.
ATM Was not tested and will require some changes. All code written here is
based on theoretical idea how it could work.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._created_instances = {}
@abstractmethod
def _get_serialized_instances(self):
"""Receive serialized instances from client process.
Returns:
List[Dict[str, Any]]: Serialized instances.
"""
pass
def _on_create_instance_change(self):
serialized_instances = self._get_serialized_instances()
created_instances = {}
for serialized_data in serialized_instances:
item = CreatedInstance.deserialize_on_remote(
serialized_data,
self._creator_items
)
created_instances[item.id] = item
self._created_instances = created_instances
self._emit_event("instances.refresh.finished")
def remote_events_handler(self, event_data):
event = Event.from_data(event_data)
# Topics that cause "replication" of controller changes
if event.topic == "publish.max_progress.changed":
self.publish_max_progress = event["value"]
return
if event.topic == "publish.progress.changed":
self.publish_progress = event["value"]
return
if event.topic == "publish.has_validated.changed":
self.publish_has_validated = event["value"]
return
if event.topic == "publish.is_running.changed":
self.publish_is_running = event["value"]
return
if event.topic == "publish.publish_error.changed":
self.publish_error_msg = event["value"]
return
if event.topic == "publish.has_crashed.changed":
self.publish_has_crashed = event["value"]
return
if event.topic == "publish.has_validation_errors.changed":
self.publish_has_validation_errors = event["value"]
return
if event.topic == "publish.finished.changed":
self.publish_has_finished = event["value"]
return
if event.topic == "publish.host_is_valid.changed":
self.host_is_valid = event["value"]
return
# Topics that can be just passed by because are not affecting
# controller itself
# - "show.card.message"
# - "show.detailed.help"
# - "publish.reset.finished"
# - "instances.refresh.finished"
# - "plugins.refresh.finished"
# - "controller.reset.finished"
# - "publish.process.started"
# - "publish.process.stopped"
# - "publish.process.plugin.changed"
# - "publish.process.instance.changed"
self.event_system.emit_event(event)
@abstractproperty
def project_name(self):
"""Current context project name from client.
Returns:
str: Name of project.
"""
pass
@abstractproperty
def current_asset_name(self):
"""Current context asset name from client.
Returns:
Union[str, None]: Name of asset.
"""
pass
@abstractproperty
def current_task_name(self):
"""Current context task name from client.
Returns:
Union[str, None]: Name of task.
"""
pass
@property
def instances(self):
"""Collected/created instances.
Returns:
List[CreatedInstance]: List of created instances.
"""
return self._created_instances
def get_context_title(self):
"""Get context title for artist shown at the top of main window.
Returns:
Union[str, None]: Context title for window or None. In case of None
a warning is displayed (not nice for artists).
"""
pass
def get_asset_docs(self):
pass
def get_asset_hierarchy(self):
pass
def get_task_names_by_asset_names(self, asset_names):
pass
def get_existing_subset_names(self, asset_name):
pass
@abstractmethod
def get_subset_name(
self,
creator_identifier,
variant,
task_name,
asset_name,
instance_id=None
):
"""Get subset name based on passed data.
Args:
creator_identifier (str): Identifier of creator which should be
responsible for subset name creation.
variant (str): Variant value from user's input.
task_name (str): Name of task for which is instance created.
asset_name (str): Name of asset for which is instance created.
instance_id (Union[str, None]): Existing instance id when subset
name is updated.
"""
pass
@abstractmethod
def create(
self, creator_identifier, subset_name, instance_data, options
):
"""Trigger creation by creator identifier.
Should also trigger refresh of instanes.
Args:
creator_identifier (str): Identifier of Creator plugin.
subset_name (str): Calculated subset name.
instance_data (Dict[str, Any]): Base instance data with variant,
asset name and task name.
options (Dict[str, Any]): Data from pre-create attributes.
"""
pass
def _get_instance_changes_for_client(self):
"""Preimplemented method to receive instance changes for client."""
created_instance_changes = {}
for instance_id, instance in self._created_instances.items():
created_instance_changes[instance_id] = (
instance.remote_changes()
)
return created_instance_changes
@abstractmethod
def _send_instance_changes_to_client(self):
instance_changes = self._get_instance_changes_for_client()
# Implement to send 'instance_changes' value to client
@abstractmethod
def save_changes(self):
"""Save changes happened during creation."""
self._send_instance_changes_to_client()
@abstractmethod
def remove_instances(self, instance_ids):
"""Remove list of instances from create context."""
# TODO add Args:
pass
@abstractmethod
def get_publish_report(self):
pass
@abstractmethod
def get_validation_errors(self):
pass
@abstractmethod
def reset(self):
"""Reset whole controller.
This should reset create context, publish context and all variables
that are related to it.
"""
self._send_instance_changes_to_client()
pass
@abstractmethod
def publish(self):
"""Trigger publishing without any order limitations."""
self._send_instance_changes_to_client()
pass
@abstractmethod
def validate(self):
"""Trigger publishing which will stop after validation order."""
self._send_instance_changes_to_client()
pass
@abstractmethod
def stop_publish(self):
"""Stop publishing can be also used to pause publishing.
Pause of publishing is possible only if all plugins successfully
finished.
"""
pass
@abstractmethod
def run_action(self, plugin_id, action_id):
"""Trigger pyblish action on a plugin.
Args:
plugin_id (str): Id of publish plugin.
action_id (str): Id of publish action.
"""
pass
@abstractmethod
def set_comment(self, comment):
"""Set comment on pyblish context.
Set "comment" key on current pyblish.api.Context data.
Args:
comment (str): Artist's comment.
"""
pass
@abstractmethod
def emit_card_message(self, message):
"""Emit a card message which can have a lifetime.
This is for UI purposes. Method can be extended to more arguments
in future e.g. different message timeout or type (color).
Args:
message (str): Message that will be showed.
"""
pass

View file

@ -139,6 +139,9 @@ class PluginLoadReportWidget(QtWidgets.QWidget):
class ZoomPlainText(QtWidgets.QPlainTextEdit):
min_point_size = 1.0
max_point_size = 200.0
def __init__(self, *args, **kwargs):
super(ZoomPlainText, self).__init__(*args, **kwargs)
@ -148,12 +151,12 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit):
anim_timer.timeout.connect(self._scaling_callback)
self._anim_timer = anim_timer
self._zoom_enabled = False
self._scheduled_scalings = 0
self._point_size = None
def wheelEvent(self, event):
if not self._zoom_enabled:
modifiers = QtWidgets.QApplication.keyboardModifiers()
if modifiers != QtCore.Qt.ControlModifier:
super(ZoomPlainText, self).wheelEvent(event)
return
@ -172,33 +175,40 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit):
factor = 1.0 + (self._scheduled_scalings / 300)
font = self.font()
if self._point_size is None:
self._point_size = font.pointSizeF()
point_size = font.pointSizeF()
else:
point_size = self._point_size
self._point_size *= factor
if self._point_size < 1:
self._point_size = 1.0
point_size *= factor
min_hit = False
max_hit = False
if point_size < self.min_point_size:
point_size = self.min_point_size
min_hit = True
elif point_size > self.max_point_size:
point_size = self.max_point_size
max_hit = True
font.setPointSizeF(self._point_size)
self._point_size = point_size
font.setPointSizeF(point_size)
# Using 'self.setFont(font)' would not be propagated when stylesheets
# are applied on this widget
self.setStyleSheet("font-size: {}pt".format(font.pointSize()))
if self._scheduled_scalings > 0:
if (
(max_hit and self._scheduled_scalings > 0)
or (min_hit and self._scheduled_scalings < 0)
):
self._scheduled_scalings = 0
elif self._scheduled_scalings > 0:
self._scheduled_scalings -= 1
else:
self._scheduled_scalings += 1
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Control:
self._zoom_enabled = True
super(ZoomPlainText, self).keyPressEvent(event)
def keyReleaseEvent(self, event):
if event.key() == QtCore.Qt.Key_Control:
self._zoom_enabled = False
super(ZoomPlainText, self).keyReleaseEvent(event)
class DetailsWidget(QtWidgets.QWidget):
def __init__(self, parent):

View file

@ -1,6 +1,7 @@
import collections
from Qt import QtWidgets, QtCore, QtGui
from openpype.tools.utils import (
PlaceholderLineEdit,
RecursiveSortFilterProxyModel,
@ -163,6 +164,16 @@ class AssetsHierarchyModel(QtGui.QStandardItemModel):
return item_name in self._items_by_name
class AssetDialogView(QtWidgets.QTreeView):
double_clicked = QtCore.Signal(QtCore.QModelIndex)
def mouseDoubleClickEvent(self, event):
index = self.indexAt(event.pos())
if index.isValid():
self.double_clicked.emit(index)
event.accept()
class AssetsDialog(QtWidgets.QDialog):
"""Dialog to select asset for a context of instance."""
@ -178,7 +189,7 @@ class AssetsDialog(QtWidgets.QDialog):
filter_input = PlaceholderLineEdit(self)
filter_input.setPlaceholderText("Filter assets..")
asset_view = QtWidgets.QTreeView(self)
asset_view = AssetDialogView(self)
asset_view.setModel(proxy_model)
asset_view.setHeaderHidden(True)
asset_view.setFrameShape(QtWidgets.QFrame.NoFrame)
@ -200,6 +211,7 @@ class AssetsDialog(QtWidgets.QDialog):
layout.addWidget(asset_view, 1)
layout.addLayout(btns_layout, 0)
asset_view.double_clicked.connect(self._on_ok_clicked)
filter_input.textChanged.connect(self._on_filter_change)
ok_btn.clicked.connect(self._on_ok_clicked)
cancel_btn.clicked.connect(self._on_cancel_clicked)
@ -274,7 +286,7 @@ class AssetsDialog(QtWidgets.QDialog):
index = self._asset_view.currentIndex()
asset_name = None
if index.isValid():
asset_name = index.data(QtCore.Qt.DisplayRole)
asset_name = index.data(ASSET_NAME_ROLE)
self._selected_asset = asset_name
self.done(1)

View file

@ -41,9 +41,26 @@ from ..constants import (
)
class SelectionType:
def __init__(self, name):
self.name = name
def __eq__(self, other):
if isinstance(other, SelectionType):
other = other.name
return self.name == other
class SelectionTypes:
clear = SelectionType("clear")
extend = SelectionType("extend")
extend_to = SelectionType("extend_to")
class GroupWidget(QtWidgets.QWidget):
"""Widget wrapping instances under group."""
selected = QtCore.Signal(str, str)
selected = QtCore.Signal(str, str, SelectionType)
active_changed = QtCore.Signal()
removed_selected = QtCore.Signal()
@ -72,21 +89,73 @@ class GroupWidget(QtWidgets.QWidget):
self._group_icons = group_icons
self._widgets_by_id = {}
self._ordered_instance_ids = []
self._label_widget = label_widget
self._content_layout = layout
@property
def group_name(self):
"""Group which widget represent.
Returns:
str: Name of group.
"""
return self._group
def get_selected_instance_ids(self):
"""Selected instance ids.
Returns:
Set[str]: Instance ids that are selected.
"""
return {
instance_id
for instance_id, widget in self._widgets_by_id.items()
if widget.is_selected
}
def get_selected_widgets(self):
"""Access to widgets marked as selected.
Returns:
List[InstanceCardWidget]: Instance widgets that are selected.
"""
return [
widget
for instance_id, widget in self._widgets_by_id.items()
if widget.is_selected
]
def get_ordered_widgets(self):
"""Get instance ids in order as are shown in ui.
Returns:
List[str]: Instance ids.
"""
return [
self._widgets_by_id[instance_id]
for instance_id in self._ordered_instance_ids
]
def get_widget_by_instance_id(self, instance_id):
"""Get instance widget by it's id."""
return self._widgets_by_id.get(instance_id)
def update_instance_values(self):
"""Trigger update on instance widgets."""
for widget in self._widgets_by_id.values():
widget.update_instance_values()
def confirm_remove_instance_id(self, instance_id):
"""Delete widget by instance id."""
widget = self._widgets_by_id.pop(instance_id)
widget.setVisible(False)
self._content_layout.removeWidget(widget)
@ -123,6 +192,7 @@ class GroupWidget(QtWidgets.QWidget):
# Sort instances by subset name
sorted_subset_names = list(sorted(instances_by_subset_name.keys()))
# Add new instances to widget
widget_idx = 1
for subset_names in sorted_subset_names:
@ -135,17 +205,30 @@ class GroupWidget(QtWidgets.QWidget):
widget = InstanceCardWidget(
instance, group_icon, self
)
widget.selected.connect(self.selected)
widget.selected.connect(self._on_widget_selection)
widget.active_changed.connect(self.active_changed)
self._widgets_by_id[instance.id] = widget
self._content_layout.insertWidget(widget_idx, widget)
widget_idx += 1
ordered_instance_ids = []
for idx in range(self._content_layout.count()):
if idx > 0:
item = self._content_layout.itemAt(idx)
widget = item.widget()
if widget is not None:
ordered_instance_ids.append(widget.id)
self._ordered_instance_ids = ordered_instance_ids
def _on_widget_selection(self, instance_id, group_id, selection_type):
self.selected.emit(instance_id, group_id, selection_type)
class CardWidget(BaseClickableFrame):
"""Clickable card used as bigger button."""
selected = QtCore.Signal(str, str)
selected = QtCore.Signal(str, str, SelectionType)
# Group identifier of card
# - this must be set because if send when mouse is released with card id
_group_identifier = None
@ -157,6 +240,12 @@ class CardWidget(BaseClickableFrame):
self._selected = False
self._id = None
@property
def id(self):
"""Id of card."""
return self._id
@property
def is_selected(self):
"""Is card selected."""
@ -173,7 +262,16 @@ class CardWidget(BaseClickableFrame):
def _mouse_release_callback(self):
"""Trigger selected signal."""
self.selected.emit(self._id, self._group_identifier)
modifiers = QtWidgets.QApplication.keyboardModifiers()
selection_type = SelectionTypes.clear
if bool(modifiers & QtCore.Qt.ShiftModifier):
selection_type = SelectionTypes.extend_to
elif bool(modifiers & QtCore.Qt.ControlModifier):
selection_type = SelectionTypes.extend
self.selected.emit(self._id, self._group_identifier, selection_type)
class ContextCardWidget(CardWidget):
@ -351,10 +449,11 @@ class InstanceCardView(AbstractInstanceView):
Wrapper of all widgets in card view.
"""
def __init__(self, controller, parent):
super(InstanceCardView, self).__init__(parent)
self.controller = controller
self._controller = controller
scroll_area = QtWidgets.QScrollArea(self)
scroll_area.setWidgetResizable(True)
@ -381,11 +480,12 @@ class InstanceCardView(AbstractInstanceView):
self._content_layout = content_layout
self._content_widget = content_widget
self._widgets_by_group = {}
self._context_widget = None
self._widgets_by_group = {}
self._ordered_groups = []
self._selected_group = None
self._selected_instance_id = None
self._explicitly_selected_instance_ids = []
self._explicitly_selected_groups = []
self.setSizePolicy(
QtWidgets.QSizePolicy.Minimum,
@ -405,21 +505,30 @@ class InstanceCardView(AbstractInstanceView):
result.setWidth(width)
return result
def _get_selected_widget(self):
if self._selected_instance_id == CONTEXT_ID:
return self._context_widget
def _get_selected_widgets(self):
output = []
if (
self._context_widget is not None
and self._context_widget.is_selected
):
output.append(self._context_widget)
group_widget = self._widgets_by_group.get(
self._selected_group
)
if group_widget is not None:
widget = group_widget.get_widget_by_instance_id(
self._selected_instance_id
)
if widget is not None:
return widget
for group_widget in self._widgets_by_group.values():
for widget in group_widget.get_selected_widgets():
output.append(widget)
return output
return None
def _get_selected_instance_ids(self):
output = []
if (
self._context_widget is not None
and self._context_widget.is_selected
):
output.append(CONTEXT_ID)
for group_widget in self._widgets_by_group.values():
output.extend(group_widget.get_selected_instance_ids())
return output
def refresh(self):
"""Refresh instances in view based on CreatedContext."""
@ -435,12 +544,10 @@ class InstanceCardView(AbstractInstanceView):
self.selection_changed.emit()
self._content_layout.insertWidget(0, widget)
self.select_item(CONTEXT_ID, None)
# Prepare instances by group and identifiers by group
instances_by_group = collections.defaultdict(list)
identifiers_by_group = collections.defaultdict(set)
for instance in self.controller.instances:
for instance in self._controller.instances.values():
group_name = instance.group_label
instances_by_group[group_name].append(instance)
identifiers_by_group[group_name].add(
@ -452,15 +559,17 @@ class InstanceCardView(AbstractInstanceView):
if group_name in instances_by_group:
continue
if group_name == self._selected_group:
self._on_remove_selected()
widget = self._widgets_by_group.pop(group_name)
widget.setVisible(False)
self._content_layout.removeWidget(widget)
widget.deleteLater()
if group_name in self._explicitly_selected_groups:
self._explicitly_selected_groups.remove(group_name)
# Sort groups
sorted_group_names = list(sorted(instances_by_group.keys()))
# Keep track of widget indexes
# - we start with 1 because Context item as at the top
widget_idx = 1
@ -469,7 +578,7 @@ class InstanceCardView(AbstractInstanceView):
group_widget = self._widgets_by_group[group_name]
else:
group_icons = {
idenfier: self.controller.get_icon_for_family(idenfier)
idenfier: self._controller.get_creator_icon(idenfier)
for idenfier in identifiers_by_group[group_name]
}
@ -478,9 +587,6 @@ class InstanceCardView(AbstractInstanceView):
)
group_widget.active_changed.connect(self._on_active_changed)
group_widget.selected.connect(self._on_widget_selection)
group_widget.removed_selected.connect(
self._on_remove_selected
)
self._content_layout.insertWidget(widget_idx, group_widget)
self._widgets_by_group[group_name] = group_widget
@ -489,6 +595,16 @@ class InstanceCardView(AbstractInstanceView):
instances_by_group[group_name]
)
ordered_group_names = [""]
for idx in range(self._content_layout.count()):
if idx > 0:
item = self._content_layout.itemAt(idx)
group_widget = item.widget()
if group_widget is not None:
ordered_group_names.append(group_widget.group_name)
self._ordered_groups = ordered_group_names
def refresh_instance_states(self):
"""Trigger update of instances on group widgets."""
for widget in self._widgets_by_group.values():
@ -497,10 +613,7 @@ class InstanceCardView(AbstractInstanceView):
def _on_active_changed(self):
self.active_changed.emit()
def _on_widget_selection(self, instance_id, group_name):
self.select_item(instance_id, group_name)
def select_item(self, instance_id, group_name):
def _on_widget_selection(self, instance_id, group_name, selection_type):
"""Select specific item by instance id.
Pass `CONTEXT_ID` as instance id and empty string as group to select
@ -512,34 +625,318 @@ class InstanceCardView(AbstractInstanceView):
group_widget = self._widgets_by_group[group_name]
new_widget = group_widget.get_widget_by_instance_id(instance_id)
selected_widget = self._get_selected_widget()
if new_widget is selected_widget:
return
if selected_widget is not None:
selected_widget.set_selected(False)
self._selected_instance_id = instance_id
self._selected_group = group_name
if new_widget is not None:
new_widget.set_selected(True)
if selection_type is SelectionTypes.clear:
self._select_item_clear(instance_id, group_name, new_widget)
elif selection_type is SelectionTypes.extend:
self._select_item_extend(instance_id, group_name, new_widget)
elif selection_type is SelectionTypes.extend_to:
self._select_item_extend_to(instance_id, group_name, new_widget)
self.selection_changed.emit()
def _on_remove_selected(self):
selected_widget = self._get_selected_widget()
if selected_widget is None:
self._on_widget_selection(CONTEXT_ID, None)
def _select_item_clear(self, instance_id, group_name, new_widget):
"""Select specific item by instance id and clear previous selection.
Pass `CONTEXT_ID` as instance id and empty string as group to select
global context item.
"""
selected_widgets = self._get_selected_widgets()
for widget in selected_widgets:
if widget.id != instance_id:
widget.set_selected(False)
self._explicitly_selected_groups = [group_name]
self._explicitly_selected_instance_ids = [instance_id]
if new_widget is not None:
new_widget.set_selected(True)
def _select_item_extend(self, instance_id, group_name, new_widget):
"""Add/Remove single item to/from current selection.
If item is already selected the selection is removed.
"""
self._explicitly_selected_instance_ids = (
self._get_selected_instance_ids()
)
if new_widget.is_selected:
self._explicitly_selected_instance_ids.remove(instance_id)
new_widget.set_selected(False)
remove_group = False
if instance_id == CONTEXT_ID:
remove_group = True
else:
group_widget = self._widgets_by_group[group_name]
if not group_widget.get_selected_widgets():
remove_group = True
if remove_group:
self._explicitly_selected_groups.remove(group_name)
return
self._explicitly_selected_instance_ids.append(instance_id)
if group_name in self._explicitly_selected_groups:
self._explicitly_selected_groups.remove(group_name)
self._explicitly_selected_groups.append(group_name)
new_widget.set_selected(True)
def _select_item_extend_to(self, instance_id, group_name, new_widget):
"""Extend selected items to specific instance id.
This method is handling Shift+click selection of widgets. Selection
is not stored to explicit selection items. That's because user can
shift select again and it should use last explicit selected item as
source item for selection.
Items selected via this function can get to explicit selection only if
selection is extended by one specific item ('_select_item_extend').
From that moment the selection is locked to new last explicit selected
item.
It's required to traverse through group widgets in their UI order and
through their instances in UI order. All explicitly selected items
must not change their selection state during this function. Passed
instance id can be above or under last selected item so a start item
and end item must be found to be able know which direction is selection
happening.
"""
# Start group name (in '_ordered_groups')
start_group = None
# End group name (in '_ordered_groups')
end_group = None
# Instance id of first selected item
start_instance_id = None
# Instance id of last selected item
end_instance_id = None
# Get previously selected group by explicit selected groups
previous_group = None
if self._explicitly_selected_groups:
previous_group = self._explicitly_selected_groups[-1]
# Find last explicitly selected instance id
previous_last_selected_id = None
if self._explicitly_selected_instance_ids:
previous_last_selected_id = (
self._explicitly_selected_instance_ids[-1]
)
# If last instance id was not found or available then last selected
# group is also invalid.
# NOTE: This probably never happen?
if previous_last_selected_id is None:
previous_group = None
# Check if previously selected group is available and find out if
# new instance group is above or under previous selection
# - based on these information are start/end group/instance filled
if previous_group in self._ordered_groups:
new_idx = self._ordered_groups.index(group_name)
prev_idx = self._ordered_groups.index(previous_group)
if new_idx < prev_idx:
start_group = group_name
end_group = previous_group
start_instance_id = instance_id
end_instance_id = previous_last_selected_id
else:
start_group = previous_group
end_group = group_name
start_instance_id = previous_last_selected_id
end_instance_id = instance_id
# If start group is not set then use context item group name
if start_group is None:
start_group = ""
# If start instance id is not filled then use context id (similar to
# group)
if start_instance_id is None:
start_instance_id = CONTEXT_ID
# If end group is not defined then use passed group name
# - this can be happen when previous group was not selected
# - when this happens the selection will probably happen from context
# item to item selected by user
if end_group is None:
end_group = group_name
# If end instance is not filled then use instance selected by user
if end_instance_id is None:
end_instance_id = instance_id
# Start and end group are the same
# - a different logic is needed in that case
same_group = start_group == end_group
# Process known information and change selection of items
passed_start_group = False
passed_end_group = False
# Go through ordered groups (from top to bottom) and change selection
for name in self._ordered_groups:
# Prepare sorted instance widgets
if name == "":
sorted_widgets = [self._context_widget]
else:
group_widget = self._widgets_by_group[name]
sorted_widgets = group_widget.get_ordered_widgets()
# Change selection based on explicit selection if start group
# was not passed yet
if not passed_start_group:
if name != start_group:
for widget in sorted_widgets:
widget.set_selected(
widget.id in self._explicitly_selected_instance_ids
)
continue
# Change selection based on explicit selection if end group
# already passed
if passed_end_group:
for widget in sorted_widgets:
widget.set_selected(
widget.id in self._explicitly_selected_instance_ids
)
continue
# Start group is already passed and end group was not yet hit
if same_group:
passed_start_group = True
passed_end_group = True
passed_start_instance = False
passed_end_instance = False
for widget in sorted_widgets:
if not passed_start_instance:
if widget.id in (start_instance_id, end_instance_id):
if widget.id != start_instance_id:
# Swap start/end instance if start instance is
# after end
# - fix 'passed_end_instance' check
start_instance_id, end_instance_id = (
end_instance_id, start_instance_id
)
passed_start_instance = True
# Find out if widget should be selected
select = False
if passed_end_instance:
select = False
elif passed_start_instance:
select = True
# Check if instance is in explicitly selected items if
# should ont be selected
if (
not select
and widget.id in self._explicitly_selected_instance_ids
):
select = True
widget.set_selected(select)
if (
not passed_end_instance
and widget.id == end_instance_id
):
passed_end_instance = True
elif name == start_group:
# First group from which selection should start
# - look for start instance first from which the selection
# should happen
passed_start_group = True
passed_start_instance = False
for widget in sorted_widgets:
if widget.id == start_instance_id:
passed_start_instance = True
select = False
# Check if passed start instance or instance is
# in explicitly selected items to be selected
if (
passed_start_instance
or widget.id in self._explicitly_selected_instance_ids
):
select = True
widget.set_selected(select)
elif name == end_group:
# Last group where selection should happen
# - look for end instance first after which the selection
# should stop
passed_end_group = True
passed_end_instance = False
for widget in sorted_widgets:
select = False
# Check if not yet passed end instance or if instance is
# in explicitly selected items to be selected
if (
not passed_end_instance
or widget.id in self._explicitly_selected_instance_ids
):
select = True
widget.set_selected(select)
if widget.id == end_instance_id:
passed_end_instance = True
else:
# Just select everything between start and end group
for widget in sorted_widgets:
widget.set_selected(True)
def get_selected_items(self):
"""Get selected instance ids and context."""
instances = []
context_selected = False
selected_widget = self._get_selected_widget()
if selected_widget is self._context_widget:
context_selected = True
selected_widgets = self._get_selected_widgets()
elif selected_widget is not None:
instances.append(selected_widget.instance)
context_selected = False
for widget in selected_widgets:
if widget is self._context_widget:
context_selected = True
else:
instances.append(widget.id)
return instances, context_selected
def set_selected_items(self, instance_ids, context_selected):
s_instance_ids = set(instance_ids)
cur_ids, cur_context = self.get_selected_items()
if (
set(cur_ids) == s_instance_ids
and cur_context == context_selected
):
return
selected_groups = []
selected_instances = []
if context_selected:
selected_groups.append("")
selected_instances.append(CONTEXT_ID)
self._context_widget.set_selected(context_selected)
for group_name in self._ordered_groups:
if group_name == "":
continue
group_widget = self._widgets_by_group[group_name]
group_selected = False
for widget in group_widget.get_ordered_widgets():
select = False
if widget.id in s_instance_ids:
selected_instances.append(widget.id)
group_selected = True
select = True
widget.set_selected(select)
if group_selected:
selected_groups.append(group_name)
self._explicitly_selected_groups = selected_groups
self._explicitly_selected_instance_ids = selected_instances

View file

@ -1,11 +1,9 @@
import sys
import re
import traceback
import copy
from Qt import QtWidgets, QtCore, QtGui
from openpype.client import get_asset_by_name, get_subsets
from openpype.pipeline.create import (
CreatorError,
SUBSET_NAME_ALLOWED_SYMBOLS,
@ -150,18 +148,18 @@ class CreatorShortDescWidget(QtWidgets.QWidget):
self._family_label = family_label
self._description_label = description_label
def set_plugin(self, plugin=None):
if not plugin:
def set_creator_item(self, creator_item=None):
if not creator_item:
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 ""
plugin_icon = creator_item.icon
description = creator_item.description or ""
self._icon_widget.set_icon_def(plugin_icon)
self._family_label.setText("<b>{}</b>".format(plugin.family))
self._family_label.setText("<b>{}</b>".format(creator_item.family))
self._family_label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
self._description_label.setText(description)
@ -174,7 +172,7 @@ class CreateWidget(QtWidgets.QWidget):
self._controller = controller
self._asset_doc = None
self._asset_name = None
self._subset_names = None
self._selected_creator = None
@ -380,7 +378,7 @@ class CreateWidget(QtWidgets.QWidget):
if asset_name is None:
asset_name = self.current_asset_name
return asset_name
return asset_name or None
def _get_task_name(self):
task_name = None
@ -444,7 +442,7 @@ class CreateWidget(QtWidgets.QWidget):
prereq_available = False
creator_btn_tooltips.append("Creator is not selected")
if self._context_change_is_enabled() and self._asset_doc is None:
if self._context_change_is_enabled() and self._asset_name is None:
# QUESTION how to handle invalid asset?
prereq_available = False
creator_btn_tooltips.append("Context is not selected")
@ -468,30 +466,19 @@ class CreateWidget(QtWidgets.QWidget):
asset_name = self._get_asset_name()
# Skip if asset did not change
if self._asset_doc and self._asset_doc["name"] == asset_name:
if self._asset_name and self._asset_name == asset_name:
return
# Make sure `_asset_doc` and `_subset_names` variables are reset
self._asset_doc = None
# Make sure `_asset_name` and `_subset_names` variables are reset
self._asset_name = asset_name
self._subset_names = None
if asset_name is None:
return
project_name = self._controller.project_name
asset_doc = get_asset_by_name(project_name, asset_name)
self._asset_doc = asset_doc
subset_names = self._controller.get_existing_subset_names(asset_name)
if asset_doc:
asset_id = asset_doc["_id"]
subset_docs = get_subsets(
project_name, asset_ids=[asset_id], fields=["name"]
)
self._subset_names = {
subset_doc["name"]
for subset_doc in subset_docs
}
if not asset_doc:
self._subset_names = subset_names
if subset_names is None:
self.subset_name_input.setText("< Asset is not set >")
def _refresh_creators(self):
@ -506,7 +493,10 @@ class CreateWidget(QtWidgets.QWidget):
# Add new families
new_creators = set()
for identifier, creator in self._controller.manual_creators.items():
for identifier, creator_item in self._controller.creator_items.items():
if creator_item.creator_type != "artist":
continue
# TODO add details about creator
new_creators.add(identifier)
if identifier in existing_items:
@ -518,10 +508,9 @@ class CreateWidget(QtWidgets.QWidget):
)
self._creators_model.appendRow(item)
label = creator.label or identifier
item.setData(label, QtCore.Qt.DisplayRole)
item.setData(creator_item.label, QtCore.Qt.DisplayRole)
item.setData(identifier, CREATOR_IDENTIFIER_ROLE)
item.setData(creator.family, FAMILY_ROLE)
item.setData(creator_item.family, FAMILY_ROLE)
# Remove families that are no more available
for identifier in (old_creators - new_creators):
@ -572,11 +561,11 @@ class CreateWidget(QtWidgets.QWidget):
identifier = new_index.data(CREATOR_IDENTIFIER_ROLE)
self._set_creator_by_identifier(identifier)
def _set_creator_detailed_text(self, creator):
def _set_creator_detailed_text(self, creator_item):
# TODO implement
description = ""
if creator is not None:
description = creator.get_detail_description() or description
if creator_item is not None:
description = creator_item.detailed_description or description
self._controller.event_system.emit(
"show.detailed.help",
{
@ -586,32 +575,39 @@ class CreateWidget(QtWidgets.QWidget):
)
def _set_creator_by_identifier(self, identifier):
creator = self._controller.manual_creators.get(identifier)
self._set_creator(creator)
creator_item = self._controller.creator_items.get(identifier)
self._set_creator(creator_item)
def _set_creator(self, creator):
self._creator_short_desc_widget.set_plugin(creator)
self._set_creator_detailed_text(creator)
self._pre_create_widget.set_plugin(creator)
def _set_creator(self, creator_item):
"""Set current creator item.
self._selected_creator = creator
Args:
creator_item (CreatorItem): Item representing creator that can be
triggered by artist.
"""
if not creator:
self._creator_short_desc_widget.set_creator_item(creator_item)
self._set_creator_detailed_text(creator_item)
self._pre_create_widget.set_creator_item(creator_item)
self._selected_creator = creator_item
if not creator_item:
self._set_context_enabled(False)
return
if (
creator.create_allow_context_change
creator_item.create_allow_context_change
!= self._context_change_is_enabled()
):
self._set_context_enabled(creator.create_allow_context_change)
self._set_context_enabled(creator_item.create_allow_context_change)
self._refresh_asset()
default_variants = creator.get_default_variants()
default_variants = creator_item.default_variants
if not default_variants:
default_variants = ["Main"]
default_variant = creator.get_default_variant()
default_variant = creator_item.default_variant
if not default_variant:
default_variant = default_variants[0]
@ -670,14 +666,13 @@ class CreateWidget(QtWidgets.QWidget):
self.subset_name_input.setText("< Valid variant >")
return
project_name = self._controller.project_name
asset_name = self._get_asset_name()
task_name = self._get_task_name()
asset_doc = copy.deepcopy(self._asset_doc)
creator_idenfier = self._selected_creator.identifier
# Calculate subset name with Creator plugin
try:
subset_name = self._selected_creator.get_subset_name(
variant_value, task_name, asset_doc, project_name
subset_name = self._controller.get_subset_name(
creator_idenfier, variant_value, task_name, asset_name
)
except TaskNotSetError:
self._create_btn.setEnabled(False)

View file

@ -44,8 +44,10 @@ class HelpWidget(QtWidgets.QWidget):
if commonmark:
html = commonmark.commonmark(text)
self._detail_description_input.setHtml(html)
else:
elif hasattr(self._detail_description_input, "setMarkdown"):
self._detail_description_input.setMarkdown(text)
else:
self._detail_description_input.setText(text)
class HelpDialog(QtWidgets.QDialog):

View file

@ -409,7 +409,7 @@ class InstanceListView(AbstractInstanceView):
def __init__(self, controller, parent):
super(InstanceListView, self).__init__(parent)
self.controller = controller
self._controller = controller
instance_view = InstanceTreeView(self)
instance_delegate = ListItemDelegate(instance_view)
@ -520,7 +520,7 @@ class InstanceListView(AbstractInstanceView):
# Prepare instances by their groups
instances_by_group_name = collections.defaultdict(list)
group_names = set()
for instance in self.controller.instances:
for instance in self._controller.instances.values():
group_label = instance.group_label
group_names.add(group_label)
instances_by_group_name[group_label].append(instance)
@ -723,13 +723,13 @@ class InstanceListView(AbstractInstanceView):
widget.update_instance_values()
def _on_active_changed(self, changed_instance_id, new_value):
selected_instances, _ = self.get_selected_items()
selected_instance_ids, _ = self.get_selected_items()
selected_ids = set()
found = False
for instance in selected_instances:
selected_ids.add(instance.id)
if not found and instance.id == changed_instance_id:
for instance_id in selected_instance_ids:
selected_ids.add(instance_id)
if not found and instance_id == changed_instance_id:
found = True
if not found:
@ -760,32 +760,6 @@ class InstanceListView(AbstractInstanceView):
if changed_ids:
self.active_changed.emit()
def get_selected_items(self):
"""Get selected instance ids and context selection.
Returns:
tuple<list, bool>: Selected instance ids and boolean if context
is selected.
"""
instances = []
context_selected = False
instances_by_id = {
instance.id: instance
for instance in self.controller.instances
}
for index in self._instance_view.selectionModel().selectedIndexes():
instance_id = index.data(INSTANCE_ID_ROLE)
if not context_selected and instance_id == CONTEXT_ID:
context_selected = True
elif instance_id is not None:
instance = instances_by_id.get(instance_id)
if instance:
instances.append(instance)
return instances, context_selected
def _on_selection_change(self, *_args):
self.selection_changed.emit()
@ -825,3 +799,102 @@ class InstanceListView(AbstractInstanceView):
proxy_index = self._proxy_model.mapFromSource(group_item.index())
if not self._instance_view.isExpanded(proxy_index):
self._instance_view.expand(proxy_index)
def get_selected_items(self):
"""Get selected instance ids and context selection.
Returns:
tuple<list, bool>: Selected instance ids and boolean if context
is selected.
"""
instance_ids = []
context_selected = False
for index in self._instance_view.selectionModel().selectedIndexes():
instance_id = index.data(INSTANCE_ID_ROLE)
if not context_selected and instance_id == CONTEXT_ID:
context_selected = True
elif instance_id is not None:
instance_ids.append(instance_id)
return instance_ids, context_selected
def set_selected_items(self, instance_ids, context_selected):
s_instance_ids = set(instance_ids)
cur_ids, cur_context = self.get_selected_items()
if (
set(cur_ids) == s_instance_ids
and cur_context == context_selected
):
return
view = self._instance_view
src_model = self._instance_model
proxy_model = self._proxy_model
select_indexes = []
select_queue = collections.deque()
select_queue.append(
(src_model.invisibleRootItem(), [])
)
while select_queue:
queue_item = select_queue.popleft()
item, parent_items = queue_item
if item.hasChildren():
new_parent_items = list(parent_items)
new_parent_items.append(item)
for row in range(item.rowCount()):
select_queue.append(
(item.child(row), list(new_parent_items))
)
instance_id = item.data(INSTANCE_ID_ROLE)
if not instance_id:
continue
if instance_id in s_instance_ids:
select_indexes.append(item.index())
for parent_item in parent_items:
index = parent_item.index()
proxy_index = proxy_model.mapFromSource(index)
if not view.isExpanded(proxy_index):
view.expand(proxy_index)
elif context_selected and instance_id == CONTEXT_ID:
select_indexes.append(item.index())
selection_model = view.selectionModel()
if not select_indexes:
selection_model.clear()
return
if len(select_indexes) == 1:
proxy_index = proxy_model.mapFromSource(select_indexes[0])
selection_model.setCurrentIndex(
proxy_index,
selection_model.ClearAndSelect | selection_model.Rows
)
return
first_index = proxy_model.mapFromSource(select_indexes.pop(0))
last_index = proxy_model.mapFromSource(select_indexes.pop(-1))
selection_model.setCurrentIndex(
first_index,
selection_model.ClearAndSelect | selection_model.Rows
)
for index in select_indexes:
proxy_index = proxy_model.mapFromSource(index)
selection_model.select(
proxy_index,
selection_model.Select | selection_model.Rows
)
selection_model.setCurrentIndex(
last_index,
selection_model.Select | selection_model.Rows
)

View file

@ -201,16 +201,16 @@ class OverviewWidget(QtWidgets.QFrame):
self.create_requested.emit()
def _on_delete_clicked(self):
instances, _ = self.get_selected_items()
instance_ids, _ = self.get_selected_items()
# Ask user if he really wants to remove instances
dialog = QtWidgets.QMessageBox(self)
dialog.setIcon(QtWidgets.QMessageBox.Question)
dialog.setWindowTitle("Are you sure?")
if len(instances) > 1:
if len(instance_ids) > 1:
msg = (
"Do you really want to remove {} instances?"
).format(len(instances))
).format(len(instance_ids))
else:
msg = (
"Do you really want to remove the instance?"
@ -224,7 +224,8 @@ class OverviewWidget(QtWidgets.QFrame):
dialog.exec_()
# Skip if OK was not clicked
if dialog.result() == QtWidgets.QMessageBox.Ok:
self._controller.remove_instances(instances)
instance_ids = set(instance_ids)
self._controller.remove_instances(instance_ids)
def _on_change_view_clicked(self):
self._change_view_type()
@ -234,11 +235,16 @@ class OverviewWidget(QtWidgets.QFrame):
if self._refreshing_instances:
return
instances, context_selected = self.get_selected_items()
instance_ids, context_selected = self.get_selected_items()
# Disable delete button if nothing is selected
self._delete_btn.setEnabled(len(instances) > 0)
self._delete_btn.setEnabled(len(instance_ids) > 0)
instances_by_id = self._controller.instances
instances = [
instances_by_id[instance_id]
for instance_id in instance_ids
]
self._subset_attributes_widget.set_current_instances(
instances, context_selected
)
@ -315,15 +321,21 @@ class OverviewWidget(QtWidgets.QFrame):
def _change_view_type(self):
idx = self._subset_views_layout.currentIndex()
new_idx = (idx + 1) % self._subset_views_layout.count()
self._subset_views_layout.setCurrentIndex(new_idx)
new_view = self._subset_views_layout.currentWidget()
old_view = self._subset_views_layout.currentWidget()
new_view = self._subset_views_layout.widget(new_idx)
if not new_view.refreshed:
new_view.refresh()
new_view.set_refreshed(True)
else:
new_view.refresh_instance_states()
instance_ids, context_selected = old_view.get_selected_items()
new_view.set_selected_items(instance_ids, context_selected)
self._subset_views_layout.setCurrentIndex(new_idx)
self._on_subset_change()
def _refresh_instances(self):

View file

@ -58,12 +58,12 @@ class PreCreateWidget(QtWidgets.QWidget):
def current_value(self):
return self._attributes_widget.current_value()
def set_plugin(self, creator):
def set_creator_item(self, creator_item):
attr_defs = []
creator_selected = False
if creator is not None:
if creator_item is not None:
creator_selected = True
attr_defs = creator.get_pre_create_attr_defs()
attr_defs = creator_item.pre_create_attributes_defs
self._attributes_widget.set_attr_defs(attr_defs)

View file

@ -4,8 +4,6 @@ import time
from Qt import QtWidgets, QtCore
from openpype.pipeline import KnownPublishError
from .widgets import (
StopBtn,
ResetBtn,
@ -170,7 +168,7 @@ class PublishFrame(QtWidgets.QWidget):
"publish.process.started", self._on_publish_start
)
controller.event_system.add_callback(
"publish.process.validated", self._on_publish_validated
"publish.has_validated.changed", self._on_publish_validated_change
)
controller.event_system.add_callback(
"publish.process.stopped", self._on_publish_stop
@ -185,7 +183,7 @@ class PublishFrame(QtWidgets.QWidget):
self._shrunk_anim = shrunk_anim
self.controller = controller
self._controller = controller
self._content_frame = content_frame
self._content_layout = content_layout
@ -320,8 +318,8 @@ class PublishFrame(QtWidgets.QWidget):
self._validate_btn.setEnabled(True)
self._publish_btn.setEnabled(True)
self._progress_bar.setValue(self.controller.publish_progress)
self._progress_bar.setMaximum(self.controller.publish_max_progress)
self._progress_bar.setValue(self._controller.publish_progress)
self._progress_bar.setMaximum(self._controller.publish_max_progress)
def _on_publish_start(self):
if self._last_plugin_label:
@ -330,7 +328,7 @@ class PublishFrame(QtWidgets.QWidget):
if self._last_instance_label:
self._instance_label.setText(self._last_instance_label)
self._set_success_property(-1)
self._set_success_property(3)
self._set_progress_visibility(True)
self._set_main_label("Publishing...")
@ -341,8 +339,9 @@ class PublishFrame(QtWidgets.QWidget):
self.set_shrunk_state(False)
def _on_publish_validated(self):
self._validate_btn.setEnabled(False)
def _on_publish_validated_change(self, event):
if event["value"]:
self._validate_btn.setEnabled(False)
def _on_instance_change(self, event):
"""Change instance label when instance is going to be processed."""
@ -355,12 +354,12 @@ class PublishFrame(QtWidgets.QWidget):
"""Change plugin label when instance is going to be processed."""
self._last_plugin_label = event["plugin_label"]
self._progress_bar.setValue(self.controller.publish_progress)
self._progress_bar.setValue(self._controller.publish_progress)
self._plugin_label.setText(event["plugin_label"])
QtWidgets.QApplication.processEvents()
def _on_publish_stop(self):
self._progress_bar.setValue(self.controller.publish_progress)
self._progress_bar.setValue(self._controller.publish_progress)
self._reset_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
@ -368,33 +367,31 @@ class PublishFrame(QtWidgets.QWidget):
self._instance_label.setText("")
self._plugin_label.setText("")
validate_enabled = not self.controller.publish_has_crashed
publish_enabled = not self.controller.publish_has_crashed
validate_enabled = not self._controller.publish_has_crashed
publish_enabled = not self._controller.publish_has_crashed
if validate_enabled:
validate_enabled = not self.controller.publish_has_validated
validate_enabled = not self._controller.publish_has_validated
if publish_enabled:
if (
self.controller.publish_has_validated
and self.controller.publish_has_validation_errors
self._controller.publish_has_validated
and self._controller.publish_has_validation_errors
):
publish_enabled = False
else:
publish_enabled = not self.controller.publish_has_finished
publish_enabled = not self._controller.publish_has_finished
self._validate_btn.setEnabled(validate_enabled)
self._publish_btn.setEnabled(publish_enabled)
error = self.controller.get_publish_crash_error()
validation_errors = self.controller.get_validation_errors()
if error:
self._set_error(error)
if self._controller.publish_has_crashed:
self._set_error_msg()
elif validation_errors:
elif self._controller.publish_has_validation_errors:
self._set_progress_visibility(False)
self._set_validation_errors()
elif self.controller.publish_has_finished:
elif self._controller.publish_has_finished:
self._set_finished()
else:
@ -402,7 +399,7 @@ class PublishFrame(QtWidgets.QWidget):
def _set_stopped(self):
main_label = "Publish paused"
if self.controller.publish_has_validated:
if self._controller.publish_has_validated:
main_label += " - Validation passed"
self._set_main_label(main_label)
@ -410,20 +407,16 @@ class PublishFrame(QtWidgets.QWidget):
"Hit publish (play button) to continue."
)
self._set_success_property(-1)
self._set_success_property(4)
def _set_error_msg(self):
"""Show error message to artist on publish crash."""
def _set_error(self, error):
self._set_main_label("Error happened")
if isinstance(error, KnownPublishError):
msg = str(error)
else:
msg = (
"Something went wrong. Send report"
" to your supervisor or OpenPype."
)
self._message_label_top.setText(msg)
self._set_success_property(0)
self._message_label_top.setText(self._controller.publish_error_msg)
self._set_success_property(1)
def _set_validation_errors(self):
self._set_main_label("Your publish didn't pass studio validations")
@ -433,7 +426,7 @@ class PublishFrame(QtWidgets.QWidget):
def _set_finished(self):
self._set_main_label("Finished")
self._message_label_top.setText("")
self._set_success_property(1)
self._set_success_property(0)
def _set_progress_visibility(self, visible):
window_height = self.height()
@ -454,6 +447,17 @@ class PublishFrame(QtWidgets.QWidget):
self.move(window_pos.x(), window_pos_y)
def _set_success_property(self, state=None):
"""Apply styles by state.
State enum:
- None - Default state after restart
- 0 - Success finish
- 1 - Error happened
- 2 - Validation error
- 3 - In progress
- 4 - Stopped/Paused
"""
if state is None:
state = ""
else:
@ -465,7 +469,7 @@ class PublishFrame(QtWidgets.QWidget):
widget.style().polish(widget)
def _copy_report(self):
logs = self.controller.get_publish_report()
logs = self._controller.get_publish_report()
logs_string = json.dumps(logs, indent=4)
mime_data = QtCore.QMimeData()
@ -488,7 +492,7 @@ class PublishFrame(QtWidgets.QWidget):
if not ext or not new_filepath:
return
logs = self.controller.get_publish_report()
logs = self._controller.get_publish_report()
full_path = new_filepath + ext
dir_path = os.path.dirname(full_path)
if not os.path.exists(dir_path):
@ -508,13 +512,13 @@ class PublishFrame(QtWidgets.QWidget):
self.details_page_requested.emit()
def _on_reset_clicked(self):
self.controller.reset()
self._controller.reset()
def _on_stop_clicked(self):
self.controller.stop_publish()
self._controller.stop_publish()
def _on_validate_clicked(self):
self.controller.validate()
self._controller.validate()
def _on_publish_clicked(self):
self.controller.publish()
self._controller.publish()

View file

@ -50,6 +50,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
Has toggle button to show/hide instances on which validation error happened
if there is a list (Valdation error may happen on context).
"""
selected = QtCore.Signal(int)
instance_changed = QtCore.Signal(int)
@ -75,34 +76,31 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
title_frame_layout.addWidget(toggle_instance_btn, 0)
instances_model = QtGui.QStandardItemModel()
error_info = error_info["error_info"]
help_text_by_instance_id = {}
context_validation = False
items = []
if (
not error_info
or (len(error_info) == 1 and error_info[0][0] is None)
):
context_validation = True
toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow)
description = self._prepare_description(error_info[0][1])
help_text_by_instance_id[None] = description
# Add fake item to have minimum size hint of view widget
items.append(QtGui.QStandardItem("Context"))
else:
for instance, exception in error_info:
label = instance.data.get("label") or instance.data.get("name")
item = QtGui.QStandardItem(label)
item.setFlags(
QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
)
item.setData(label, QtCore.Qt.ToolTipRole)
item.setData(instance.id, INSTANCE_ID_ROLE)
items.append(item)
description = self._prepare_description(exception)
help_text_by_instance_id[instance.id] = description
items = []
context_validation = False
for error_item in error_info["error_items"]:
context_validation = error_item.context_validation
if context_validation:
toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow)
description = self._prepare_description(error_item)
help_text_by_instance_id[None] = description
# Add fake item to have minimum size hint of view widget
items.append(QtGui.QStandardItem("Context"))
continue
label = error_item.instance_label
item = QtGui.QStandardItem(label)
item.setFlags(
QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
)
item.setData(label, QtCore.Qt.ToolTipRole)
item.setData(error_item.instance_id, INSTANCE_ID_ROLE)
items.append(item)
description = self._prepare_description(error_item)
help_text_by_instance_id[error_item.instance_id] = description
if items:
root_item = instances_model.invisibleRootItem()
@ -167,9 +165,19 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
def minimumSizeHint(self):
return self.sizeHint()
def _prepare_description(self, exception):
dsc = exception.description
detail = exception.detail
def _prepare_description(self, error_item):
"""Prepare description text for detail intput.
Args:
error_item (ValidationErrorItem): Item which hold information about
validation error.
Returns:
str: Prepared detailed description.
"""
dsc = error_item.description
detail = error_item.detail
if detail:
dsc += "<br/><br/>{}".format(detail)
@ -196,32 +204,51 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
@property
def is_selected(self):
"""Is widget marked a selected"""
"""Is widget marked a selected.
Returns:
bool: Item is selected or not.
"""
return self._selected
@property
def index(self):
"""Widget's index set by parent."""
"""Widget's index set by parent.
Returns:
int: Index of widget.
"""
return self._index
def set_index(self, index):
"""Set index of widget (called by parent)."""
"""Set index of widget (called by parent).
Args:
int: New index of widget.
"""
self._index = index
def _change_style_property(self, selected):
"""Change style of widget based on selection."""
value = "1" if selected else ""
self._title_frame.setProperty("selected", value)
self._title_frame.style().polish(self._title_frame)
def set_selected(self, selected=None):
"""Change selected state of widget."""
if selected is None:
selected = not self._selected
# Clear instance view selection on deselect
if not selected:
self._instances_view.clearSelection()
# Skip if has same value
if selected == self._selected:
return
@ -263,18 +290,23 @@ class ActionButton(BaseClickableFrame):
"""Plugin's action callback button.
Action may have label or icon or both.
"""
action_clicked = QtCore.Signal(str)
def __init__(self, action, parent):
Args:
plugin_action_item (PublishPluginActionItem): Action item that can be
triggered by it's id.
"""
action_clicked = QtCore.Signal(str, str)
def __init__(self, plugin_action_item, parent):
super(ActionButton, self).__init__(parent)
self.setObjectName("ValidationActionButton")
self.action = action
self.plugin_action_item = plugin_action_item
action_label = action.label or action.__name__
action_icon = getattr(action, "icon", None)
action_label = plugin_action_item.label
action_icon = plugin_action_item.icon
label_widget = QtWidgets.QLabel(action_label, self)
icon_label = None
if action_icon:
@ -292,7 +324,10 @@ class ActionButton(BaseClickableFrame):
)
def _mouse_release_callback(self):
self.action_clicked.emit(self.action.id)
self.action_clicked.emit(
self.plugin_action_item.plugin_id,
self.plugin_action_item.action_id
)
class ValidateActionsWidget(QtWidgets.QFrame):
@ -300,6 +335,7 @@ class ValidateActionsWidget(QtWidgets.QFrame):
Change actions based on selected validation error.
"""
def __init__(self, controller, parent):
super(ValidateActionsWidget, self).__init__(parent)
@ -312,10 +348,9 @@ class ValidateActionsWidget(QtWidgets.QFrame):
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(content_widget)
self.controller = controller
self._controller = controller
self._content_widget = content_widget
self._content_layout = content_layout
self._plugin = None
self._actions_mapping = {}
def clear(self):
@ -328,28 +363,34 @@ class ValidateActionsWidget(QtWidgets.QFrame):
widget.deleteLater()
self._actions_mapping = {}
def set_plugin(self, plugin):
def set_error_item(self, error_item):
"""Set selected plugin and show it's actions.
Clears current actions from widget and recreate them from the plugin.
Args:
Dict[str, Any]: Object holding error items, title and possible
actions to run.
"""
self.clear()
self._plugin = plugin
if not plugin:
if not error_item:
self.setVisible(False)
return
actions = getattr(plugin, "actions", [])
for action in actions:
if not action.active:
plugin_action_items = error_item["plugin_action_items"]
for plugin_action_item in plugin_action_items:
if not plugin_action_item.active:
continue
if action.on not in ("failed", "all"):
if plugin_action_item.on_filter not in ("failed", "all"):
continue
self._actions_mapping[action.id] = action
action_id = plugin_action_item.action_id
self._actions_mapping[action_id] = plugin_action_item
action_btn = ActionButton(action, self._content_widget)
action_btn = ActionButton(plugin_action_item, self._content_widget)
action_btn.action_clicked.connect(self._on_action_click)
self._content_layout.addWidget(action_btn)
@ -359,9 +400,8 @@ class ValidateActionsWidget(QtWidgets.QFrame):
else:
self.setVisible(False)
def _on_action_click(self, action_id):
action = self._actions_mapping[action_id]
self.controller.run_action(self._plugin, action)
def _on_action_click(self, plugin_id, action_id):
self._controller.run_action(plugin_id, action_id)
class VerticallScrollArea(QtWidgets.QScrollArea):
@ -373,6 +413,7 @@ class VerticallScrollArea(QtWidgets.QScrollArea):
Resize if deferred by 100ms because at the moment of resize are not yet
propagated sizes and visibility of scroll bars.
"""
def __init__(self, *args, **kwargs):
super(VerticallScrollArea, self).__init__(*args, **kwargs)
@ -584,45 +625,31 @@ class ValidationsWidget(QtWidgets.QFrame):
self._errors_widget.setVisible(False)
self._actions_widget.setVisible(False)
def set_errors(self, errors):
"""Set errors into context and created titles."""
def _set_errors(self, validation_error_report):
"""Set errors into context and created titles.
Args:
validation_error_report (PublishValidationErrorsReport): Report
with information about validation errors and publish plugin
actions.
"""
self.clear()
if not errors:
if not validation_error_report:
return
self._top_label.setVisible(True)
self._error_details_frame.setVisible(True)
self._errors_widget.setVisible(True)
errors_by_title = []
for plugin_info in errors:
titles = []
error_info_by_title = {}
for error_info in plugin_info["errors"]:
exception = error_info["exception"]
title = exception.title
if title not in titles:
titles.append(title)
error_info_by_title[title] = []
error_info_by_title[title].append(
(error_info["instance"], exception)
)
for title in titles:
errors_by_title.append({
"plugin": plugin_info["plugin"],
"error_info": error_info_by_title[title],
"title": title
})
for idx, item in enumerate(errors_by_title):
widget = ValidationErrorTitleWidget(idx, item, self)
grouped_error_items = validation_error_report.group_items_by_title()
for idx, error_info in enumerate(grouped_error_items):
widget = ValidationErrorTitleWidget(idx, error_info, self)
widget.selected.connect(self._on_select)
widget.instance_changed.connect(self._on_instance_change)
self._errors_layout.addWidget(widget)
self._title_widgets[idx] = widget
self._error_info[idx] = item
self._error_info[idx] = error_info
self._errors_layout.addStretch(1)
@ -648,7 +675,7 @@ class ValidationsWidget(QtWidgets.QFrame):
if self._controller.publish_has_validation_errors:
validation_errors = self._controller.get_validation_errors()
self._set_current_widget(self._validations_widget)
self.set_errors(validation_errors)
self._set_errors(validation_errors)
return
if self._controller.publish_has_finished:
@ -667,7 +694,7 @@ class ValidationsWidget(QtWidgets.QFrame):
error_item = self._error_info[index]
self._actions_widget.set_plugin(error_item["plugin"])
self._actions_widget.set_error_item(error_item)
self._update_description()
@ -682,5 +709,7 @@ class ValidationsWidget(QtWidgets.QFrame):
if commonmark:
html = commonmark.commonmark(description)
self._error_details_input.setHtml(html)
else:
elif hasattr(self._error_details_input, "setMarkdown"):
self._error_details_input.setMarkdown(description)
else:
self._error_details_input.setText(description)

View file

@ -306,10 +306,25 @@ class AbstractInstanceView(QtWidgets.QWidget):
Example: When delete button is clicked to know what should be deleted.
"""
raise NotImplementedError((
"{} Method 'get_selected_items' is not implemented."
).format(self.__class__.__name__))
def set_selected_items(self, instance_ids, context_selected):
"""Change selection for instances and context.
Used to applying selection from one view to other.
Args:
instance_ids (List[str]): Selected instance ids.
context_selected (bool): Context is selected.
"""
raise NotImplementedError((
"{} Method 'set_selected_items' is not implemented."
).format(self.__class__.__name__))
class ClickableLineEdit(QtWidgets.QLineEdit):
"""QLineEdit capturing left mouse click.
@ -994,7 +1009,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
def __init__(self, controller, parent):
super(GlobalAttrsWidget, self).__init__(parent)
self.controller = controller
self._controller = controller
self._current_instances = []
variant_input = VariantInputWidget(self)
@ -1060,24 +1075,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
if self.task_value_widget.has_value_changed():
task_name = self.task_value_widget.get_selected_items()[0]
asset_docs_by_name = {}
asset_names = set()
if asset_name is None:
for instance in self._current_instances:
asset_names.add(instance.get("asset"))
else:
asset_names.add(asset_name)
for asset_doc in self.controller.get_asset_docs():
_asset_name = asset_doc["name"]
if _asset_name in asset_names:
asset_names.remove(_asset_name)
asset_docs_by_name[_asset_name] = asset_doc
if not asset_names:
break
project_name = self.controller.project_name
subset_names = set()
invalid_tasks = False
for instance in self._current_instances:
@ -1093,16 +1090,15 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
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_subset_name = self._controller.get_subset_name(
instance.creator_identifier,
new_variant_value,
new_task_name,
asset_doc,
project_name,
instance=instance
new_asset_name,
instance.id,
)
except TaskNotSetError:
invalid_tasks = True
instance.set_task_invalid(True)
@ -1249,7 +1245,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
self._main_layout = main_layout
self.controller = controller
self._controller = controller
self._scroll_area = scroll_area
self._attr_def_id_to_instances = {}
@ -1278,7 +1274,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
self._attr_def_id_to_instances = {}
self._attr_def_id_to_attr_def = {}
result = self.controller.get_creator_attribute_definitions(
result = self._controller.get_creator_attribute_definitions(
instances
)
@ -1370,7 +1366,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
self._main_layout = main_layout
self.controller = controller
self._controller = controller
self._scroll_area = scroll_area
self._attr_def_id_to_instances = {}
@ -1402,7 +1398,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
self._attr_def_id_to_attr_def = {}
self._attr_def_id_to_plugin_name = {}
result = self.controller.get_publish_attribute_definitions(
result = self._controller.get_publish_attribute_definitions(
instances, context_selected
)
@ -1517,7 +1513,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
self._on_instance_context_changed
)
self.controller = controller
self._controller = controller
self.global_attrs_widget = global_attrs_widget

View file

@ -11,7 +11,7 @@ from openpype.tools.utils import (
)
from .publish_report_viewer import PublishReportViewerWidget
from .control import PublisherController
from .control_qt import QtPublisherController
from .widgets import (
OverviewWidget,
ValidationsWidget,
@ -36,7 +36,7 @@ class PublisherWindow(QtWidgets.QDialog):
footer_border = 8
publish_footer_spacer = 2
def __init__(self, parent=None, reset_on_show=None):
def __init__(self, parent=None, controller=None, reset_on_show=None):
super(PublisherWindow, self).__init__(parent)
self.setWindowTitle("OpenPype publisher")
@ -61,7 +61,8 @@ class PublisherWindow(QtWidgets.QDialog):
| on_top_flag
)
controller = PublisherController()
if controller is None:
controller = QtPublisherController()
help_dialog = HelpDialog(controller, self)
@ -250,7 +251,7 @@ class PublisherWindow(QtWidgets.QDialog):
"publish.process.started", self._on_publish_start
)
controller.event_system.add_callback(
"publish.process.validated", self._on_publish_validated
"publish.has_validated.changed", self._on_publish_validated_change
)
controller.event_system.add_callback(
"publish.process.stopped", self._on_publish_stop
@ -441,11 +442,7 @@ class PublisherWindow(QtWidgets.QDialog):
self._controller.stop_publish()
def _set_publish_comment(self):
if self._controller.publish_comment_is_set:
return
comment = self._comment_input.text()
self._controller.set_comment(comment)
self._controller.set_comment(self._comment_input.text())
def _on_validate_clicked(self):
self._set_publish_comment()
@ -473,6 +470,11 @@ class PublisherWindow(QtWidgets.QDialog):
self._set_publish_visibility(False)
self._set_footer_enabled(False)
self._update_publish_details_widget()
if (
not self._tabs_widget.is_current_tab("create")
or not self._tabs_widget.is_current_tab("publish")
):
self._tabs_widget.set_current_tab("publish")
def _on_publish_start(self):
self._create_tab.setEnabled(False)
@ -491,15 +493,20 @@ class PublisherWindow(QtWidgets.QDialog):
if self._tabs_widget.is_current_tab(self._create_tab):
self._tabs_widget.set_current_tab("publish")
def _on_publish_validated(self):
self._validate_btn.setEnabled(False)
def _on_publish_validated_change(self, event):
if event["value"]:
self._validate_btn.setEnabled(False)
def _on_publish_stop(self):
self._set_publish_overlay_visibility(False)
self._reset_btn.setEnabled(True)
self._stop_btn.setEnabled(False)
validate_enabled = not self._controller.publish_has_crashed
publish_enabled = not self._controller.publish_has_crashed
publish_has_crashed = self._controller.publish_has_crashed
validate_enabled = not publish_has_crashed
publish_enabled = not publish_has_crashed
if self._tabs_widget.is_current_tab("publish"):
self._go_to_report_tab()
if validate_enabled:
validate_enabled = not self._controller.publish_has_validated
if publish_enabled:
@ -508,8 +515,6 @@ class PublisherWindow(QtWidgets.QDialog):
and self._controller.publish_has_validation_errors
):
publish_enabled = False
if self._tabs_widget.is_current_tab("publish"):
self._go_to_report_tab()
else:
publish_enabled = not self._controller.publish_has_finished
@ -525,7 +530,7 @@ class PublisherWindow(QtWidgets.QDialog):
return
all_valid = None
for instance in self._controller.instances:
for instance in self._controller.instances.values():
if not instance["active"]:
continue

View file

@ -15,6 +15,7 @@ import appdirs
from openpype.lib import JSONSettingRegistry
from openpype.pipeline import install_host
from openpype.hosts.traypublisher.api import TrayPublisherHost
from openpype.tools.publisher.control_qt import QtPublisherController
from openpype.tools.publisher.window import PublisherWindow
from openpype.tools.utils import PlaceholderLineEdit
from openpype.tools.utils.constants import PROJECT_NAME_ROLE
@ -24,6 +25,15 @@ from openpype.tools.utils.models import (
)
class TrayPublisherController(QtPublisherController):
@property
def host(self):
return self._host
def reset_project_data_cache(self):
self._asset_docs_cache.reset()
class TrayPublisherRegistry(JSONSettingRegistry):
"""Class handling OpenPype general settings registry.
@ -179,7 +189,10 @@ class StandaloneOverlayWidget(QtWidgets.QFrame):
class TrayPublishWindow(PublisherWindow):
def __init__(self, *args, **kwargs):
super(TrayPublishWindow, self).__init__(reset_on_show=False)
controller = TrayPublisherController()
super(TrayPublishWindow, self).__init__(
controller=controller, reset_on_show=False
)
flags = self.windowFlags()
# Disable always on top hint

View file

@ -269,25 +269,25 @@ class HostToolsHelper:
dialog.activateWindow()
dialog.showNormal()
def get_publisher_tool(self, parent):
def get_publisher_tool(self, parent=None, controller=None):
"""Create, cache and return publisher window."""
if self._publisher_tool is None:
from openpype.tools.publisher import PublisherWindow
from openpype.tools.publisher.window import PublisherWindow
host = registered_host()
ILoadHost.validate_load_methods(host)
publisher_window = PublisherWindow(
parent=parent or self._parent
controller=controller, parent=parent or self._parent
)
self._publisher_tool = publisher_window
return self._publisher_tool
def show_publisher_tool(self, parent=None):
def show_publisher_tool(self, parent=None, controller=None):
with qt_app_context():
dialog = self.get_publisher_tool(parent)
dialog = self.get_publisher_tool(parent, controller)
dialog.show()
dialog.raise_()