mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge branch 'develop' into enhancement/1142-attach-reviewables-to-other-instances-using-enumdef-on-attachable-integrator
This commit is contained in:
commit
ca2c6264a1
28 changed files with 1210 additions and 407 deletions
|
|
@ -8,7 +8,6 @@ from pathlib import Path
|
|||
import warnings
|
||||
|
||||
import click
|
||||
import acre
|
||||
|
||||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.addon import AddonsManager
|
||||
|
|
@ -18,6 +17,11 @@ from ayon_core.lib import (
|
|||
is_running_from_build,
|
||||
Logger,
|
||||
)
|
||||
from ayon_core.lib.env_tools import (
|
||||
parse_env_variables_structure,
|
||||
compute_env_variables_structure,
|
||||
merge_env_variables,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
@ -235,19 +239,15 @@ def version(build):
|
|||
|
||||
def _set_global_environments() -> None:
|
||||
"""Set global AYON environments."""
|
||||
general_env = get_general_environments()
|
||||
# First resolve general environment
|
||||
general_env = parse_env_variables_structure(get_general_environments())
|
||||
|
||||
# first resolve general environment because merge doesn't expect
|
||||
# values to be list.
|
||||
# TODO: switch to AYON environment functions
|
||||
merged_env = acre.merge(
|
||||
acre.compute(acre.parse(general_env), cleanup=False),
|
||||
# Merge environments with current environments and update values
|
||||
merged_env = merge_env_variables(
|
||||
compute_env_variables_structure(general_env),
|
||||
dict(os.environ)
|
||||
)
|
||||
env = acre.compute(
|
||||
merged_env,
|
||||
cleanup=False
|
||||
)
|
||||
env = compute_env_variables_structure(merged_env)
|
||||
os.environ.clear()
|
||||
os.environ.update(env)
|
||||
|
||||
|
|
@ -263,8 +263,8 @@ def _set_addons_environments(addons_manager):
|
|||
|
||||
# Merge environments with current environments and update values
|
||||
if module_envs := addons_manager.collect_global_environments():
|
||||
parsed_envs = acre.parse(module_envs)
|
||||
env = acre.merge(parsed_envs, dict(os.environ))
|
||||
parsed_envs = parse_env_variables_structure(module_envs)
|
||||
env = merge_env_variables(parsed_envs, dict(os.environ))
|
||||
os.environ.clear()
|
||||
os.environ.update(env)
|
||||
|
||||
|
|
|
|||
|
|
@ -550,29 +550,38 @@ class EnumDef(AbstractAttrDef):
|
|||
passed items or list of values for multiselection.
|
||||
multiselection (Optional[bool]): If True, multiselection is allowed.
|
||||
Output is list of selected items.
|
||||
placeholder (Optional[str]): Placeholder for UI purposes, only for
|
||||
multiselection enumeration.
|
||||
|
||||
"""
|
||||
type = "enum"
|
||||
|
||||
type_attributes = [
|
||||
"multiselection",
|
||||
"placeholder",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: str,
|
||||
items: "EnumItemsInputType",
|
||||
default: "Union[str, List[Any]]" = None,
|
||||
multiselection: Optional[bool] = False,
|
||||
placeholder: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
if not items:
|
||||
raise ValueError((
|
||||
"Empty 'items' value. {} must have"
|
||||
if multiselection is None:
|
||||
multiselection = False
|
||||
|
||||
if not items and not multiselection:
|
||||
raise ValueError(
|
||||
f"Empty 'items' value. {self.__class__.__name__} must have"
|
||||
" defined values on initialization."
|
||||
).format(self.__class__.__name__))
|
||||
)
|
||||
|
||||
items = self.prepare_enum_items(items)
|
||||
item_values = [item["value"] for item in items]
|
||||
item_values_set = set(item_values)
|
||||
if multiselection is None:
|
||||
multiselection = False
|
||||
|
||||
if multiselection:
|
||||
if default is None:
|
||||
|
|
@ -587,6 +596,7 @@ class EnumDef(AbstractAttrDef):
|
|||
self.items: List["EnumItemDict"] = items
|
||||
self._item_values: Set[Any] = item_values_set
|
||||
self.multiselection: bool = multiselection
|
||||
self.placeholder: Optional[str] = placeholder
|
||||
|
||||
def convert_value(self, value):
|
||||
if not self.multiselection:
|
||||
|
|
@ -612,7 +622,6 @@ class EnumDef(AbstractAttrDef):
|
|||
def serialize(self):
|
||||
data = super().serialize()
|
||||
data["items"] = copy.deepcopy(self.items)
|
||||
data["multiselection"] = self.multiselection
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -1,7 +1,34 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
import platform
|
||||
import typing
|
||||
import collections
|
||||
from string import Formatter
|
||||
from typing import Optional
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Union, Literal
|
||||
|
||||
PlatformName = Literal["windows", "linux", "darwin"]
|
||||
EnvValue = Union[str, list[str], dict[str, str], dict[str, list[str]]]
|
||||
|
||||
|
||||
def env_value_to_bool(env_key=None, value=None, default=False):
|
||||
class CycleError(ValueError):
|
||||
"""Raised when a cycle is detected in dynamic env variables compute."""
|
||||
pass
|
||||
|
||||
|
||||
class DynamicKeyClashError(Exception):
|
||||
"""Raised when dynamic key clashes with an existing key."""
|
||||
pass
|
||||
|
||||
|
||||
def env_value_to_bool(
|
||||
env_key: Optional[str] = None,
|
||||
value: Optional[str] = None,
|
||||
default: bool = False,
|
||||
) -> bool:
|
||||
"""Convert environment variable value to boolean.
|
||||
|
||||
Function is based on value of the environemt variable. Value is lowered
|
||||
|
|
@ -11,6 +38,7 @@ def env_value_to_bool(env_key=None, value=None, default=False):
|
|||
bool: If value match to one of ["true", "yes", "1"] result if True
|
||||
but if value match to ["false", "no", "0"] result is False else
|
||||
default value is returned.
|
||||
|
||||
"""
|
||||
if value is None and env_key is None:
|
||||
return default
|
||||
|
|
@ -27,18 +55,23 @@ def env_value_to_bool(env_key=None, value=None, default=False):
|
|||
return default
|
||||
|
||||
|
||||
def get_paths_from_environ(env_key=None, env_value=None, return_first=False):
|
||||
def get_paths_from_environ(
|
||||
env_key: Optional[str] = None,
|
||||
env_value: Optional[str] = None,
|
||||
return_first: bool = False,
|
||||
) -> Optional[Union[str, list[str]]]:
|
||||
"""Return existing paths from specific environment variable.
|
||||
|
||||
Args:
|
||||
env_key (str): Environment key where should look for paths.
|
||||
env_value (str): Value of environment variable. Argument `env_key` is
|
||||
skipped if this argument is entered.
|
||||
env_key (Optional[str]): Environment key where should look for paths.
|
||||
env_value (Optional[str]): Value of environment variable.
|
||||
Argument `env_key` is skipped if this argument is entered.
|
||||
return_first (bool): Return first found value or return list of found
|
||||
paths. `None` or empty list returned if nothing found.
|
||||
|
||||
Returns:
|
||||
str, list, None: Result of found path/s.
|
||||
Optional[Union[str, list[str]]]: Result of found path/s.
|
||||
|
||||
"""
|
||||
existing_paths = []
|
||||
if not env_key and not env_value:
|
||||
|
|
@ -69,3 +102,225 @@ def get_paths_from_environ(env_key=None, env_value=None, return_first=False):
|
|||
return None
|
||||
# Return all existing paths from environment variable
|
||||
return existing_paths
|
||||
|
||||
|
||||
def parse_env_variables_structure(
|
||||
env: dict[str, EnvValue],
|
||||
platform_name: Optional[PlatformName] = None
|
||||
) -> dict[str, str]:
|
||||
"""Parse environment for platform-specific values and paths as lists.
|
||||
|
||||
Args:
|
||||
env (dict): The source environment to read.
|
||||
platform_name (Optional[PlatformName]): Name of platform to parse for.
|
||||
Defaults to current platform.
|
||||
|
||||
Returns:
|
||||
dict: The flattened environment for a platform.
|
||||
|
||||
"""
|
||||
if platform_name is None:
|
||||
platform_name = platform.system().lower()
|
||||
|
||||
# Separator based on OS 'os.pathsep' is ';' on Windows and ':' on Unix
|
||||
sep = ";" if platform_name == "windows" else ":"
|
||||
|
||||
result = {}
|
||||
for variable, value in env.items():
|
||||
# Platform specific values
|
||||
if isinstance(value, dict):
|
||||
value = value.get(platform_name)
|
||||
|
||||
# Allow to have lists as values in the tool data
|
||||
if isinstance(value, (list, tuple)):
|
||||
value = sep.join(value)
|
||||
|
||||
if not value:
|
||||
continue
|
||||
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"Expected 'str' got '{type(value)}'")
|
||||
|
||||
result[variable] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _topological_sort(
|
||||
dependencies: dict[str, set[str]]
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""Sort values subject to dependency constraints.
|
||||
|
||||
Args:
|
||||
dependencies (dict[str, set[str]): Mapping of environment variable
|
||||
keys to a set of keys they depend on.
|
||||
|
||||
Returns:
|
||||
tuple[list[str], list[str]]: A tuple of two lists. The first list
|
||||
contains the ordered keys in which order should be environment
|
||||
keys filled, the second list contains the keys that would cause
|
||||
cyclic fill of values.
|
||||
|
||||
"""
|
||||
num_heads = collections.defaultdict(int) # num arrows pointing in
|
||||
tails = collections.defaultdict(list) # list of arrows going out
|
||||
heads = [] # unique list of heads in order first seen
|
||||
for head, tail_values in dependencies.items():
|
||||
for tail_value in tail_values:
|
||||
num_heads[tail_value] += 1
|
||||
if head not in tails:
|
||||
heads.append(head)
|
||||
tails[head].append(tail_value)
|
||||
|
||||
ordered = [head for head in heads if head not in num_heads]
|
||||
for head in ordered:
|
||||
for tail in tails[head]:
|
||||
num_heads[tail] -= 1
|
||||
if not num_heads[tail]:
|
||||
ordered.append(tail)
|
||||
cyclic = [tail for tail, heads in num_heads.items() if heads]
|
||||
return ordered, cyclic
|
||||
|
||||
|
||||
class _PartialFormatDict(dict):
|
||||
"""This supports partial formatting.
|
||||
|
||||
Missing keys are replaced with the return value of __missing__.
|
||||
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._missing_template: str = "{{{key}}}"
|
||||
|
||||
def set_missing_template(self, template: str):
|
||||
self._missing_template = template
|
||||
|
||||
def __missing__(self, key: str) -> str:
|
||||
return self._missing_template.format(key=key)
|
||||
|
||||
|
||||
def _partial_format(
|
||||
value: str,
|
||||
data: dict[str, str],
|
||||
missing_template: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Return string `s` formatted by `data` allowing a partial format
|
||||
|
||||
Arguments:
|
||||
value (str): The string that will be formatted
|
||||
data (dict): The dictionary used to format with.
|
||||
missing_template (Optional[str]): The template to use when a key is
|
||||
missing from the data. If `None`, the key will remain unformatted.
|
||||
|
||||
Example:
|
||||
>>> _partial_format("{d} {a} {b} {c} {d}", {'b': "and", 'd': "left"})
|
||||
'left {a} and {c} left'
|
||||
|
||||
"""
|
||||
|
||||
mapping = _PartialFormatDict(**data)
|
||||
if missing_template is not None:
|
||||
mapping.set_missing_template(missing_template)
|
||||
|
||||
formatter = Formatter()
|
||||
try:
|
||||
output = formatter.vformat(value, (), mapping)
|
||||
except Exception:
|
||||
r_token = re.compile(r"({.*?})")
|
||||
output = value
|
||||
for match in re.findall(r_token, value):
|
||||
try:
|
||||
output = re.sub(match, match.format(**data), output)
|
||||
except (KeyError, ValueError, IndexError):
|
||||
continue
|
||||
return output
|
||||
|
||||
|
||||
def compute_env_variables_structure(
|
||||
env: dict[str, str],
|
||||
fill_dynamic_keys: bool = True,
|
||||
) -> dict[str, str]:
|
||||
"""Compute the result from recursive dynamic environment.
|
||||
|
||||
Note: Keys that are not present in the data will remain unformatted as the
|
||||
original keys. So they can be formatted against the current user
|
||||
environment when merging. So {"A": "{key}"} will remain {key} if not
|
||||
present in the dynamic environment.
|
||||
|
||||
"""
|
||||
env = env.copy()
|
||||
|
||||
# Collect dependencies
|
||||
dependencies = collections.defaultdict(set)
|
||||
for key, value in env.items():
|
||||
dependent_keys = re.findall("{(.+?)}", value)
|
||||
for dependent_key in dependent_keys:
|
||||
# Ignore reference to itself or key is not in env
|
||||
if dependent_key != key and dependent_key in env:
|
||||
dependencies[key].add(dependent_key)
|
||||
|
||||
ordered, cyclic = _topological_sort(dependencies)
|
||||
|
||||
# Check cycle
|
||||
if cyclic:
|
||||
raise CycleError(f"A cycle is detected on: {cyclic}")
|
||||
|
||||
# Format dynamic values
|
||||
for key in reversed(ordered):
|
||||
if key in env:
|
||||
if not isinstance(env[key], str):
|
||||
continue
|
||||
data = env.copy()
|
||||
data.pop(key) # format without itself
|
||||
env[key] = _partial_format(env[key], data=data)
|
||||
|
||||
# Format dynamic keys
|
||||
if fill_dynamic_keys:
|
||||
formatted = {}
|
||||
for key, value in env.items():
|
||||
if not isinstance(value, str):
|
||||
formatted[key] = value
|
||||
continue
|
||||
|
||||
new_key = _partial_format(key, data=env)
|
||||
if new_key in formatted:
|
||||
raise DynamicKeyClashError(
|
||||
f"Key clashes on: {new_key} (source: {key})"
|
||||
)
|
||||
|
||||
formatted[new_key] = value
|
||||
env = formatted
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def merge_env_variables(
|
||||
src_env: dict[str, str],
|
||||
dst_env: dict[str, str],
|
||||
missing_template: Optional[str] = None,
|
||||
) -> dict[str, str]:
|
||||
"""Merge the tools environment with the 'current_env'.
|
||||
|
||||
This finalizes the join with a current environment by formatting the
|
||||
remainder of dynamic variables with that from the current environment.
|
||||
|
||||
Remaining missing variables result in an empty value.
|
||||
|
||||
Args:
|
||||
src_env (dict): The dynamic environment
|
||||
dst_env (dict): The target environment variables mapping to merge
|
||||
the dynamic environment into.
|
||||
missing_template (str): Argument passed to '_partial_format' during
|
||||
merging. `None` should keep missing keys unchanged.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: The resulting environment after the merge.
|
||||
|
||||
"""
|
||||
result = dst_env.copy()
|
||||
for key, value in src_env.items():
|
||||
result[key] = _partial_format(
|
||||
str(value), dst_env, missing_template
|
||||
)
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from datetime import datetime
|
|||
from abc import ABC, abstractmethod
|
||||
from functools import lru_cache
|
||||
|
||||
import appdirs
|
||||
import platformdirs
|
||||
import ayon_api
|
||||
|
||||
_PLACEHOLDER = object()
|
||||
|
|
@ -17,7 +17,7 @@ _PLACEHOLDER = object()
|
|||
|
||||
def _get_ayon_appdirs(*args):
|
||||
return os.path.join(
|
||||
appdirs.user_data_dir("AYON", "Ynput"),
|
||||
platformdirs.user_data_dir("AYON", "Ynput"),
|
||||
*args
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ from ayon_core.pipeline.plugin_discover import (
|
|||
from ayon_core.pipeline.create import (
|
||||
discover_legacy_creator_plugins,
|
||||
CreateContext,
|
||||
HiddenCreator,
|
||||
)
|
||||
|
||||
_NOT_SET = object()
|
||||
|
|
@ -309,7 +310,13 @@ class AbstractTemplateBuilder(ABC):
|
|||
self._creators_by_name = creators_by_name
|
||||
|
||||
def _collect_creators(self):
|
||||
self._creators_by_name = dict(self.create_context.creators)
|
||||
self._creators_by_name = {
|
||||
identifier: creator
|
||||
for identifier, creator
|
||||
in self.create_context.manual_creators.items()
|
||||
# Do not list HiddenCreator even though it is a 'manual creator'
|
||||
if not isinstance(creator, HiddenCreator)
|
||||
}
|
||||
|
||||
def get_creators_by_name(self):
|
||||
if self._creators_by_name is None:
|
||||
|
|
|
|||
|
|
@ -116,11 +116,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
|
||||
if not_found_folder_paths:
|
||||
joined_folder_paths = ", ".join(
|
||||
["\"{}\"".format(path) for path in not_found_folder_paths]
|
||||
[f"\"{path}\"" for path in not_found_folder_paths]
|
||||
)
|
||||
self.log.warning(
|
||||
f"Not found folder entities with paths {joined_folder_paths}."
|
||||
)
|
||||
self.log.warning((
|
||||
"Not found folder entities with paths \"{}\"."
|
||||
).format(joined_folder_paths))
|
||||
|
||||
def fill_missing_task_entities(self, context, project_name):
|
||||
self.log.debug("Querying task entities for instances.")
|
||||
|
|
|
|||
|
|
@ -32,10 +32,12 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin):
|
|||
|
||||
for key in [
|
||||
"AYON_BUNDLE_NAME",
|
||||
"AYON_DEFAULT_SETTINGS_VARIANT",
|
||||
"AYON_USE_STAGING",
|
||||
"AYON_IN_TESTS",
|
||||
# NOTE Not sure why workdir is needed?
|
||||
"AYON_WORKDIR",
|
||||
# DEPRECATED remove when deadline stops using it (added in 1.1.2)
|
||||
"AYON_DEFAULT_SETTINGS_VARIANT",
|
||||
]:
|
||||
value = os.getenv(key)
|
||||
if value:
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ class ExtractOTIOReview(
|
|||
)
|
||||
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info("Adding representation: {}".format(representation))
|
||||
self.log.debug("Adding representation: {}".format(representation))
|
||||
|
||||
def _create_representation(self, start, duration):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from operator import attrgetter
|
||||
import dataclasses
|
||||
import os
|
||||
from typing import Dict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pyblish.api
|
||||
try:
|
||||
|
|
@ -14,7 +14,8 @@ from ayon_core.lib import (
|
|||
BoolDef,
|
||||
UISeparatorDef,
|
||||
UILabelDef,
|
||||
EnumDef
|
||||
EnumDef,
|
||||
filter_profiles
|
||||
)
|
||||
try:
|
||||
from ayon_core.pipeline.usdlib import (
|
||||
|
|
@ -281,6 +282,9 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"fx": 500,
|
||||
"lighting": 600,
|
||||
}
|
||||
# Default profiles to set certain instance attribute defaults based on
|
||||
# profiles in settings
|
||||
profiles: List[Dict[str, Any]] = []
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings):
|
||||
|
|
@ -298,6 +302,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
if contribution_layers:
|
||||
cls.contribution_layers = contribution_layers
|
||||
|
||||
cls.profiles = plugin_settings.get("profiles", [])
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
|
|
@ -463,6 +469,28 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
if not cls.instance_matches_plugin_families(instance):
|
||||
return []
|
||||
|
||||
# Set default target layer based on product type
|
||||
current_context_task_type = create_context.get_current_task_type()
|
||||
profile = filter_profiles(cls.profiles, {
|
||||
"product_types": instance.data["productType"],
|
||||
"task_types": current_context_task_type
|
||||
})
|
||||
if not profile:
|
||||
profile = {}
|
||||
|
||||
# Define defaults
|
||||
default_contribution_layer = profile.get(
|
||||
"contribution_layer", None)
|
||||
default_apply_as_variant = profile.get(
|
||||
"contribution_apply_as_variant", False)
|
||||
default_target_product = profile.get(
|
||||
"contribution_target_product", "usdAsset")
|
||||
default_init_as = (
|
||||
"asset"
|
||||
if profile.get("contribution_target_product") == "usdAsset"
|
||||
else "shot")
|
||||
init_as_visible = False
|
||||
|
||||
# Attributes logic
|
||||
publish_attributes = instance["publish_attributes"].get(
|
||||
cls.__name__, {})
|
||||
|
|
@ -495,7 +523,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"the contribution itself will be added to the "
|
||||
"department layer."
|
||||
),
|
||||
default="usdAsset",
|
||||
default=default_target_product,
|
||||
visible=visible),
|
||||
EnumDef("contribution_target_product_init",
|
||||
label="Initialize as",
|
||||
|
|
@ -507,8 +535,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"setting will do nothing."
|
||||
),
|
||||
items=["asset", "shot"],
|
||||
default="asset",
|
||||
visible=visible),
|
||||
default=default_init_as,
|
||||
visible=visible and init_as_visible),
|
||||
|
||||
# Asset layer, e.g. model.usd, look.usd, rig.usd
|
||||
EnumDef("contribution_layer",
|
||||
|
|
@ -520,7 +548,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"the list) will contribute as a stronger opinion."
|
||||
),
|
||||
items=list(cls.contribution_layers.keys()),
|
||||
default="model",
|
||||
default=default_contribution_layer,
|
||||
visible=visible),
|
||||
BoolDef("contribution_apply_as_variant",
|
||||
label="Add as variant",
|
||||
|
|
@ -532,7 +560,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
"appended to as a sublayer to the department layer "
|
||||
"instead."
|
||||
),
|
||||
default=True,
|
||||
default=default_apply_as_variant,
|
||||
visible=visible),
|
||||
TextDef("contribution_variant_set_name",
|
||||
label="Variant Set Name",
|
||||
|
|
@ -588,31 +616,6 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin,
|
|||
instance.set_publish_plugin_attr_defs(cls.__name__, new_attrs)
|
||||
|
||||
|
||||
class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions):
|
||||
"""
|
||||
This is solely here to expose the attribute definitions for the
|
||||
Houdini "look" family.
|
||||
"""
|
||||
# TODO: Improve how this is built for the look family
|
||||
hosts = ["houdini"]
|
||||
families = ["look"]
|
||||
label = CollectUSDLayerContributions.label + " (Look)"
|
||||
|
||||
@classmethod
|
||||
def get_attr_defs_for_instance(cls, create_context, instance):
|
||||
# Filtering of instance, if needed, can be customized
|
||||
if not cls.instance_matches_plugin_families(instance):
|
||||
return []
|
||||
|
||||
defs = super().get_attr_defs_for_instance(create_context, instance)
|
||||
|
||||
# Update default for department layer to look
|
||||
layer_def = next(d for d in defs if d.key == "contribution_layer")
|
||||
layer_def.default = "look"
|
||||
|
||||
return defs
|
||||
|
||||
|
||||
class ValidateUSDDependencies(pyblish.api.InstancePlugin):
|
||||
families = ["usdLayer"]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import copy
|
|||
import typing
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core.lib.attribute_definitions import (
|
||||
AbstractAttrDef,
|
||||
|
|
@ -22,6 +22,8 @@ from ayon_core.tools.utils import (
|
|||
FocusSpinBox,
|
||||
FocusDoubleSpinBox,
|
||||
MultiSelectionComboBox,
|
||||
PlaceholderLineEdit,
|
||||
PlaceholderPlainTextEdit,
|
||||
set_style_property,
|
||||
)
|
||||
from ayon_core.tools.utils import NiceCheckbox
|
||||
|
|
@ -502,9 +504,9 @@ class TextAttrWidget(_BaseAttrDefWidget):
|
|||
|
||||
self.multiline = self.attr_def.multiline
|
||||
if self.multiline:
|
||||
input_widget = QtWidgets.QPlainTextEdit(self)
|
||||
input_widget = PlaceholderPlainTextEdit(self)
|
||||
else:
|
||||
input_widget = QtWidgets.QLineEdit(self)
|
||||
input_widget = PlaceholderLineEdit(self)
|
||||
|
||||
# Override context menu event to add revert to default action
|
||||
input_widget.contextMenuEvent = self._input_widget_context_event
|
||||
|
|
@ -641,7 +643,9 @@ class EnumAttrWidget(_BaseAttrDefWidget):
|
|||
|
||||
def _ui_init(self):
|
||||
if self.multiselection:
|
||||
input_widget = MultiSelectionComboBox(self)
|
||||
input_widget = MultiSelectionComboBox(
|
||||
self, placeholder=self.attr_def.placeholder
|
||||
)
|
||||
|
||||
else:
|
||||
input_widget = CustomTextComboBox(self)
|
||||
|
|
@ -655,6 +659,9 @@ class EnumAttrWidget(_BaseAttrDefWidget):
|
|||
for item in self.attr_def.items:
|
||||
input_widget.addItem(item["label"], item["value"])
|
||||
|
||||
if not self.attr_def.items:
|
||||
self._add_empty_item(input_widget)
|
||||
|
||||
idx = input_widget.findData(self.attr_def.default)
|
||||
if idx >= 0:
|
||||
input_widget.setCurrentIndex(idx)
|
||||
|
|
@ -671,6 +678,20 @@ class EnumAttrWidget(_BaseAttrDefWidget):
|
|||
input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
input_widget.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
def _add_empty_item(self, input_widget):
|
||||
model = input_widget.model()
|
||||
if not isinstance(model, QtGui.QStandardItemModel):
|
||||
return
|
||||
|
||||
root_item = model.invisibleRootItem()
|
||||
|
||||
empty_item = QtGui.QStandardItem()
|
||||
empty_item.setData("< No items to select >", QtCore.Qt.DisplayRole)
|
||||
empty_item.setData("", QtCore.Qt.UserRole)
|
||||
empty_item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
|
||||
root_item.appendRow(empty_item)
|
||||
|
||||
def _on_context_menu(self, pos):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import collections
|
||||
import contextlib
|
||||
import typing
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.lib import NestedCacheItem
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Union
|
||||
|
||||
HIERARCHY_MODEL_SENDER = "hierarchy.model"
|
||||
|
||||
|
||||
|
|
@ -82,19 +88,26 @@ class TaskItem:
|
|||
Args:
|
||||
task_id (str): Task id.
|
||||
name (str): Name of task.
|
||||
name (Union[str, None]): Task label.
|
||||
task_type (str): Type of task.
|
||||
parent_id (str): Parent folder id.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, task_id, name, task_type, parent_id
|
||||
self,
|
||||
task_id: str,
|
||||
name: str,
|
||||
label: Union[str, None],
|
||||
task_type: str,
|
||||
parent_id: str,
|
||||
):
|
||||
self.task_id = task_id
|
||||
self.name = name
|
||||
self.label = label
|
||||
self.task_type = task_type
|
||||
self.parent_id = parent_id
|
||||
|
||||
self._label = None
|
||||
self._full_label = None
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
|
|
@ -107,16 +120,17 @@ class TaskItem:
|
|||
return self.task_id
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
def full_label(self):
|
||||
"""Label of task item for UI.
|
||||
|
||||
Returns:
|
||||
str: Label of task item.
|
||||
"""
|
||||
|
||||
if self._label is None:
|
||||
self._label = "{} ({})".format(self.name, self.task_type)
|
||||
return self._label
|
||||
if self._full_label is None:
|
||||
label = self.label or self.name
|
||||
self._full_label = f"{label} ({self.task_type})"
|
||||
return self._full_label
|
||||
|
||||
def to_data(self):
|
||||
"""Converts task item to data.
|
||||
|
|
@ -128,6 +142,7 @@ class TaskItem:
|
|||
return {
|
||||
"task_id": self.task_id,
|
||||
"name": self.name,
|
||||
"label": self.label,
|
||||
"parent_id": self.parent_id,
|
||||
"task_type": self.task_type,
|
||||
}
|
||||
|
|
@ -159,6 +174,7 @@ def _get_task_items_from_tasks(tasks):
|
|||
output.append(TaskItem(
|
||||
task["id"],
|
||||
task["name"],
|
||||
task["label"],
|
||||
task["type"],
|
||||
folder_id
|
||||
))
|
||||
|
|
@ -368,7 +384,7 @@ class HierarchyModel(object):
|
|||
sender (Union[str, None]): Who requested the task item.
|
||||
|
||||
Returns:
|
||||
Union[TaskItem, None]: Task item found by name and folder id.
|
||||
Optional[TaskItem]: Task item found by name and folder id.
|
||||
|
||||
"""
|
||||
for task_item in self.get_task_items(project_name, folder_id, sender):
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ class AttributesWidget(QtWidgets.QWidget):
|
|||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING)
|
||||
layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)
|
||||
layout.setColumnStretch(0, 0)
|
||||
layout.setColumnStretch(1, 1)
|
||||
|
||||
self._layout = layout
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ from . import settings, util
|
|||
from .awesome import tags as awesome
|
||||
from qtpy import QtCore, QtGui
|
||||
import qtawesome
|
||||
from six import text_type
|
||||
from .constants import PluginStates, InstanceStates, GroupStates, Roles
|
||||
|
||||
|
||||
|
|
@ -985,7 +984,7 @@ class TerminalModel(QtGui.QStandardItemModel):
|
|||
record_item = record
|
||||
else:
|
||||
record_item = {
|
||||
"label": text_type(record.msg),
|
||||
"label": str(record.msg),
|
||||
"type": "record",
|
||||
"levelno": record.levelno,
|
||||
"threadName": record.threadName,
|
||||
|
|
@ -993,7 +992,7 @@ class TerminalModel(QtGui.QStandardItemModel):
|
|||
"filename": record.filename,
|
||||
"pathname": record.pathname,
|
||||
"lineno": record.lineno,
|
||||
"msg": text_type(record.msg),
|
||||
"msg": str(record.msg),
|
||||
"msecs": record.msecs,
|
||||
"levelname": record.levelname
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import sys
|
|||
import collections
|
||||
|
||||
from qtpy import QtCore
|
||||
from six import text_type
|
||||
import pyblish.api
|
||||
|
||||
root = os.path.dirname(__file__)
|
||||
|
|
@ -64,7 +63,7 @@ def u_print(msg, **kwargs):
|
|||
**kwargs: Keyword argument for `print` function.
|
||||
"""
|
||||
|
||||
if isinstance(msg, text_type):
|
||||
if isinstance(msg, str):
|
||||
encoding = None
|
||||
try:
|
||||
encoding = os.getenv('PYTHONIOENCODING', sys.stdout.encoding)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ from __future__ import print_function
|
|||
import json
|
||||
import os
|
||||
|
||||
import six
|
||||
from qtpy import QtCore, QtGui
|
||||
|
||||
|
||||
|
|
@ -152,7 +151,7 @@ class IconicFont(QtCore.QObject):
|
|||
def hook(obj):
|
||||
result = {}
|
||||
for key in obj:
|
||||
result[key] = six.unichr(int(obj[key], 16))
|
||||
result[key] = chr(int(obj[key], 16))
|
||||
return result
|
||||
|
||||
if directory is None:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from .widgets import (
|
|||
ComboBox,
|
||||
CustomTextComboBox,
|
||||
PlaceholderLineEdit,
|
||||
PlaceholderPlainTextEdit,
|
||||
ElideLabel,
|
||||
HintedLineEdit,
|
||||
ExpandingTextEdit,
|
||||
|
|
@ -89,6 +90,7 @@ __all__ = (
|
|||
"ComboBox",
|
||||
"CustomTextComboBox",
|
||||
"PlaceholderLineEdit",
|
||||
"PlaceholderPlainTextEdit",
|
||||
"ElideLabel",
|
||||
"HintedLineEdit",
|
||||
"ExpandingTextEdit",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
|
||||
from ayon_core.style import get_objected_colors
|
||||
|
||||
from .lib import (
|
||||
checkstate_int_to_enum,
|
||||
checkstate_enum_to_int,
|
||||
|
|
@ -45,15 +47,16 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
|
|||
top_bottom_padding = 2
|
||||
left_right_padding = 3
|
||||
left_offset = 4
|
||||
top_bottom_margins = 2
|
||||
top_bottom_margins = 1
|
||||
item_spacing = 5
|
||||
|
||||
item_bg_color = QtGui.QColor("#31424e")
|
||||
_placeholder_color = None
|
||||
|
||||
def __init__(
|
||||
self, parent=None, placeholder="", separator=", ", **kwargs
|
||||
):
|
||||
super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs)
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
self.setObjectName("MultiSelectionComboBox")
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
|
|
@ -61,7 +64,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
|
|||
self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True)
|
||||
self._initial_mouse_pos = None
|
||||
self._separator = separator
|
||||
self._placeholder_text = placeholder
|
||||
self._placeholder_text = placeholder or ""
|
||||
delegate = ComboItemDelegate(self)
|
||||
self.setItemDelegate(delegate)
|
||||
|
||||
|
|
@ -74,7 +77,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
|
|||
return self._placeholder_text
|
||||
|
||||
def set_placeholder_text(self, text):
|
||||
self._placeholder_text = text
|
||||
self._placeholder_text = text or ""
|
||||
self._update_size_hint()
|
||||
|
||||
def set_custom_text(self, text):
|
||||
|
|
@ -206,19 +209,36 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
|
|||
combotext = self._placeholder_text
|
||||
else:
|
||||
draw_text = False
|
||||
if draw_text:
|
||||
option.currentText = combotext
|
||||
option.palette.setCurrentColorGroup(QtGui.QPalette.Disabled)
|
||||
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option)
|
||||
return
|
||||
|
||||
font_metricts = self.fontMetrics()
|
||||
if draw_text:
|
||||
color = self._get_placeholder_color()
|
||||
pen = painter.pen()
|
||||
pen.setColor(color)
|
||||
painter.setPen(pen)
|
||||
|
||||
left_x = option.rect.left() + self.left_offset
|
||||
|
||||
font = self.font()
|
||||
# This is hardcoded point size from styles
|
||||
font.setPointSize(10)
|
||||
painter.setFont(font)
|
||||
|
||||
label_rect = QtCore.QRect(option.rect)
|
||||
label_rect.moveLeft(left_x)
|
||||
|
||||
painter.drawText(
|
||||
label_rect,
|
||||
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
|
||||
combotext
|
||||
)
|
||||
return
|
||||
|
||||
if self._item_height is None:
|
||||
self.updateGeometry()
|
||||
self.update()
|
||||
return
|
||||
|
||||
font_metrics = self.fontMetrics()
|
||||
for line, items in self._lines.items():
|
||||
top_y = (
|
||||
option.rect.top()
|
||||
|
|
@ -227,7 +247,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
|
|||
)
|
||||
left_x = option.rect.left() + self.left_offset
|
||||
for item in items:
|
||||
label_rect = font_metricts.boundingRect(item)
|
||||
label_rect = font_metrics.boundingRect(item)
|
||||
label_height = label_rect.height()
|
||||
|
||||
label_rect.moveTop(top_y)
|
||||
|
|
@ -237,22 +257,25 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
|
|||
label_rect.width() + self.left_right_padding
|
||||
)
|
||||
|
||||
bg_rect = QtCore.QRectF(label_rect)
|
||||
bg_rect.setWidth(
|
||||
label_rect.width() + self.left_right_padding
|
||||
)
|
||||
left_x = bg_rect.right() + self.item_spacing
|
||||
if not draw_text:
|
||||
bg_rect = QtCore.QRectF(label_rect)
|
||||
bg_rect.setWidth(
|
||||
label_rect.width() + self.left_right_padding
|
||||
)
|
||||
left_x = bg_rect.right() + self.item_spacing
|
||||
|
||||
bg_rect.setHeight(
|
||||
label_height + (2 * self.top_bottom_padding)
|
||||
)
|
||||
bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins)
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(bg_rect, 5, 5)
|
||||
|
||||
painter.fillPath(path, self.item_bg_color)
|
||||
|
||||
label_rect.moveLeft(label_rect.x() + self.left_right_padding)
|
||||
|
||||
bg_rect.setHeight(label_height + (2 * self.top_bottom_padding))
|
||||
bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins)
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(bg_rect, 5, 5)
|
||||
|
||||
painter.fillPath(path, self.item_bg_color)
|
||||
|
||||
painter.drawText(
|
||||
label_rect,
|
||||
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
|
||||
|
|
@ -287,11 +310,11 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
|
|||
line = 0
|
||||
self._lines = {line: []}
|
||||
|
||||
font_metricts = self.fontMetrics()
|
||||
font_metrics = self.fontMetrics()
|
||||
default_left_x = 0 + self.left_offset
|
||||
left_x = int(default_left_x)
|
||||
for item in items:
|
||||
rect = font_metricts.boundingRect(item)
|
||||
rect = font_metrics.boundingRect(item)
|
||||
width = rect.width() + (2 * self.left_right_padding)
|
||||
right_x = left_x + width
|
||||
if right_x > total_width:
|
||||
|
|
@ -382,3 +405,12 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
|
|||
return event.ignore()
|
||||
|
||||
return super(MultiSelectionComboBox, self).keyPressEvent(event)
|
||||
|
||||
@classmethod
|
||||
def _get_placeholder_color(cls):
|
||||
if cls._placeholder_color is None:
|
||||
color_obj = get_objected_colors("font")
|
||||
color = color_obj.get_qcolor()
|
||||
color.setAlpha(67)
|
||||
cls._placeholder_color = color
|
||||
return cls._placeholder_color
|
||||
|
|
|
|||
|
|
@ -328,6 +328,9 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
if frame_rect.width() < 0 or frame_rect.height() < 0:
|
||||
return
|
||||
|
||||
frame_rect.setLeft(frame_rect.x() + (frame_rect.width() % 2))
|
||||
frame_rect.setTop(frame_rect.y() + (frame_rect.height() % 2))
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
|
|
@ -364,18 +367,23 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
margin_size_c = 0
|
||||
|
||||
checkbox_rect = QtCore.QRect(
|
||||
frame_rect.x() + margin_size_c,
|
||||
frame_rect.y() + margin_size_c,
|
||||
frame_rect.width() - (margin_size_c * 2),
|
||||
frame_rect.height() - (margin_size_c * 2)
|
||||
frame_rect.x(),
|
||||
frame_rect.y(),
|
||||
frame_rect.width(),
|
||||
frame_rect.height()
|
||||
)
|
||||
if margin_size_c:
|
||||
checkbox_rect.adjust(
|
||||
margin_size_c, margin_size_c,
|
||||
-margin_size_c, -margin_size_c
|
||||
)
|
||||
|
||||
if checkbox_rect.width() > checkbox_rect.height():
|
||||
radius = floor(checkbox_rect.height() * 0.5)
|
||||
else:
|
||||
radius = floor(checkbox_rect.width() * 0.5)
|
||||
|
||||
painter.setPen(QtCore.Qt.transparent)
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(bg_color)
|
||||
painter.drawRoundedRect(checkbox_rect, radius, radius)
|
||||
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
task_type_item_by_name,
|
||||
task_type_icon_cache
|
||||
)
|
||||
item.setData(task_item.label, QtCore.Qt.DisplayRole)
|
||||
item.setData(task_item.full_label, QtCore.Qt.DisplayRole)
|
||||
item.setData(name, ITEM_NAME_ROLE)
|
||||
item.setData(task_item.id, ITEM_ID_ROLE)
|
||||
item.setData(task_item.task_type, TASK_TYPE_ROLE)
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class ComboBox(QtWidgets.QComboBox):
|
|||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ComboBox, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
delegate = QtWidgets.QStyledItemDelegate()
|
||||
self.setItemDelegate(delegate)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
|
@ -63,7 +63,7 @@ class ComboBox(QtWidgets.QComboBox):
|
|||
|
||||
def wheelEvent(self, event):
|
||||
if self.hasFocus():
|
||||
return super(ComboBox, self).wheelEvent(event)
|
||||
return super().wheelEvent(event)
|
||||
|
||||
|
||||
class CustomTextComboBox(ComboBox):
|
||||
|
|
@ -71,7 +71,7 @@ class CustomTextComboBox(ComboBox):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._custom_text = None
|
||||
super(CustomTextComboBox, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def set_custom_text(self, text=None):
|
||||
if self._custom_text != text:
|
||||
|
|
@ -88,23 +88,48 @@ class CustomTextComboBox(ComboBox):
|
|||
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option)
|
||||
|
||||
|
||||
class PlaceholderLineEdit(QtWidgets.QLineEdit):
|
||||
"""Set placeholder color of QLineEdit in Qt 5.12 and higher."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PlaceholderLineEdit, self).__init__(*args, **kwargs)
|
||||
# Change placeholder palette color
|
||||
if hasattr(QtGui.QPalette, "PlaceholderText"):
|
||||
filter_palette = self.palette()
|
||||
class _Cache:
|
||||
_placeholder_color = None
|
||||
|
||||
@classmethod
|
||||
def get_placeholder_color(cls):
|
||||
if cls._placeholder_color is None:
|
||||
color_obj = get_objected_colors("font")
|
||||
color = color_obj.get_qcolor()
|
||||
color.setAlpha(67)
|
||||
cls._placeholder_color = color
|
||||
return cls._placeholder_color
|
||||
|
||||
|
||||
class PlaceholderLineEdit(QtWidgets.QLineEdit):
|
||||
"""Set placeholder color of QLineEdit in Qt 5.12 and higher."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Change placeholder palette color
|
||||
if hasattr(QtGui.QPalette, "PlaceholderText"):
|
||||
filter_palette = self.palette()
|
||||
filter_palette.setColor(
|
||||
QtGui.QPalette.PlaceholderText,
|
||||
color
|
||||
_Cache.get_placeholder_color()
|
||||
)
|
||||
self.setPalette(filter_palette)
|
||||
|
||||
|
||||
class PlaceholderPlainTextEdit(QtWidgets.QPlainTextEdit):
|
||||
"""Set placeholder color of QPlainTextEdit in Qt 5.12 and higher."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Change placeholder palette color
|
||||
if hasattr(QtGui.QPalette, "PlaceholderText"):
|
||||
viewport = self.viewport()
|
||||
filter_palette = viewport.palette()
|
||||
filter_palette.setColor(
|
||||
QtGui.QPalette.PlaceholderText,
|
||||
_Cache.get_placeholder_color()
|
||||
)
|
||||
viewport.setPalette(filter_palette)
|
||||
|
||||
|
||||
class ElideLabel(QtWidgets.QLabel):
|
||||
"""Label which elide text.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.1.0+dev"
|
||||
__version__ = "1.1.1+dev"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue