mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into feature/OP-4178_Add-Shared-data-for-collection-phase
This commit is contained in:
commit
149aebaac9
33 changed files with 3469 additions and 682 deletions
6
.github/workflows/prerelease.yml
vendored
6
.github/workflows/prerelease.yml
vendored
|
|
@ -40,13 +40,13 @@ jobs:
|
|||
- name: "✏️ Generate full changelog"
|
||||
if: steps.version_type.outputs.type != 'skip'
|
||||
id: generate-full-changelog
|
||||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
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.0.0"
|
||||
sinceTag: "3.12.0"
|
||||
maxIssues: 100
|
||||
pullRequests: true
|
||||
prWoLabels: false
|
||||
|
|
@ -92,4 +92,4 @@ jobs:
|
|||
github_token: ${{ secrets.ADMIN_TOKEN }}
|
||||
source_ref: 'main'
|
||||
target_branch: 'develop'
|
||||
commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'
|
||||
commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'
|
||||
|
|
|
|||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -36,13 +36,13 @@ jobs:
|
|||
- name: "✏️ Generate full changelog"
|
||||
if: steps.version.outputs.release_tag != 'skip'
|
||||
id: generate-full-changelog
|
||||
uses: heinrichreimer/github-changelog-generator-action@v2.2
|
||||
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.0.0"
|
||||
sinceTag: "3.12.0"
|
||||
maxIssues: 100
|
||||
pullRequests: true
|
||||
prWoLabels: false
|
||||
|
|
@ -121,4 +121,4 @@ jobs:
|
|||
github_token: ${{ secrets.ADMIN_TOKEN }}
|
||||
source_ref: 'main'
|
||||
target_branch: 'develop'
|
||||
commit_message_template: '[Automated] Merged release {source_ref} into {target_branch}'
|
||||
commit_message_template: '[Automated] Merged release {source_ref} into {target_branch}'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Host API required Work Files tool"""
|
||||
|
||||
import os
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import Logger
|
||||
# from .. import (
|
||||
# get_project_manager,
|
||||
# get_current_project
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@ import json
|
|||
import tempfile
|
||||
import contextlib
|
||||
import socket
|
||||
from pprint import pformat
|
||||
|
||||
from openpype.lib import (
|
||||
PreLaunchHook,
|
||||
get_openpype_username
|
||||
get_openpype_username,
|
||||
run_subprocess,
|
||||
)
|
||||
from openpype.lib.applications import (
|
||||
ApplicationLaunchFailed
|
||||
)
|
||||
from openpype.hosts import flame as opflame
|
||||
import openpype
|
||||
from pprint import pformat
|
||||
|
||||
|
||||
class FlamePrelaunch(PreLaunchHook):
|
||||
|
|
@ -127,7 +128,6 @@ class FlamePrelaunch(PreLaunchHook):
|
|||
except OSError as exc:
|
||||
self.log.warning("Not able to open files: {}".format(exc))
|
||||
|
||||
|
||||
def _get_flame_fps(self, fps_num):
|
||||
fps_table = {
|
||||
float(23.976): "23.976 fps",
|
||||
|
|
@ -179,7 +179,7 @@ class FlamePrelaunch(PreLaunchHook):
|
|||
"env": self.launch_context.env
|
||||
}
|
||||
|
||||
openpype.api.run_subprocess(args, **process_kwargs)
|
||||
run_subprocess(args, **process_kwargs)
|
||||
|
||||
# process returned json file to pass launch args
|
||||
return_json_data = open(tmp_json_path).read()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ class BatchMovieCreator(TrayPublishCreator):
|
|||
folders=False,
|
||||
single_item=False,
|
||||
extensions=self.extensions,
|
||||
allow_sequences=False,
|
||||
label="Filepath"
|
||||
),
|
||||
BoolDef(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,33 @@
|
|||
import os
|
||||
import logging
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
log = logging.getLogger("Vendor utils")
|
||||
|
||||
|
||||
class CachedToolPaths:
|
||||
"""Cache already used and discovered tools and their executables.
|
||||
|
||||
Discovering path can take some time and can trigger subprocesses so it's
|
||||
better to cache the paths on first get.
|
||||
"""
|
||||
|
||||
_cached_paths = {}
|
||||
|
||||
@classmethod
|
||||
def is_tool_cached(cls, tool):
|
||||
return tool in cls._cached_paths
|
||||
|
||||
@classmethod
|
||||
def get_executable_path(cls, tool):
|
||||
return cls._cached_paths.get(tool)
|
||||
|
||||
@classmethod
|
||||
def cache_executable_path(cls, tool, path):
|
||||
cls._cached_paths[tool] = path
|
||||
|
||||
|
||||
def is_file_executable(filepath):
|
||||
"""Filepath lead to executable file.
|
||||
|
||||
|
|
@ -98,6 +121,7 @@ def get_vendor_bin_path(bin_app):
|
|||
Returns:
|
||||
str: Path to vendorized binaries folder.
|
||||
"""
|
||||
|
||||
return os.path.join(
|
||||
os.environ["OPENPYPE_ROOT"],
|
||||
"vendor",
|
||||
|
|
@ -107,6 +131,112 @@ def get_vendor_bin_path(bin_app):
|
|||
)
|
||||
|
||||
|
||||
def find_tool_in_custom_paths(paths, tool, validation_func=None):
|
||||
"""Find a tool executable in custom paths.
|
||||
|
||||
Args:
|
||||
paths (Iterable[str]): Iterable of paths where to look for tool.
|
||||
tool (str): Name of tool (binary file) to find in passed paths.
|
||||
validation_func (Function): Custom validation function of path.
|
||||
Function must expect one argument which is path to executable.
|
||||
If not passed only 'find_executable' is used to be able identify
|
||||
if path is valid.
|
||||
|
||||
Reuturns:
|
||||
Union[str, None]: Path to validated executable or None if was not
|
||||
found.
|
||||
"""
|
||||
|
||||
for path in paths:
|
||||
# Skip empty strings
|
||||
if not path:
|
||||
continue
|
||||
|
||||
# Handle cases when path is just an executable
|
||||
# - it allows to use executable from PATH
|
||||
# - basename must match 'tool' value (without extension)
|
||||
extless_path, ext = os.path.splitext(path)
|
||||
if extless_path == tool:
|
||||
executable_path = find_executable(tool)
|
||||
if executable_path and (
|
||||
validation_func is None
|
||||
or validation_func(executable_path)
|
||||
):
|
||||
return executable_path
|
||||
continue
|
||||
|
||||
# Normalize path because it should be a path and check if exists
|
||||
normalized = os.path.normpath(path)
|
||||
if not os.path.exists(normalized):
|
||||
continue
|
||||
|
||||
# Note: Path can be both file and directory
|
||||
|
||||
# If path is a file validate it
|
||||
if os.path.isfile(normalized):
|
||||
basename, ext = os.path.splitext(os.path.basename(path))
|
||||
# Check if the filename has actually the sane bane as 'tool'
|
||||
if basename == tool:
|
||||
executable_path = find_executable(normalized)
|
||||
if executable_path and (
|
||||
validation_func is None
|
||||
or validation_func(executable_path)
|
||||
):
|
||||
return executable_path
|
||||
|
||||
# Check if path is a directory and look for tool inside the dir
|
||||
if os.path.isdir(normalized):
|
||||
executable_path = find_executable(os.path.join(normalized, tool))
|
||||
if executable_path and (
|
||||
validation_func is None
|
||||
or validation_func(executable_path)
|
||||
):
|
||||
return executable_path
|
||||
return None
|
||||
|
||||
|
||||
def _oiio_executable_validation(filepath):
|
||||
"""Validate oiio tool executable if can be executed.
|
||||
|
||||
Validation has 2 steps. First is using 'find_executable' to fill possible
|
||||
missing extension or fill directory then launch executable and validate
|
||||
that it can be executed. For that is used '--help' argument which is fast
|
||||
and does not need any other inputs.
|
||||
|
||||
Any possible crash of missing libraries or invalid build should be catched.
|
||||
|
||||
Main reason is to validate if executable can be executed on OS just running
|
||||
which can be issue ob linux machines.
|
||||
|
||||
Note:
|
||||
It does not validate if the executable is really a oiio tool which
|
||||
should be used.
|
||||
|
||||
Args:
|
||||
filepath (str): Path to executable.
|
||||
|
||||
Returns:
|
||||
bool: Filepath is valid executable.
|
||||
"""
|
||||
|
||||
filepath = find_executable(filepath)
|
||||
if not filepath:
|
||||
return False
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[filepath, "--help"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
proc.wait()
|
||||
return proc.returncode == 0
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def get_oiio_tools_path(tool="oiiotool"):
|
||||
"""Path to vendorized OpenImageIO tool executables.
|
||||
|
||||
|
|
@ -117,10 +247,73 @@ def get_oiio_tools_path(tool="oiiotool"):
|
|||
Default is "oiiotool".
|
||||
"""
|
||||
|
||||
oiio_dir = get_vendor_bin_path("oiio")
|
||||
if platform.system().lower() == "linux":
|
||||
oiio_dir = os.path.join(oiio_dir, "bin")
|
||||
return find_executable(os.path.join(oiio_dir, tool))
|
||||
if CachedToolPaths.is_tool_cached(tool):
|
||||
return CachedToolPaths.get_executable_path(tool)
|
||||
|
||||
custom_paths_str = os.environ.get("OPENPYPE_OIIO_PATHS") or ""
|
||||
tool_executable_path = find_tool_in_custom_paths(
|
||||
custom_paths_str.split(os.pathsep),
|
||||
tool,
|
||||
_oiio_executable_validation
|
||||
)
|
||||
|
||||
if not tool_executable_path:
|
||||
oiio_dir = get_vendor_bin_path("oiio")
|
||||
if platform.system().lower() == "linux":
|
||||
oiio_dir = os.path.join(oiio_dir, "bin")
|
||||
default_path = os.path.join(oiio_dir, tool)
|
||||
if _oiio_executable_validation(default_path):
|
||||
tool_executable_path = default_path
|
||||
|
||||
# Look to PATH for the tool
|
||||
if not tool_executable_path:
|
||||
from_path = find_executable(tool)
|
||||
if from_path and _oiio_executable_validation(from_path):
|
||||
tool_executable_path = from_path
|
||||
|
||||
CachedToolPaths.cache_executable_path(tool, tool_executable_path)
|
||||
return tool_executable_path
|
||||
|
||||
|
||||
def _ffmpeg_executable_validation(filepath):
|
||||
"""Validate ffmpeg tool executable if can be executed.
|
||||
|
||||
Validation has 2 steps. First is using 'find_executable' to fill possible
|
||||
missing extension or fill directory then launch executable and validate
|
||||
that it can be executed. For that is used '-version' argument which is fast
|
||||
and does not need any other inputs.
|
||||
|
||||
Any possible crash of missing libraries or invalid build should be catched.
|
||||
|
||||
Main reason is to validate if executable can be executed on OS just running
|
||||
which can be issue ob linux machines.
|
||||
|
||||
Note:
|
||||
It does not validate if the executable is really a ffmpeg tool.
|
||||
|
||||
Args:
|
||||
filepath (str): Path to executable.
|
||||
|
||||
Returns:
|
||||
bool: Filepath is valid executable.
|
||||
"""
|
||||
|
||||
filepath = find_executable(filepath)
|
||||
if not filepath:
|
||||
return False
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[filepath, "-version"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
proc.wait()
|
||||
return proc.returncode == 0
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def get_ffmpeg_tool_path(tool="ffmpeg"):
|
||||
|
|
@ -133,10 +326,33 @@ def get_ffmpeg_tool_path(tool="ffmpeg"):
|
|||
Returns:
|
||||
str: Full path to ffmpeg executable.
|
||||
"""
|
||||
ffmpeg_dir = get_vendor_bin_path("ffmpeg")
|
||||
if platform.system().lower() == "windows":
|
||||
ffmpeg_dir = os.path.join(ffmpeg_dir, "bin")
|
||||
return find_executable(os.path.join(ffmpeg_dir, tool))
|
||||
|
||||
if CachedToolPaths.is_tool_cached(tool):
|
||||
return CachedToolPaths.get_executable_path(tool)
|
||||
|
||||
custom_paths_str = os.environ.get("OPENPYPE_FFMPEG_PATHS") or ""
|
||||
tool_executable_path = find_tool_in_custom_paths(
|
||||
custom_paths_str.split(os.pathsep),
|
||||
tool,
|
||||
_ffmpeg_executable_validation
|
||||
)
|
||||
|
||||
if not tool_executable_path:
|
||||
ffmpeg_dir = get_vendor_bin_path("ffmpeg")
|
||||
if platform.system().lower() == "windows":
|
||||
ffmpeg_dir = os.path.join(ffmpeg_dir, "bin")
|
||||
tool_path = find_executable(os.path.join(ffmpeg_dir, tool))
|
||||
if tool_path and _ffmpeg_executable_validation(tool_path):
|
||||
tool_executable_path = tool_path
|
||||
|
||||
# Look to PATH for the tool
|
||||
if not tool_executable_path:
|
||||
from_path = find_executable(tool)
|
||||
if from_path and _oiio_executable_validation(from_path):
|
||||
tool_executable_path = from_path
|
||||
|
||||
CachedToolPaths.cache_executable_path(tool, tool_executable_path)
|
||||
return tool_executable_path
|
||||
|
||||
|
||||
def is_oiio_supported():
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
125
openpype/modules/kitsu/actions/launcher_show_in_kitsu.py
Normal file
125
openpype/modules/kitsu/actions/launcher_show_in_kitsu.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import webbrowser
|
||||
|
||||
from openpype.pipeline import LauncherAction
|
||||
from openpype.modules import ModulesManager
|
||||
from openpype.client import get_project, get_asset_by_name
|
||||
|
||||
|
||||
class ShowInKitsu(LauncherAction):
|
||||
name = "showinkitsu"
|
||||
label = "Show in Kitsu"
|
||||
icon = "external-link-square"
|
||||
color = "#e0e1e1"
|
||||
order = 10
|
||||
|
||||
@staticmethod
|
||||
def get_kitsu_module():
|
||||
return ModulesManager().modules_by_name.get("kitsu")
|
||||
|
||||
def is_compatible(self, session):
|
||||
if not session.get("AVALON_PROJECT"):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def process(self, session, **kwargs):
|
||||
|
||||
# Context inputs
|
||||
project_name = session["AVALON_PROJECT"]
|
||||
asset_name = session.get("AVALON_ASSET", None)
|
||||
task_name = session.get("AVALON_TASK", None)
|
||||
|
||||
project = get_project(project_name=project_name,
|
||||
fields=["data.zou_id"])
|
||||
if not project:
|
||||
raise RuntimeError(f"Project {project_name} not found.")
|
||||
|
||||
project_zou_id = project["data"].get("zou_id")
|
||||
if not project_zou_id:
|
||||
raise RuntimeError(f"Project {project_name} has no "
|
||||
f"connected kitsu id.")
|
||||
|
||||
asset_zou_name = None
|
||||
asset_zou_id = None
|
||||
asset_zou_type = 'Assets'
|
||||
task_zou_id = None
|
||||
zou_sub_type = ['AssetType', 'Sequence']
|
||||
if asset_name:
|
||||
asset_zou_name = asset_name
|
||||
asset_fields = ["data.zou.id", "data.zou.type"]
|
||||
if task_name:
|
||||
asset_fields.append(f"data.tasks.{task_name}.zou.id")
|
||||
|
||||
asset = get_asset_by_name(project_name,
|
||||
asset_name=asset_name,
|
||||
fields=asset_fields)
|
||||
|
||||
asset_zou_data = asset["data"].get("zou")
|
||||
|
||||
if asset_zou_data:
|
||||
asset_zou_type = asset_zou_data["type"]
|
||||
if asset_zou_type not in zou_sub_type:
|
||||
asset_zou_id = asset_zou_data["id"]
|
||||
else:
|
||||
asset_zou_type = asset_name
|
||||
|
||||
if task_name:
|
||||
task_data = asset["data"]["tasks"][task_name]
|
||||
task_zou_data = task_data.get("zou", {})
|
||||
if not task_zou_data:
|
||||
self.log.debug(f"No zou task data for task: {task_name}")
|
||||
task_zou_id = task_zou_data["id"]
|
||||
|
||||
# Define URL
|
||||
url = self.get_url(project_id=project_zou_id,
|
||||
asset_name=asset_zou_name,
|
||||
asset_id=asset_zou_id,
|
||||
asset_type=asset_zou_type,
|
||||
task_id=task_zou_id)
|
||||
|
||||
# Open URL in webbrowser
|
||||
self.log.info(f"Opening URL: {url}")
|
||||
webbrowser.open(url,
|
||||
# Try in new tab
|
||||
new=2)
|
||||
|
||||
def get_url(self,
|
||||
project_id,
|
||||
asset_name=None,
|
||||
asset_id=None,
|
||||
asset_type=None,
|
||||
task_id=None):
|
||||
|
||||
shots_url = {'Shots', 'Sequence', 'Shot'}
|
||||
sub_type = {'AssetType', 'Sequence'}
|
||||
kitsu_module = self.get_kitsu_module()
|
||||
|
||||
# Get kitsu url with /api stripped
|
||||
kitsu_url = kitsu_module.server_url
|
||||
if kitsu_url.endswith("/api"):
|
||||
kitsu_url = kitsu_url[:-len("/api")]
|
||||
|
||||
sub_url = f"/productions/{project_id}"
|
||||
asset_type_url = "Shots" if asset_type in shots_url else "Assets"
|
||||
|
||||
if task_id:
|
||||
# Go to task page
|
||||
# /productions/{project-id}/{asset_type}/tasks/{task_id}
|
||||
sub_url += f"/{asset_type_url}/tasks/{task_id}"
|
||||
|
||||
elif asset_id:
|
||||
# Go to asset or shot page
|
||||
# /productions/{project-id}/assets/{entity_id}
|
||||
# /productions/{project-id}/shots/{entity_id}
|
||||
sub_url += f"/{asset_type_url}/{asset_id}"
|
||||
|
||||
else:
|
||||
# Go to project page
|
||||
# Project page must end with a view
|
||||
# /productions/{project-id}/assets/
|
||||
# Add search method if is a sub_type
|
||||
sub_url += f"/{asset_type_url}"
|
||||
if asset_type in sub_type:
|
||||
sub_url += f'?search={asset_name}'
|
||||
|
||||
return f"{kitsu_url}{sub_url}"
|
||||
|
|
@ -89,7 +89,10 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction):
|
|||
"""Implementation of abstract method for `IPluginPaths`."""
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
return {"publish": [os.path.join(current_dir, "plugins", "publish")]}
|
||||
return {
|
||||
"publish": [os.path.join(current_dir, "plugins", "publish")],
|
||||
"actions": [os.path.join(current_dir, "actions")]
|
||||
}
|
||||
|
||||
def cli(self, click_group):
|
||||
click_group.add_command(cli_main)
|
||||
|
|
|
|||
|
|
@ -115,7 +115,9 @@ def update_op_assets(
|
|||
item_data["frameStart"] = frame_in
|
||||
# Frames duration, fallback on 0
|
||||
try:
|
||||
frames_duration = int(item_data.pop("nb_frames", 0))
|
||||
# NOTE nb_frames is stored directly in item
|
||||
# because of zou's legacy design
|
||||
frames_duration = int(item.get("nb_frames", 0))
|
||||
except (TypeError, ValueError):
|
||||
frames_duration = 0
|
||||
# Frame out, fallback on frame_in + duration or project's value or 1001
|
||||
|
|
@ -170,7 +172,7 @@ def update_op_assets(
|
|||
# Substitute item type for general classification (assets or shots)
|
||||
if item_type in ["Asset", "AssetType"]:
|
||||
entity_root_asset_name = "Assets"
|
||||
elif item_type in ["Episode", "Sequence"]:
|
||||
elif item_type in ["Episode", "Sequence", "Shot"]:
|
||||
entity_root_asset_name = "Shots"
|
||||
|
||||
# Root parent folder if exist
|
||||
|
|
@ -276,11 +278,13 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne:
|
|||
|
||||
match_res = re.match(r"(\d+)x(\d+)", project["resolution"])
|
||||
if match_res:
|
||||
project_data['resolutionWidth'] = int(match_res.group(1))
|
||||
project_data['resolutionHeight'] = int(match_res.group(2))
|
||||
project_data["resolutionWidth"] = int(match_res.group(1))
|
||||
project_data["resolutionHeight"] = int(match_res.group(2))
|
||||
else:
|
||||
log.warning(f"\'{project['resolution']}\' does not match the expected"
|
||||
" format for the resolution, for example: 1920x1080")
|
||||
log.warning(
|
||||
f"'{project['resolution']}' does not match the expected"
|
||||
" format for the resolution, for example: 1920x1080"
|
||||
)
|
||||
|
||||
return UpdateOne(
|
||||
{"_id": project_doc["_id"]},
|
||||
|
|
|
|||
|
|
@ -205,6 +205,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.
|
||||
|
|
@ -338,6 +348,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."""
|
||||
|
||||
|
|
@ -735,6 +760,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.
|
||||
|
|
@ -825,6 +941,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."""
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
405
openpype/tools/publisher/control_qt.py
Normal file
405
openpype/tools/publisher/control_qt.py
Normal 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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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_()
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ as a naive barier to prevent artists from accidental setting changes.
|
|||
**`Disk mapping`** - Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up.
|
||||
Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume).
|
||||
|
||||
### FFmpeg and OpenImageIO tools
|
||||
We bundle FFmpeg tools for all platforms and OpenImageIO tools for Windows and Linux. By default are used bundled tools but it is possible to set environment variables `OPENPYPE_FFMPEG_PATHS` and `OPENPYPE_OIIO_PATHS` in system settings environments to look for them in different directory e.g. for different linux distributions or to add oiio support for MacOs. Values of both environment variables should lead to directory where tool executables are located (multiple paths are supported).
|
||||
|
||||
### OpenPype deployment control
|
||||
**`Versions Repository`** - Location where automatic update mechanism searches for zip files with
|
||||
OpenPype update packages. To read more about preparing OpenPype for automatic updates go to [Admin Distribute docs](admin_distribute.md#2-openpype-codebase)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue