mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
merge develop
This commit is contained in:
commit
5d31e66472
105 changed files with 3886 additions and 2212 deletions
48
.github/workflows/assign_pr_to_project.yml
vendored
Normal file
48
.github/workflows/assign_pr_to_project.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: 🔸Auto assign pr
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
type: string
|
||||
description: "Run workflow for this PR number"
|
||||
required: true
|
||||
project_id:
|
||||
type: string
|
||||
description: "Github Project Number"
|
||||
required: true
|
||||
default: "16"
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
jobs:
|
||||
get-pr-repo:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
pr_repo_name: ${{ steps.get-repo-name.outputs.repo_name || github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
# INFO `github.event.pull_request.head.repo.full_name` is not available on manual triggered (dispatched) runs
|
||||
steps:
|
||||
- name: Get PR repo name
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
id: get-repo-name
|
||||
run: |
|
||||
repo_name=$(gh pr view ${{ inputs.pr_number }} --json headRepository,headRepositoryOwner --repo ${{ github.repository }} | jq -r '.headRepositoryOwner.login + "/" + .headRepository.name')
|
||||
echo "repo_name=$repo_name" >> $GITHUB_OUTPUT
|
||||
|
||||
auto-assign-pr:
|
||||
needs:
|
||||
- get-pr-repo
|
||||
if: ${{ needs.get-pr-repo.outputs.pr_repo_name == github.repository }}
|
||||
uses: ynput/ops-repo-automation/.github/workflows/pr_to_project.yml@main
|
||||
with:
|
||||
repo: "${{ github.repository }}"
|
||||
project_id: ${{ inputs.project_id != '' && fromJSON(inputs.project_id) || 16 }}
|
||||
pull_request_number: ${{ github.event.pull_request.number || fromJSON(inputs.pr_number) }}
|
||||
secrets:
|
||||
# INFO fallback to default `github.token` is required for PRs from forks
|
||||
# INFO organization secrets won't be available to forks
|
||||
token: ${{ secrets.YNPUT_BOT_TOKEN || github.token}}
|
||||
18
.github/workflows/validate_pr_labels.yml
vendored
Normal file
18
.github/workflows/validate_pr_labels.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: 🔎 Validate PR Labels
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- labeled
|
||||
- unlabeled
|
||||
|
||||
jobs:
|
||||
validate-type-label:
|
||||
uses: ynput/ops-repo-automation/.github/workflows/validate_pr_labels.yml@main
|
||||
with:
|
||||
repo: "${{ github.repository }}"
|
||||
pull_request_number: ${{ github.event.pull_request.number }}
|
||||
query_prefix: "type: "
|
||||
secrets:
|
||||
token: ${{ secrets.YNPUT_BOT_TOKEN }}
|
||||
|
|
@ -370,67 +370,11 @@ def _load_ayon_addons(log):
|
|||
return all_addon_modules
|
||||
|
||||
|
||||
def _load_addons_in_core(log):
|
||||
# Add current directory at first place
|
||||
# - has small differences in import logic
|
||||
addon_modules = []
|
||||
modules_dir = os.path.join(AYON_CORE_ROOT, "modules")
|
||||
if not os.path.exists(modules_dir):
|
||||
log.warning(
|
||||
f"Could not find path when loading AYON addons \"{modules_dir}\""
|
||||
)
|
||||
return addon_modules
|
||||
|
||||
ignored_filenames = IGNORED_FILENAMES | IGNORED_DEFAULT_FILENAMES
|
||||
for filename in os.listdir(modules_dir):
|
||||
# Ignore filenames
|
||||
if filename in ignored_filenames:
|
||||
continue
|
||||
|
||||
fullpath = os.path.join(modules_dir, filename)
|
||||
basename, ext = os.path.splitext(filename)
|
||||
|
||||
# Validations
|
||||
if os.path.isdir(fullpath):
|
||||
# Check existence of init file
|
||||
init_path = os.path.join(fullpath, "__init__.py")
|
||||
if not os.path.exists(init_path):
|
||||
log.debug((
|
||||
"Addon directory does not contain __init__.py"
|
||||
f" file {fullpath}"
|
||||
))
|
||||
continue
|
||||
|
||||
elif ext != ".py":
|
||||
continue
|
||||
|
||||
# TODO add more logic how to define if folder is addon or not
|
||||
# - check manifest and content of manifest
|
||||
try:
|
||||
# Don't import dynamically current directory modules
|
||||
import_str = f"ayon_core.modules.{basename}"
|
||||
default_module = __import__(import_str, fromlist=("", ))
|
||||
addon_modules.append(default_module)
|
||||
|
||||
except Exception:
|
||||
log.error(
|
||||
f"Failed to import in-core addon '{basename}'.",
|
||||
exc_info=True
|
||||
)
|
||||
return addon_modules
|
||||
|
||||
|
||||
def _load_addons():
|
||||
log = Logger.get_logger("AddonsLoader")
|
||||
|
||||
addon_modules = _load_ayon_addons(log)
|
||||
# All addon in 'modules' folder are tray actions and should be moved
|
||||
# to tray tool.
|
||||
# TODO remove
|
||||
addon_modules.extend(_load_addons_in_core(log))
|
||||
|
||||
# Store modules to local cache
|
||||
_LoadCache.addon_modules = addon_modules
|
||||
_LoadCache.addon_modules = _load_ayon_addons(log)
|
||||
|
||||
|
||||
class AYONAddon(ABC):
|
||||
|
|
@ -535,8 +479,8 @@ class AYONAddon(ABC):
|
|||
Implementation of this method is optional.
|
||||
|
||||
Note:
|
||||
The logic can be similar to logic in tray, but tray does not require
|
||||
to be logged in.
|
||||
The logic can be similar to logic in tray, but tray does not
|
||||
require to be logged in.
|
||||
|
||||
Args:
|
||||
process_context (ProcessContext): Context of child
|
||||
|
|
@ -950,6 +894,21 @@ class AddonsManager:
|
|||
output.extend(paths)
|
||||
return output
|
||||
|
||||
def collect_launcher_action_paths(self):
|
||||
"""Helper to collect launcher action paths from addons.
|
||||
|
||||
Returns:
|
||||
list: List of paths to launcher actions.
|
||||
|
||||
"""
|
||||
output = self._collect_plugin_paths(
|
||||
"get_launcher_action_paths"
|
||||
)
|
||||
# Add default core actions
|
||||
actions_dir = os.path.join(AYON_CORE_ROOT, "plugins", "actions")
|
||||
output.insert(0, actions_dir)
|
||||
return output
|
||||
|
||||
def collect_create_plugin_paths(self, host_name):
|
||||
"""Helper to collect creator plugin paths from addons.
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,13 @@ class IPluginPaths(AYONInterface):
|
|||
paths = [paths]
|
||||
return paths
|
||||
|
||||
def get_launcher_action_paths(self):
|
||||
"""Receive launcher actions paths.
|
||||
|
||||
Give addons ability to add launcher actions paths.
|
||||
"""
|
||||
return self._get_plugin_paths_by_type("actions")
|
||||
|
||||
def get_create_plugin_paths(self, host_name: str) -> list[str]:
|
||||
"""Receive create plugin paths.
|
||||
|
||||
|
|
@ -154,8 +161,9 @@ class ITrayAddon(AYONInterface):
|
|||
|
||||
"""
|
||||
tray_initialized = False
|
||||
manager: AddonsManager = None
|
||||
_tray_manager: TrayManager = None
|
||||
_tray_manager = None
|
||||
_admin_submenu = None
|
||||
|
||||
@abstractmethod
|
||||
def tray_init(self) -> None:
|
||||
|
|
@ -236,6 +244,27 @@ class ITrayAddon(AYONInterface):
|
|||
if hasattr(self.manager, "add_doubleclick_callback"):
|
||||
self.manager.add_doubleclick_callback(self, callback)
|
||||
|
||||
@staticmethod
|
||||
def admin_submenu(tray_menu):
|
||||
if ITrayAddon._admin_submenu is None:
|
||||
from qtpy import QtWidgets
|
||||
|
||||
admin_submenu = QtWidgets.QMenu("Admin", tray_menu)
|
||||
admin_submenu.menuAction().setVisible(False)
|
||||
ITrayAddon._admin_submenu = admin_submenu
|
||||
return ITrayAddon._admin_submenu
|
||||
|
||||
@staticmethod
|
||||
def add_action_to_admin_submenu(label, tray_menu):
|
||||
from qtpy import QtWidgets
|
||||
|
||||
menu = ITrayAddon.admin_submenu(tray_menu)
|
||||
action = QtWidgets.QAction(label, menu)
|
||||
menu.addAction(action)
|
||||
if not menu.menuAction().isVisible():
|
||||
menu.menuAction().setVisible(True)
|
||||
return action
|
||||
|
||||
|
||||
class ITrayAction(ITrayAddon):
|
||||
"""Implementation of Tray action.
|
||||
|
|
@ -249,7 +278,6 @@ class ITrayAction(ITrayAddon):
|
|||
"""
|
||||
|
||||
admin_action = False
|
||||
_admin_submenu = None
|
||||
_action_item = None
|
||||
|
||||
@property
|
||||
|
|
@ -268,12 +296,7 @@ class ITrayAction(ITrayAddon):
|
|||
from qtpy import QtWidgets
|
||||
|
||||
if self.admin_action:
|
||||
menu = self.admin_submenu(tray_menu)
|
||||
action = QtWidgets.QAction(self.label, menu)
|
||||
menu.addAction(action)
|
||||
if not menu.menuAction().isVisible():
|
||||
menu.menuAction().setVisible(True)
|
||||
|
||||
action = self.add_action_to_admin_submenu(self.label, tray_menu)
|
||||
else:
|
||||
action = QtWidgets.QAction(self.label, tray_menu)
|
||||
tray_menu.addAction(action)
|
||||
|
|
@ -289,17 +312,6 @@ class ITrayAction(ITrayAddon):
|
|||
"""Cleanup method which is executed on tray shutdown."""
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def admin_submenu(tray_menu: QtWidgets.QMenu) -> QtWidgets.QMenu:
|
||||
"""Get or create admin submenu."""
|
||||
if ITrayAction._admin_submenu is None:
|
||||
from qtpy import QtWidgets
|
||||
|
||||
admin_submenu = QtWidgets.QMenu("Admin", tray_menu)
|
||||
admin_submenu.menuAction().setVisible(False)
|
||||
ITrayAction._admin_submenu = admin_submenu
|
||||
return ITrayAction._admin_submenu
|
||||
|
||||
|
||||
class ITrayService(ITrayAddon):
|
||||
"""Tray service Interface."""
|
||||
|
|
|
|||
|
|
@ -146,7 +146,8 @@ def publish_report_viewer():
|
|||
@main_cli.command()
|
||||
@click.argument("output_path")
|
||||
@click.option("--project", help="Define project context")
|
||||
@click.option("--folder", help="Define folder in project (project must be set)")
|
||||
@click.option(
|
||||
"--folder", help="Define folder in project (project must be set)")
|
||||
@click.option(
|
||||
"--strict",
|
||||
is_flag=True,
|
||||
|
|
|
|||
|
|
@ -6,82 +6,58 @@ import json
|
|||
import copy
|
||||
import warnings
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Any, Optional
|
||||
import typing
|
||||
from typing import (
|
||||
Any,
|
||||
Optional,
|
||||
List,
|
||||
Set,
|
||||
Dict,
|
||||
Iterable,
|
||||
TypeVar,
|
||||
)
|
||||
|
||||
import clique
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Self, Tuple, Union, TypedDict, Pattern
|
||||
|
||||
|
||||
class EnumItemDict(TypedDict):
|
||||
label: str
|
||||
value: Any
|
||||
|
||||
|
||||
EnumItemsInputType = Union[
|
||||
Dict[Any, str],
|
||||
List[Tuple[Any, str]],
|
||||
List[Any],
|
||||
List[EnumItemDict]
|
||||
]
|
||||
|
||||
|
||||
class FileDefItemDict(TypedDict):
|
||||
directory: str
|
||||
filenames: List[str]
|
||||
frames: Optional[List[int]]
|
||||
template: Optional[str]
|
||||
is_sequence: Optional[bool]
|
||||
|
||||
|
||||
# Global variable which store attribute definitions by type
|
||||
# - default types are registered on import
|
||||
_attr_defs_by_type = {}
|
||||
|
||||
|
||||
def register_attr_def_class(cls):
|
||||
"""Register attribute definition.
|
||||
|
||||
Currently registered definitions are used to deserialize data to objects.
|
||||
|
||||
Attrs:
|
||||
cls (AbstractAttrDef): 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.
|
||||
|
||||
Args:
|
||||
attribute_definitions (List[AbstractAttrDef]): Objects of attribute
|
||||
definitions.
|
||||
|
||||
Returns:
|
||||
Set[str]: Keys that will be created using passed attribute definitions.
|
||||
"""
|
||||
|
||||
keys = set()
|
||||
if not attribute_definitions:
|
||||
return keys
|
||||
|
||||
for attribute_def in attribute_definitions:
|
||||
if not isinstance(attribute_def, UIDef):
|
||||
keys.add(attribute_def.key)
|
||||
return keys
|
||||
|
||||
|
||||
def get_default_values(attribute_definitions):
|
||||
"""Receive default values for attribute definitions.
|
||||
|
||||
Args:
|
||||
attribute_definitions (List[AbstractAttrDef]): Attribute definitions
|
||||
for which default values should be collected.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Default values for passed attribute definitions.
|
||||
"""
|
||||
|
||||
output = {}
|
||||
if not attribute_definitions:
|
||||
return output
|
||||
|
||||
for attr_def in attribute_definitions:
|
||||
# Skip UI definitions
|
||||
if not isinstance(attr_def, UIDef):
|
||||
output[attr_def.key] = attr_def.default
|
||||
return output
|
||||
# Type hint helpers
|
||||
IntFloatType = "Union[int, float]"
|
||||
|
||||
|
||||
class AbstractAttrDefMeta(ABCMeta):
|
||||
"""Metaclass to validate the existence of 'key' attribute.
|
||||
|
||||
Each object of `AbstractAttrDef` must have defined 'key' attribute.
|
||||
"""
|
||||
|
||||
"""
|
||||
def __call__(cls, *args, **kwargs):
|
||||
obj = super(AbstractAttrDefMeta, cls).__call__(*args, **kwargs)
|
||||
init_class = getattr(obj, "__init__class__", None)
|
||||
|
|
@ -93,8 +69,12 @@ class AbstractAttrDefMeta(ABCMeta):
|
|||
|
||||
|
||||
def _convert_reversed_attr(
|
||||
main_value, depr_value, main_label, depr_label, default
|
||||
):
|
||||
main_value: Any,
|
||||
depr_value: Any,
|
||||
main_label: str,
|
||||
depr_label: str,
|
||||
default: Any,
|
||||
) -> Any:
|
||||
if main_value is not None and depr_value is not None:
|
||||
if main_value == depr_value:
|
||||
print(
|
||||
|
|
@ -140,8 +120,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
enabled (Optional[bool]): Item is enabled (for UI purposes).
|
||||
hidden (Optional[bool]): DEPRECATED: Use 'visible' instead.
|
||||
disabled (Optional[bool]): DEPRECATED: Use 'enabled' instead.
|
||||
"""
|
||||
|
||||
"""
|
||||
type_attributes = []
|
||||
|
||||
is_value_def = True
|
||||
|
|
@ -183,7 +163,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
def id(self) -> str:
|
||||
return self._id
|
||||
|
||||
def clone(self):
|
||||
def clone(self) -> "Self":
|
||||
data = self.serialize()
|
||||
data.pop("type")
|
||||
return self.deserialize(data)
|
||||
|
|
@ -251,28 +231,28 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
|
||||
Returns:
|
||||
str: Type of attribute definition.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> Any:
|
||||
"""Convert value to a valid one.
|
||||
|
||||
Convert passed value to a valid type. Use default if value can't be
|
||||
converted.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self) -> Dict[str, Any]:
|
||||
"""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,
|
||||
|
|
@ -288,7 +268,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
return data
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, data):
|
||||
def deserialize(cls, data: Dict[str, Any]) -> "Self":
|
||||
"""Recreate object from data.
|
||||
|
||||
Data can be received using 'serialize' method.
|
||||
|
|
@ -299,10 +279,12 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
|
||||
return cls(**data)
|
||||
|
||||
def _def_type_compare(self, other: "AbstractAttrDef") -> bool:
|
||||
def _def_type_compare(self, other: "Self") -> bool:
|
||||
return True
|
||||
|
||||
|
||||
AttrDefType = TypeVar("AttrDefType", bound=AbstractAttrDef)
|
||||
|
||||
# -----------------------------------------
|
||||
# UI attribute definitions won't hold value
|
||||
# -----------------------------------------
|
||||
|
|
@ -310,13 +292,19 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
|
|||
class UIDef(AbstractAttrDef):
|
||||
is_value_def = False
|
||||
|
||||
def __init__(self, key=None, default=None, *args, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
key: Optional[str] = None,
|
||||
default: Optional[Any] = None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(key, default, *args, **kwargs)
|
||||
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -343,18 +331,18 @@ class UnknownDef(AbstractAttrDef):
|
|||
|
||||
This attribute can be used to keep existing data unchanged but does not
|
||||
have known definition of type.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "unknown"
|
||||
|
||||
def __init__(self, key, default=None, **kwargs):
|
||||
def __init__(self, key: str, default: Optional[Any] = None, **kwargs):
|
||||
kwargs["default"] = default
|
||||
super().__init__(key, **kwargs)
|
||||
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -365,11 +353,11 @@ class HiddenDef(AbstractAttrDef):
|
|||
to other attributes (e.g. in multi-page UIs).
|
||||
|
||||
Keep in mind the value should be possible to parse by json parser.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "hidden"
|
||||
|
||||
def __init__(self, key, default=None, **kwargs):
|
||||
def __init__(self, key: str, default: Optional[Any] = None, **kwargs):
|
||||
kwargs["default"] = default
|
||||
kwargs["visible"] = False
|
||||
super().__init__(key, **kwargs)
|
||||
|
|
@ -377,7 +365,7 @@ class HiddenDef(AbstractAttrDef):
|
|||
def is_value_valid(self, value: Any) -> bool:
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> Any:
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -392,8 +380,8 @@ class NumberDef(AbstractAttrDef):
|
|||
maximum(int, float): Maximum possible value.
|
||||
decimals(int): Maximum decimal points of value.
|
||||
default(int, float): Default value for conversion.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "number"
|
||||
type_attributes = [
|
||||
"minimum",
|
||||
|
|
@ -402,7 +390,12 @@ class NumberDef(AbstractAttrDef):
|
|||
]
|
||||
|
||||
def __init__(
|
||||
self, key, minimum=None, maximum=None, decimals=None, default=None,
|
||||
self,
|
||||
key: str,
|
||||
minimum: Optional[IntFloatType] = None,
|
||||
maximum: Optional[IntFloatType] = None,
|
||||
decimals: Optional[int] = None,
|
||||
default: Optional[IntFloatType] = None,
|
||||
**kwargs
|
||||
):
|
||||
minimum = 0 if minimum is None else minimum
|
||||
|
|
@ -428,9 +421,9 @@ class NumberDef(AbstractAttrDef):
|
|||
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
||||
self.minimum = minimum
|
||||
self.maximum = maximum
|
||||
self.decimals = 0 if decimals is None else decimals
|
||||
self.minimum: IntFloatType = minimum
|
||||
self.maximum: IntFloatType = maximum
|
||||
self.decimals: int = 0 if decimals is None else decimals
|
||||
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
if self.decimals == 0:
|
||||
|
|
@ -442,7 +435,7 @@ class NumberDef(AbstractAttrDef):
|
|||
return False
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> IntFloatType:
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
value = float(value)
|
||||
|
|
@ -477,8 +470,8 @@ class TextDef(AbstractAttrDef):
|
|||
regex(str, re.Pattern): Regex validation.
|
||||
placeholder(str): UI placeholder for attribute.
|
||||
default(str, None): Default value. Empty string used when not defined.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "text"
|
||||
type_attributes = [
|
||||
"multiline",
|
||||
|
|
@ -486,7 +479,12 @@ class TextDef(AbstractAttrDef):
|
|||
]
|
||||
|
||||
def __init__(
|
||||
self, key, multiline=None, regex=None, placeholder=None, default=None,
|
||||
self,
|
||||
key: str,
|
||||
multiline: Optional[bool] = None,
|
||||
regex: Optional[str] = None,
|
||||
placeholder: Optional[str] = None,
|
||||
default: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
if default is None:
|
||||
|
|
@ -505,9 +503,9 @@ class TextDef(AbstractAttrDef):
|
|||
if isinstance(regex, str):
|
||||
regex = re.compile(regex)
|
||||
|
||||
self.multiline = multiline
|
||||
self.placeholder = placeholder
|
||||
self.regex = regex
|
||||
self.multiline: bool = multiline
|
||||
self.placeholder: Optional[str] = placeholder
|
||||
self.regex: Optional["Pattern"] = regex
|
||||
|
||||
def is_value_valid(self, value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
|
|
@ -516,12 +514,12 @@ class TextDef(AbstractAttrDef):
|
|||
return False
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> str:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return self.default
|
||||
|
||||
def serialize(self):
|
||||
def serialize(self) -> Dict[str, Any]:
|
||||
data = super().serialize()
|
||||
regex = None
|
||||
if self.regex is not None:
|
||||
|
|
@ -545,18 +543,24 @@ class EnumDef(AbstractAttrDef):
|
|||
is enabled.
|
||||
|
||||
Args:
|
||||
items (Union[list[str], list[dict[str, Any]]): Items definition that
|
||||
can be converted using 'prepare_enum_items'.
|
||||
key (str): Key under which value is stored.
|
||||
items (EnumItemsInputType): Items definition that can be converted
|
||||
using 'prepare_enum_items'.
|
||||
default (Optional[Any]): Default value. Must be one key(value) from
|
||||
passed items or list of values for multiselection.
|
||||
multiselection (Optional[bool]): If True, multiselection is allowed.
|
||||
Output is list of selected items.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "enum"
|
||||
|
||||
def __init__(
|
||||
self, key, items, default=None, multiselection=False, **kwargs
|
||||
self,
|
||||
key: str,
|
||||
items: "EnumItemsInputType",
|
||||
default: "Union[str, List[Any]]" = None,
|
||||
multiselection: Optional[bool] = False,
|
||||
**kwargs
|
||||
):
|
||||
if not items:
|
||||
raise ValueError((
|
||||
|
|
@ -567,6 +571,9 @@ class EnumDef(AbstractAttrDef):
|
|||
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:
|
||||
default = []
|
||||
|
|
@ -577,9 +584,9 @@ class EnumDef(AbstractAttrDef):
|
|||
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
||||
self.items = items
|
||||
self._item_values = item_values_set
|
||||
self.multiselection = multiselection
|
||||
self.items: List["EnumItemDict"] = items
|
||||
self._item_values: Set[Any] = item_values_set
|
||||
self.multiselection: bool = multiselection
|
||||
|
||||
def convert_value(self, value):
|
||||
if not self.multiselection:
|
||||
|
|
@ -609,7 +616,9 @@ class EnumDef(AbstractAttrDef):
|
|||
return data
|
||||
|
||||
@staticmethod
|
||||
def prepare_enum_items(items):
|
||||
def prepare_enum_items(
|
||||
items: "EnumItemsInputType"
|
||||
) -> List["EnumItemDict"]:
|
||||
"""Convert items to unified structure.
|
||||
|
||||
Output is a list where each item is dictionary with 'value'
|
||||
|
|
@ -625,13 +634,12 @@ class EnumDef(AbstractAttrDef):
|
|||
```
|
||||
|
||||
Args:
|
||||
items (Union[Dict[str, Any], List[Any], List[Dict[str, Any]]): The
|
||||
items to convert.
|
||||
items (EnumItemsInputType): The items to convert.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Unified structure of items.
|
||||
"""
|
||||
List[EnumItemDict]: Unified structure of items.
|
||||
|
||||
"""
|
||||
output = []
|
||||
if isinstance(items, dict):
|
||||
for value, label in items.items():
|
||||
|
|
@ -682,11 +690,11 @@ class BoolDef(AbstractAttrDef):
|
|||
|
||||
Args:
|
||||
default(bool): Default value. Set to `False` if not defined.
|
||||
"""
|
||||
|
||||
"""
|
||||
type = "bool"
|
||||
|
||||
def __init__(self, key, default=None, **kwargs):
|
||||
def __init__(self, key: str, default: Optional[bool] = None, **kwargs):
|
||||
if default is None:
|
||||
default = False
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
|
@ -694,7 +702,7 @@ class BoolDef(AbstractAttrDef):
|
|||
def is_value_valid(self, value: Any) -> bool:
|
||||
return isinstance(value, bool)
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(self, value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return self.default
|
||||
|
|
@ -702,7 +710,11 @@ class BoolDef(AbstractAttrDef):
|
|||
|
||||
class FileDefItem:
|
||||
def __init__(
|
||||
self, directory, filenames, frames=None, template=None
|
||||
self,
|
||||
directory: str,
|
||||
filenames: List[str],
|
||||
frames: Optional[List[int]] = None,
|
||||
template: Optional[str] = None,
|
||||
):
|
||||
self.directory = directory
|
||||
|
||||
|
|
@ -731,7 +743,7 @@ class FileDefItem:
|
|||
)
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
def label(self) -> Optional[str]:
|
||||
if self.is_empty:
|
||||
return None
|
||||
|
||||
|
|
@ -774,7 +786,7 @@ class FileDefItem:
|
|||
filename_template, ",".join(ranges)
|
||||
)
|
||||
|
||||
def split_sequence(self):
|
||||
def split_sequence(self) -> List["Self"]:
|
||||
if not self.is_sequence:
|
||||
raise ValueError("Cannot split single file item")
|
||||
|
||||
|
|
@ -785,7 +797,7 @@ class FileDefItem:
|
|||
return self.from_paths(paths, False)
|
||||
|
||||
@property
|
||||
def ext(self):
|
||||
def ext(self) -> Optional[str]:
|
||||
if self.is_empty:
|
||||
return None
|
||||
_, ext = os.path.splitext(self.filenames[0])
|
||||
|
|
@ -794,14 +806,14 @@ class FileDefItem:
|
|||
return None
|
||||
|
||||
@property
|
||||
def lower_ext(self):
|
||||
def lower_ext(self) -> Optional[str]:
|
||||
ext = self.ext
|
||||
if ext is not None:
|
||||
return ext.lower()
|
||||
return ext
|
||||
|
||||
@property
|
||||
def is_dir(self):
|
||||
def is_dir(self) -> bool:
|
||||
if self.is_empty:
|
||||
return False
|
||||
|
||||
|
|
@ -810,10 +822,15 @@ class FileDefItem:
|
|||
return False
|
||||
return True
|
||||
|
||||
def set_directory(self, directory):
|
||||
def set_directory(self, directory: str):
|
||||
self.directory = directory
|
||||
|
||||
def set_filenames(self, filenames, frames=None, template=None):
|
||||
def set_filenames(
|
||||
self,
|
||||
filenames: List[str],
|
||||
frames: Optional[List[int]] = None,
|
||||
template: Optional[str] = None,
|
||||
):
|
||||
if frames is None:
|
||||
frames = []
|
||||
is_sequence = False
|
||||
|
|
@ -830,17 +847,21 @@ class FileDefItem:
|
|||
self.is_sequence = is_sequence
|
||||
|
||||
@classmethod
|
||||
def create_empty_item(cls):
|
||||
def create_empty_item(cls) -> "Self":
|
||||
return cls("", "")
|
||||
|
||||
@classmethod
|
||||
def from_value(cls, value, allow_sequences):
|
||||
def from_value(
|
||||
cls,
|
||||
value: "Union[List[FileDefItemDict], FileDefItemDict]",
|
||||
allow_sequences: bool,
|
||||
) -> List["Self"]:
|
||||
"""Convert passed value to FileDefItem objects.
|
||||
|
||||
Returns:
|
||||
list: Created FileDefItem objects.
|
||||
"""
|
||||
|
||||
"""
|
||||
# Convert single item to iterable
|
||||
if not isinstance(value, (list, tuple, set)):
|
||||
value = [value]
|
||||
|
|
@ -872,7 +893,7 @@ class FileDefItem:
|
|||
return output
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
def from_dict(cls, data: "FileDefItemDict") -> "Self":
|
||||
return cls(
|
||||
data["directory"],
|
||||
data["filenames"],
|
||||
|
|
@ -881,7 +902,11 @@ class FileDefItem:
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def from_paths(cls, paths, allow_sequences):
|
||||
def from_paths(
|
||||
cls,
|
||||
paths: List[str],
|
||||
allow_sequences: bool,
|
||||
) -> List["Self"]:
|
||||
filenames_by_dir = collections.defaultdict(list)
|
||||
for path in paths:
|
||||
normalized = os.path.normpath(path)
|
||||
|
|
@ -910,7 +935,7 @@ class FileDefItem:
|
|||
|
||||
return output
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self) -> "FileDefItemDict":
|
||||
output = {
|
||||
"is_sequence": self.is_sequence,
|
||||
"directory": self.directory,
|
||||
|
|
@ -948,8 +973,15 @@ class FileDef(AbstractAttrDef):
|
|||
]
|
||||
|
||||
def __init__(
|
||||
self, key, single_item=True, folders=None, extensions=None,
|
||||
allow_sequences=True, extensions_label=None, default=None, **kwargs
|
||||
self,
|
||||
key: str,
|
||||
single_item: Optional[bool] = True,
|
||||
folders: Optional[bool] = None,
|
||||
extensions: Optional[Iterable[str]] = None,
|
||||
allow_sequences: Optional[bool] = True,
|
||||
extensions_label: Optional[str] = None,
|
||||
default: Optional["Union[FileDefItemDict, List[str]]"] = None,
|
||||
**kwargs
|
||||
):
|
||||
if folders is None and extensions is None:
|
||||
folders = True
|
||||
|
|
@ -966,7 +998,9 @@ class FileDef(AbstractAttrDef):
|
|||
FileDefItem.from_dict(default)
|
||||
|
||||
elif isinstance(default, str):
|
||||
default = FileDefItem.from_paths([default.strip()])[0]
|
||||
default = FileDefItem.from_paths(
|
||||
[default.strip()], allow_sequences
|
||||
)[0]
|
||||
|
||||
else:
|
||||
raise TypeError((
|
||||
|
|
@ -985,14 +1019,14 @@ class FileDef(AbstractAttrDef):
|
|||
if is_label_horizontal is None:
|
||||
kwargs["is_label_horizontal"] = False
|
||||
|
||||
self.single_item = single_item
|
||||
self.folders = folders
|
||||
self.extensions = set(extensions)
|
||||
self.allow_sequences = allow_sequences
|
||||
self.extensions_label = extensions_label
|
||||
self.single_item: bool = single_item
|
||||
self.folders: bool = folders
|
||||
self.extensions: Set[str] = set(extensions)
|
||||
self.allow_sequences: bool = allow_sequences
|
||||
self.extensions_label: Optional[str] = extensions_label
|
||||
super().__init__(key, default=default, **kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not super().__eq__(other):
|
||||
return False
|
||||
|
||||
|
|
@ -1026,7 +1060,9 @@ class FileDef(AbstractAttrDef):
|
|||
return False
|
||||
return True
|
||||
|
||||
def convert_value(self, value):
|
||||
def convert_value(
|
||||
self, value: Any
|
||||
) -> "Union[FileDefItemDict, List[FileDefItemDict]]":
|
||||
if isinstance(value, (str, dict)):
|
||||
value = [value]
|
||||
|
||||
|
|
@ -1044,7 +1080,9 @@ class FileDef(AbstractAttrDef):
|
|||
pass
|
||||
|
||||
if string_paths:
|
||||
file_items = FileDefItem.from_paths(string_paths)
|
||||
file_items = FileDefItem.from_paths(
|
||||
string_paths, self.allow_sequences
|
||||
)
|
||||
dict_items.extend([
|
||||
file_item.to_dict()
|
||||
for file_item in file_items
|
||||
|
|
@ -1062,55 +1100,124 @@ class FileDef(AbstractAttrDef):
|
|||
return []
|
||||
|
||||
|
||||
def serialize_attr_def(attr_def):
|
||||
def register_attr_def_class(cls: AttrDefType):
|
||||
"""Register attribute definition.
|
||||
|
||||
Currently registered definitions are used to deserialize data to objects.
|
||||
|
||||
Attrs:
|
||||
cls (AttrDefType): 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: List[AttrDefType]
|
||||
) -> Set[str]:
|
||||
"""Collect keys from list of attribute definitions.
|
||||
|
||||
Args:
|
||||
attribute_definitions (List[AttrDefType]): Objects of attribute
|
||||
definitions.
|
||||
|
||||
Returns:
|
||||
Set[str]: Keys that will be created using passed attribute definitions.
|
||||
|
||||
"""
|
||||
keys = set()
|
||||
if not attribute_definitions:
|
||||
return keys
|
||||
|
||||
for attribute_def in attribute_definitions:
|
||||
if not isinstance(attribute_def, UIDef):
|
||||
keys.add(attribute_def.key)
|
||||
return keys
|
||||
|
||||
|
||||
def get_default_values(
|
||||
attribute_definitions: List[AttrDefType]
|
||||
) -> Dict[str, Any]:
|
||||
"""Receive default values for attribute definitions.
|
||||
|
||||
Args:
|
||||
attribute_definitions (List[AttrDefType]): Attribute definitions
|
||||
for which default values should be collected.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Default values for passed attribute definitions.
|
||||
|
||||
"""
|
||||
output = {}
|
||||
if not attribute_definitions:
|
||||
return output
|
||||
|
||||
for attr_def in attribute_definitions:
|
||||
# Skip UI definitions
|
||||
if not isinstance(attr_def, UIDef):
|
||||
output[attr_def.key] = attr_def.default
|
||||
return output
|
||||
|
||||
|
||||
def serialize_attr_def(attr_def: AttrDefType) -> Dict[str, Any]:
|
||||
"""Serialize attribute definition to data.
|
||||
|
||||
Args:
|
||||
attr_def (AbstractAttrDef): Attribute definition to serialize.
|
||||
attr_def (AttrDefType): Attribute definition to serialize.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Serialized data.
|
||||
"""
|
||||
|
||||
"""
|
||||
return attr_def.serialize()
|
||||
|
||||
|
||||
def serialize_attr_defs(attr_defs):
|
||||
def serialize_attr_defs(
|
||||
attr_defs: List[AttrDefType]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Serialize attribute definitions to data.
|
||||
|
||||
Args:
|
||||
attr_defs (List[AbstractAttrDef]): Attribute definitions to serialize.
|
||||
attr_defs (List[AttrDefType]): 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):
|
||||
def deserialize_attr_def(attr_def_data: Dict[str, Any]) -> AttrDefType:
|
||||
"""Deserialize attribute definition from data.
|
||||
|
||||
Args:
|
||||
attr_def_data (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):
|
||||
def deserialize_attr_defs(
|
||||
attr_defs_data: List[Dict[str, Any]]
|
||||
) -> List[AttrDefType]:
|
||||
"""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
|
||||
|
|
|
|||
|
|
@ -276,12 +276,7 @@ class ASettingRegistry(ABC):
|
|||
@abstractmethod
|
||||
def _delete_item(self, name):
|
||||
# type: (str) -> None
|
||||
"""Delete item from settings.
|
||||
|
||||
Note:
|
||||
see :meth:`ayon_core.lib.user_settings.ARegistrySettings.delete_item`
|
||||
|
||||
"""
|
||||
"""Delete item from settings."""
|
||||
pass
|
||||
|
||||
def __delitem__(self, name):
|
||||
|
|
@ -433,12 +428,7 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
config.write(cfg)
|
||||
|
||||
def _delete_item(self, name):
|
||||
"""Delete item from default section.
|
||||
|
||||
Note:
|
||||
See :meth:`~ayon_core.lib.IniSettingsRegistry.delete_item_from_section`
|
||||
|
||||
"""
|
||||
"""Delete item from default section."""
|
||||
self.delete_item_from_section("MAIN", name)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import os
|
||||
import re
|
||||
import copy
|
||||
import numbers
|
||||
import warnings
|
||||
from string import Formatter
|
||||
import typing
|
||||
from typing import List, Dict, Any, Set
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Union
|
||||
|
||||
KEY_PATTERN = re.compile(r"(\{.*?[^{0]*\})")
|
||||
KEY_PADDING_PATTERN = re.compile(r"([^:]+)\S+[><]\S+")
|
||||
SUB_DICT_PATTERN = re.compile(r"([^\[\]]+)")
|
||||
OPTIONAL_PATTERN = re.compile(r"(<.*?[^{0]*>)[^0-9]*?")
|
||||
|
||||
|
|
@ -18,9 +24,7 @@ class TemplateUnsolved(Exception):
|
|||
def __init__(self, template, missing_keys, invalid_types):
|
||||
invalid_type_items = []
|
||||
for _key, _type in invalid_types.items():
|
||||
invalid_type_items.append(
|
||||
"\"{0}\" {1}".format(_key, str(_type))
|
||||
)
|
||||
invalid_type_items.append(f"\"{_key}\" {str(_type)}")
|
||||
|
||||
invalid_types_msg = ""
|
||||
if invalid_type_items:
|
||||
|
|
@ -33,31 +37,32 @@ class TemplateUnsolved(Exception):
|
|||
missing_keys_msg = self.missing_keys_msg.format(
|
||||
", ".join(missing_keys)
|
||||
)
|
||||
super(TemplateUnsolved, self).__init__(
|
||||
super().__init__(
|
||||
self.msg.format(template, missing_keys_msg, invalid_types_msg)
|
||||
)
|
||||
|
||||
|
||||
class StringTemplate:
|
||||
"""String that can be formatted."""
|
||||
def __init__(self, template):
|
||||
def __init__(self, template: str):
|
||||
if not isinstance(template, str):
|
||||
raise TypeError("<{}> argument must be a string, not {}.".format(
|
||||
self.__class__.__name__, str(type(template))
|
||||
))
|
||||
raise TypeError(
|
||||
f"<{self.__class__.__name__}> argument must be a string,"
|
||||
f" not {str(type(template))}."
|
||||
)
|
||||
|
||||
self._template = template
|
||||
self._template: str = template
|
||||
parts = []
|
||||
last_end_idx = 0
|
||||
for item in KEY_PATTERN.finditer(template):
|
||||
start, end = item.span()
|
||||
if start > last_end_idx:
|
||||
parts.append(template[last_end_idx:start])
|
||||
parts.append(FormattingPart(template[start:end]))
|
||||
last_end_idx = end
|
||||
formatter = Formatter()
|
||||
|
||||
if last_end_idx < len(template):
|
||||
parts.append(template[last_end_idx:len(template)])
|
||||
for item in formatter.parse(template):
|
||||
literal_text, field_name, format_spec, conversion = item
|
||||
if literal_text:
|
||||
parts.append(literal_text)
|
||||
if field_name:
|
||||
parts.append(
|
||||
FormattingPart(field_name, format_spec, conversion)
|
||||
)
|
||||
|
||||
new_parts = []
|
||||
for part in parts:
|
||||
|
|
@ -77,15 +82,17 @@ class StringTemplate:
|
|||
if substr:
|
||||
new_parts.append(substr)
|
||||
|
||||
self._parts = self.find_optional_parts(new_parts)
|
||||
self._parts: List["Union[str, OptionalPart, FormattingPart]"] = (
|
||||
self.find_optional_parts(new_parts)
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.template
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}> {}".format(self.__class__.__name__, self.template)
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}> {self.template}"
|
||||
|
||||
def __contains__(self, other):
|
||||
def __contains__(self, other: str) -> bool:
|
||||
return other in self.template
|
||||
|
||||
def replace(self, *args, **kwargs):
|
||||
|
|
@ -93,10 +100,10 @@ class StringTemplate:
|
|||
return self
|
||||
|
||||
@property
|
||||
def template(self):
|
||||
def template(self) -> str:
|
||||
return self._template
|
||||
|
||||
def format(self, data):
|
||||
def format(self, data: Dict[str, Any]) -> "TemplateResult":
|
||||
""" Figure out with whole formatting.
|
||||
|
||||
Separate advanced keys (*Like '{project[name]}') from string which must
|
||||
|
|
@ -108,6 +115,7 @@ class StringTemplate:
|
|||
Returns:
|
||||
TemplateResult: Filled or partially filled template containing all
|
||||
data needed or missing for filling template.
|
||||
|
||||
"""
|
||||
result = TemplatePartResult()
|
||||
for part in self._parts:
|
||||
|
|
@ -135,23 +143,29 @@ class StringTemplate:
|
|||
invalid_types
|
||||
)
|
||||
|
||||
def format_strict(self, *args, **kwargs):
|
||||
result = self.format(*args, **kwargs)
|
||||
def format_strict(self, data: Dict[str, Any]) -> "TemplateResult":
|
||||
result = self.format(data)
|
||||
result.validate()
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def format_template(cls, template, data):
|
||||
def format_template(
|
||||
cls, template: str, data: Dict[str, Any]
|
||||
) -> "TemplateResult":
|
||||
objected_template = cls(template)
|
||||
return objected_template.format(data)
|
||||
|
||||
@classmethod
|
||||
def format_strict_template(cls, template, data):
|
||||
def format_strict_template(
|
||||
cls, template: str, data: Dict[str, Any]
|
||||
) -> "TemplateResult":
|
||||
objected_template = cls(template)
|
||||
return objected_template.format_strict(data)
|
||||
|
||||
@staticmethod
|
||||
def find_optional_parts(parts):
|
||||
def find_optional_parts(
|
||||
parts: List["Union[str, FormattingPart]"]
|
||||
) -> List["Union[str, OptionalPart, FormattingPart]"]:
|
||||
new_parts = []
|
||||
tmp_parts = {}
|
||||
counted_symb = -1
|
||||
|
|
@ -216,11 +230,11 @@ class TemplateResult(str):
|
|||
of number.
|
||||
"""
|
||||
|
||||
used_values = None
|
||||
solved = None
|
||||
template = None
|
||||
missing_keys = None
|
||||
invalid_types = None
|
||||
used_values: Dict[str, Any] = None
|
||||
solved: bool = None
|
||||
template: str = None
|
||||
missing_keys: List[str] = None
|
||||
invalid_types: Dict[str, Any] = None
|
||||
|
||||
def __new__(
|
||||
cls, filled_template, template, solved,
|
||||
|
|
@ -248,7 +262,7 @@ class TemplateResult(str):
|
|||
self.invalid_types
|
||||
)
|
||||
|
||||
def copy(self):
|
||||
def copy(self) -> "TemplateResult":
|
||||
cls = self.__class__
|
||||
return cls(
|
||||
str(self),
|
||||
|
|
@ -259,7 +273,7 @@ class TemplateResult(str):
|
|||
self.invalid_types
|
||||
)
|
||||
|
||||
def normalized(self):
|
||||
def normalized(self) -> "TemplateResult":
|
||||
"""Convert to normalized path."""
|
||||
|
||||
cls = self.__class__
|
||||
|
|
@ -275,27 +289,28 @@ class TemplateResult(str):
|
|||
|
||||
class TemplatePartResult:
|
||||
"""Result to store result of template parts."""
|
||||
def __init__(self, optional=False):
|
||||
def __init__(self, optional: bool = False):
|
||||
# Missing keys or invalid value types of required keys
|
||||
self._missing_keys = set()
|
||||
self._invalid_types = {}
|
||||
self._missing_keys: Set[str] = set()
|
||||
self._invalid_types: Dict[str, Any] = {}
|
||||
# Missing keys or invalid value types of optional keys
|
||||
self._missing_optional_keys = set()
|
||||
self._invalid_optional_types = {}
|
||||
self._missing_optional_keys: Set[str] = set()
|
||||
self._invalid_optional_types: Dict[str, Any] = {}
|
||||
|
||||
# Used values stored by key with origin type
|
||||
# - key without any padding or key modifiers
|
||||
# - value from filling data
|
||||
# Example: {"version": 1}
|
||||
self._used_values = {}
|
||||
self._used_values: Dict[str, Any] = {}
|
||||
# Used values stored by key with all modifirs
|
||||
# - value is already formatted string
|
||||
# Example: {"version:0>3": "001"}
|
||||
self._realy_used_values = {}
|
||||
self._really_used_values: Dict[str, Any] = {}
|
||||
# Concatenated string output after formatting
|
||||
self._output = ""
|
||||
self._output: str = ""
|
||||
# Is this result from optional part
|
||||
self._optional = True
|
||||
# TODO find out why we don't use 'optional' from args
|
||||
self._optional: bool = True
|
||||
|
||||
def add_output(self, other):
|
||||
if isinstance(other, str):
|
||||
|
|
@ -313,7 +328,7 @@ class TemplatePartResult:
|
|||
if other.optional and not other.solved:
|
||||
return
|
||||
self._used_values.update(other.used_values)
|
||||
self._realy_used_values.update(other.realy_used_values)
|
||||
self._really_used_values.update(other.really_used_values)
|
||||
|
||||
else:
|
||||
raise TypeError("Cannot add data from \"{}\" to \"{}\"".format(
|
||||
|
|
@ -321,7 +336,7 @@ class TemplatePartResult:
|
|||
)
|
||||
|
||||
@property
|
||||
def solved(self):
|
||||
def solved(self) -> bool:
|
||||
if self.optional:
|
||||
if (
|
||||
len(self.missing_optional_keys) > 0
|
||||
|
|
@ -334,45 +349,53 @@ class TemplatePartResult:
|
|||
)
|
||||
|
||||
@property
|
||||
def optional(self):
|
||||
def optional(self) -> bool:
|
||||
return self._optional
|
||||
|
||||
@property
|
||||
def output(self):
|
||||
def output(self) -> str:
|
||||
return self._output
|
||||
|
||||
@property
|
||||
def missing_keys(self):
|
||||
def missing_keys(self) -> Set[str]:
|
||||
return self._missing_keys
|
||||
|
||||
@property
|
||||
def missing_optional_keys(self):
|
||||
def missing_optional_keys(self) -> Set[str]:
|
||||
return self._missing_optional_keys
|
||||
|
||||
@property
|
||||
def invalid_types(self):
|
||||
def invalid_types(self) -> Dict[str, Any]:
|
||||
return self._invalid_types
|
||||
|
||||
@property
|
||||
def invalid_optional_types(self):
|
||||
def invalid_optional_types(self) -> Dict[str, Any]:
|
||||
return self._invalid_optional_types
|
||||
|
||||
@property
|
||||
def realy_used_values(self):
|
||||
return self._realy_used_values
|
||||
def really_used_values(self) -> Dict[str, Any]:
|
||||
return self._really_used_values
|
||||
|
||||
@property
|
||||
def used_values(self):
|
||||
def realy_used_values(self) -> Dict[str, Any]:
|
||||
warnings.warn(
|
||||
"Property 'realy_used_values' is deprecated."
|
||||
" Use 'really_used_values' instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
return self._really_used_values
|
||||
|
||||
@property
|
||||
def used_values(self) -> Dict[str, Any]:
|
||||
return self._used_values
|
||||
|
||||
@staticmethod
|
||||
def split_keys_to_subdicts(values):
|
||||
def split_keys_to_subdicts(values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
output = {}
|
||||
formatter = Formatter()
|
||||
for key, value in values.items():
|
||||
key_padding = list(KEY_PADDING_PATTERN.findall(key))
|
||||
if key_padding:
|
||||
key = key_padding[0]
|
||||
key_subdict = list(SUB_DICT_PATTERN.findall(key))
|
||||
_, field_name, _, _ = next(formatter.parse(f"{{{key}}}"))
|
||||
key_subdict = list(SUB_DICT_PATTERN.findall(field_name))
|
||||
data = output
|
||||
last_key = key_subdict.pop(-1)
|
||||
for subkey in key_subdict:
|
||||
|
|
@ -382,7 +405,7 @@ class TemplatePartResult:
|
|||
data[last_key] = value
|
||||
return output
|
||||
|
||||
def get_clean_used_values(self):
|
||||
def get_clean_used_values(self) -> Dict[str, Any]:
|
||||
new_used_values = {}
|
||||
for key, value in self.used_values.items():
|
||||
if isinstance(value, FormatObject):
|
||||
|
|
@ -391,19 +414,27 @@ class TemplatePartResult:
|
|||
|
||||
return self.split_keys_to_subdicts(new_used_values)
|
||||
|
||||
def add_realy_used_value(self, key, value):
|
||||
self._realy_used_values[key] = value
|
||||
def add_really_used_value(self, key: str, value: Any):
|
||||
self._really_used_values[key] = value
|
||||
|
||||
def add_used_value(self, key, value):
|
||||
def add_realy_used_value(self, key: str, value: Any):
|
||||
warnings.warn(
|
||||
"Method 'add_realy_used_value' is deprecated."
|
||||
" Use 'add_really_used_value' instead.",
|
||||
DeprecationWarning
|
||||
)
|
||||
self.add_really_used_value(key, value)
|
||||
|
||||
def add_used_value(self, key: str, value: Any):
|
||||
self._used_values[key] = value
|
||||
|
||||
def add_missing_key(self, key):
|
||||
def add_missing_key(self, key: str):
|
||||
if self._optional:
|
||||
self._missing_optional_keys.add(key)
|
||||
else:
|
||||
self._missing_keys.add(key)
|
||||
|
||||
def add_invalid_type(self, key, value):
|
||||
def add_invalid_type(self, key: str, value: Any):
|
||||
if self._optional:
|
||||
self._invalid_optional_types[key] = type(value)
|
||||
else:
|
||||
|
|
@ -421,10 +452,10 @@ class FormatObject:
|
|||
def __format__(self, *args, **kwargs):
|
||||
return self.value.__format__(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
|
||||
|
|
@ -434,23 +465,44 @@ class FormattingPart:
|
|||
Containt only single key to format e.g. "{project[name]}".
|
||||
|
||||
Args:
|
||||
template(str): String containing the formatting key.
|
||||
field_name (str): Name of key.
|
||||
format_spec (str): Format specification.
|
||||
conversion (Union[str, None]): Conversion type.
|
||||
|
||||
"""
|
||||
def __init__(self, template):
|
||||
self._template = template
|
||||
def __init__(
|
||||
self,
|
||||
field_name: str,
|
||||
format_spec: str,
|
||||
conversion: "Union[str, None]",
|
||||
):
|
||||
format_spec_v = ""
|
||||
if format_spec:
|
||||
format_spec_v = f":{format_spec}"
|
||||
conversion_v = ""
|
||||
if conversion:
|
||||
conversion_v = f"!{conversion}"
|
||||
|
||||
self._field_name: str = field_name
|
||||
self._format_spec: str = format_spec_v
|
||||
self._conversion: str = conversion_v
|
||||
|
||||
template_base = f"{field_name}{format_spec_v}{conversion_v}"
|
||||
self._template_base: str = template_base
|
||||
self._template: str = f"{{{template_base}}}"
|
||||
|
||||
@property
|
||||
def template(self):
|
||||
def template(self) -> str:
|
||||
return self._template
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "<Format:{}>".format(self._template)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self._template
|
||||
|
||||
@staticmethod
|
||||
def validate_value_type(value):
|
||||
def validate_value_type(value: Any) -> bool:
|
||||
"""Check if value can be used for formatting of single key."""
|
||||
if isinstance(value, (numbers.Number, FormatObject)):
|
||||
return True
|
||||
|
|
@ -461,7 +513,7 @@ class FormattingPart:
|
|||
return False
|
||||
|
||||
@staticmethod
|
||||
def validate_key_is_matched(key):
|
||||
def validate_key_is_matched(key: str) -> bool:
|
||||
"""Validate that opening has closing at correct place.
|
||||
Future-proof, only square brackets are currently used in keys.
|
||||
|
||||
|
|
@ -488,16 +540,29 @@ class FormattingPart:
|
|||
return False
|
||||
return not queue
|
||||
|
||||
def format(self, data, result):
|
||||
@staticmethod
|
||||
def keys_to_template_base(keys: List[str]):
|
||||
if not keys:
|
||||
return None
|
||||
# Create copy of keys
|
||||
keys = list(keys)
|
||||
template_base = keys.pop(0)
|
||||
joined_keys = "".join([f"[{key}]" for key in keys])
|
||||
return f"{template_base}{joined_keys}"
|
||||
|
||||
def format(
|
||||
self, data: Dict[str, Any], result: TemplatePartResult
|
||||
) -> TemplatePartResult:
|
||||
"""Format the formattings string.
|
||||
|
||||
Args:
|
||||
data(dict): Data that should be used for formatting.
|
||||
result(TemplatePartResult): Object where result is stored.
|
||||
|
||||
"""
|
||||
key = self.template[1:-1]
|
||||
if key in result.realy_used_values:
|
||||
result.add_output(result.realy_used_values[key])
|
||||
key = self._template_base
|
||||
if key in result.really_used_values:
|
||||
result.add_output(result.really_used_values[key])
|
||||
return result
|
||||
|
||||
# ensure key is properly formed [({})] properly closed.
|
||||
|
|
@ -507,17 +572,38 @@ class FormattingPart:
|
|||
return result
|
||||
|
||||
# check if key expects subdictionary keys (e.g. project[name])
|
||||
existence_check = key
|
||||
key_padding = list(KEY_PADDING_PATTERN.findall(existence_check))
|
||||
if key_padding:
|
||||
existence_check = key_padding[0]
|
||||
key_subdict = list(SUB_DICT_PATTERN.findall(existence_check))
|
||||
key_subdict = list(SUB_DICT_PATTERN.findall(self._field_name))
|
||||
|
||||
value = data
|
||||
missing_key = False
|
||||
invalid_type = False
|
||||
used_keys = []
|
||||
keys_to_value = None
|
||||
used_value = None
|
||||
|
||||
for sub_key in key_subdict:
|
||||
if isinstance(value, list):
|
||||
if not sub_key.lstrip("-").isdigit():
|
||||
invalid_type = True
|
||||
break
|
||||
sub_key = int(sub_key)
|
||||
if sub_key < 0:
|
||||
sub_key = len(value) + sub_key
|
||||
|
||||
invalid = 0 > sub_key < len(data)
|
||||
if invalid:
|
||||
used_keys.append(sub_key)
|
||||
missing_key = True
|
||||
break
|
||||
|
||||
used_keys.append(sub_key)
|
||||
if keys_to_value is None:
|
||||
keys_to_value = list(used_keys)
|
||||
keys_to_value.pop(-1)
|
||||
used_value = copy.deepcopy(value)
|
||||
value = value[sub_key]
|
||||
continue
|
||||
|
||||
if (
|
||||
value is None
|
||||
or (hasattr(value, "items") and sub_key not in value)
|
||||
|
|
@ -533,45 +619,57 @@ class FormattingPart:
|
|||
used_keys.append(sub_key)
|
||||
value = value.get(sub_key)
|
||||
|
||||
if missing_key or invalid_type:
|
||||
if len(used_keys) == 0:
|
||||
invalid_key = key_subdict[0]
|
||||
else:
|
||||
invalid_key = used_keys[0]
|
||||
for idx, sub_key in enumerate(used_keys):
|
||||
if idx == 0:
|
||||
continue
|
||||
invalid_key += "[{0}]".format(sub_key)
|
||||
field_name = key_subdict[0]
|
||||
if used_keys:
|
||||
field_name = self.keys_to_template_base(used_keys)
|
||||
|
||||
if missing_key or invalid_type:
|
||||
if missing_key:
|
||||
result.add_missing_key(invalid_key)
|
||||
result.add_missing_key(field_name)
|
||||
|
||||
elif invalid_type:
|
||||
result.add_invalid_type(invalid_key, value)
|
||||
result.add_invalid_type(field_name, value)
|
||||
|
||||
result.add_output(self.template)
|
||||
return result
|
||||
|
||||
if self.validate_value_type(value):
|
||||
fill_data = {}
|
||||
first_value = True
|
||||
for used_key in reversed(used_keys):
|
||||
if first_value:
|
||||
first_value = False
|
||||
fill_data[used_key] = value
|
||||
else:
|
||||
_fill_data = {used_key: fill_data}
|
||||
fill_data = _fill_data
|
||||
|
||||
formatted_value = self.template.format(**fill_data)
|
||||
result.add_realy_used_value(key, formatted_value)
|
||||
result.add_used_value(existence_check, formatted_value)
|
||||
result.add_output(formatted_value)
|
||||
if not self.validate_value_type(value):
|
||||
result.add_invalid_type(key, value)
|
||||
result.add_output(self.template)
|
||||
return result
|
||||
|
||||
result.add_invalid_type(key, value)
|
||||
result.add_output(self.template)
|
||||
fill_data = root_fill_data = {}
|
||||
parent_fill_data = None
|
||||
parent_key = None
|
||||
fill_value = data
|
||||
value_filled = False
|
||||
for used_key in used_keys:
|
||||
if isinstance(fill_value, list):
|
||||
parent_fill_data[parent_key] = fill_value
|
||||
value_filled = True
|
||||
break
|
||||
fill_value = fill_value[used_key]
|
||||
parent_fill_data = fill_data
|
||||
fill_data = parent_fill_data.setdefault(used_key, {})
|
||||
parent_key = used_key
|
||||
|
||||
if not value_filled:
|
||||
parent_fill_data[used_keys[-1]] = value
|
||||
|
||||
template = f"{{{field_name}{self._format_spec}{self._conversion}}}"
|
||||
formatted_value = template.format(**root_fill_data)
|
||||
used_key = key
|
||||
if keys_to_value is not None:
|
||||
used_key = self.keys_to_template_base(keys_to_value)
|
||||
|
||||
if used_value is None:
|
||||
if isinstance(value, numbers.Number):
|
||||
used_value = value
|
||||
else:
|
||||
used_value = formatted_value
|
||||
result.add_really_used_value(self._field_name, used_value)
|
||||
result.add_used_value(used_key, used_value)
|
||||
result.add_output(formatted_value)
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -585,20 +683,27 @@ class OptionalPart:
|
|||
'FormattingPart'.
|
||||
"""
|
||||
|
||||
def __init__(self, parts):
|
||||
self._parts = parts
|
||||
def __init__(
|
||||
self,
|
||||
parts: List["Union[str, OptionalPart, FormattingPart]"]
|
||||
):
|
||||
self._parts: List["Union[str, OptionalPart, FormattingPart]"] = parts
|
||||
|
||||
@property
|
||||
def parts(self):
|
||||
def parts(self) -> List["Union[str, OptionalPart, FormattingPart]"]:
|
||||
return self._parts
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return "<{}>".format("".join([str(p) for p in self._parts]))
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "<Optional:{}>".format("".join([str(p) for p in self._parts]))
|
||||
|
||||
def format(self, data, result):
|
||||
def format(
|
||||
self,
|
||||
data: Dict[str, Any],
|
||||
result: TemplatePartResult,
|
||||
) -> TemplatePartResult:
|
||||
new_result = TemplatePartResult(True)
|
||||
for part in self._parts:
|
||||
if isinstance(part, str):
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
import os
|
||||
|
||||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.addon import AYONAddon, ITrayAction
|
||||
|
||||
|
||||
class LauncherAction(AYONAddon, ITrayAction):
|
||||
label = "Launcher"
|
||||
name = "launcher_tool"
|
||||
version = "1.0.0"
|
||||
|
||||
def initialize(self, settings):
|
||||
|
||||
# Tray attributes
|
||||
self._window = None
|
||||
|
||||
def tray_init(self):
|
||||
self._create_window()
|
||||
|
||||
self.add_doubleclick_callback(self._show_launcher)
|
||||
|
||||
def tray_start(self):
|
||||
return
|
||||
|
||||
def connect_with_addons(self, enabled_modules):
|
||||
# Register actions
|
||||
if not self.tray_initialized:
|
||||
return
|
||||
|
||||
from ayon_core.pipeline.actions import register_launcher_action_path
|
||||
|
||||
actions_dir = os.path.join(AYON_CORE_ROOT, "plugins", "actions")
|
||||
if os.path.exists(actions_dir):
|
||||
register_launcher_action_path(actions_dir)
|
||||
|
||||
actions_paths = self.manager.collect_plugin_paths()["actions"]
|
||||
for path in actions_paths:
|
||||
if path and os.path.exists(path):
|
||||
register_launcher_action_path(path)
|
||||
|
||||
def on_action_trigger(self):
|
||||
"""Implementation for ITrayAction interface.
|
||||
|
||||
Show launcher tool on action trigger.
|
||||
"""
|
||||
|
||||
self._show_launcher()
|
||||
|
||||
def _create_window(self):
|
||||
if self._window:
|
||||
return
|
||||
from ayon_core.tools.launcher.ui import LauncherWindow
|
||||
self._window = LauncherWindow()
|
||||
|
||||
def _show_launcher(self):
|
||||
if self._window is None:
|
||||
return
|
||||
self._window.show()
|
||||
self._window.raise_()
|
||||
self._window.activateWindow()
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
from ayon_core.addon import AYONAddon, ITrayAddon
|
||||
|
||||
|
||||
class LoaderAddon(AYONAddon, ITrayAddon):
|
||||
name = "loader_tool"
|
||||
version = "1.0.0"
|
||||
|
||||
def initialize(self, settings):
|
||||
# Tray attributes
|
||||
self._loader_imported = None
|
||||
self._loader_window = None
|
||||
|
||||
def tray_init(self):
|
||||
# Add library tool
|
||||
self._loader_imported = False
|
||||
try:
|
||||
from ayon_core.tools.loader.ui import LoaderWindow # noqa F401
|
||||
|
||||
self._loader_imported = True
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Couldn't load Loader tool for tray.",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
# Definition of Tray menu
|
||||
def tray_menu(self, tray_menu):
|
||||
if not self._loader_imported:
|
||||
return
|
||||
|
||||
from qtpy import QtWidgets
|
||||
# Actions
|
||||
action_loader = QtWidgets.QAction(
|
||||
"Loader", tray_menu
|
||||
)
|
||||
|
||||
action_loader.triggered.connect(self.show_loader)
|
||||
|
||||
tray_menu.addAction(action_loader)
|
||||
|
||||
def tray_start(self, *_a, **_kw):
|
||||
return
|
||||
|
||||
def tray_exit(self, *_a, **_kw):
|
||||
return
|
||||
|
||||
def show_loader(self):
|
||||
if self._loader_window is None:
|
||||
from ayon_core.pipeline import install_ayon_plugins
|
||||
|
||||
self._init_loader()
|
||||
|
||||
install_ayon_plugins()
|
||||
|
||||
self._loader_window.show()
|
||||
|
||||
# Raise and activate the window
|
||||
# for MacOS
|
||||
self._loader_window.raise_()
|
||||
# for Windows
|
||||
self._loader_window.activateWindow()
|
||||
|
||||
def _init_loader(self):
|
||||
from ayon_core.tools.loader.ui import LoaderWindow
|
||||
|
||||
libraryloader = LoaderWindow()
|
||||
|
||||
self._loader_window = libraryloader
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from .addon import (
|
||||
PythonInterpreterAction
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PythonInterpreterAction",
|
||||
)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
from ayon_core.addon import AYONAddon, ITrayAction
|
||||
|
||||
|
||||
class PythonInterpreterAction(AYONAddon, ITrayAction):
|
||||
label = "Console"
|
||||
name = "python_interpreter"
|
||||
version = "1.0.0"
|
||||
admin_action = True
|
||||
|
||||
def initialize(self, settings):
|
||||
self._interpreter_window = None
|
||||
|
||||
def tray_init(self):
|
||||
self.create_interpreter_window()
|
||||
|
||||
def tray_exit(self):
|
||||
if self._interpreter_window is not None:
|
||||
self._interpreter_window.save_registry()
|
||||
|
||||
def create_interpreter_window(self):
|
||||
"""Initializa Settings Qt window."""
|
||||
if self._interpreter_window:
|
||||
return
|
||||
|
||||
from ayon_core.modules.python_console_interpreter.window import (
|
||||
PythonInterpreterWidget
|
||||
)
|
||||
|
||||
self._interpreter_window = PythonInterpreterWidget()
|
||||
|
||||
def on_action_trigger(self):
|
||||
self.show_interpreter_window()
|
||||
|
||||
def show_interpreter_window(self):
|
||||
self.create_interpreter_window()
|
||||
|
||||
if self._interpreter_window.isVisible():
|
||||
self._interpreter_window.activateWindow()
|
||||
self._interpreter_window.raise_()
|
||||
return
|
||||
|
||||
self._interpreter_window.show()
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from .widgets import (
|
||||
PythonInterpreterWidget
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PythonInterpreterWidget",
|
||||
)
|
||||
|
|
@ -1,660 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
import collections
|
||||
from code import InteractiveInterpreter
|
||||
|
||||
import appdirs
|
||||
from qtpy import QtCore, QtWidgets, QtGui
|
||||
|
||||
from ayon_core import resources
|
||||
from ayon_core.style import load_stylesheet
|
||||
from ayon_core.lib import JSONSettingRegistry
|
||||
|
||||
|
||||
ayon_art = r"""
|
||||
|
||||
▄██▄
|
||||
▄███▄ ▀██▄ ▀██▀ ▄██▀ ▄██▀▀▀██▄ ▀███▄ █▄
|
||||
▄▄ ▀██▄ ▀██▄ ▄██▀ ██▀ ▀██▄ ▄ ▀██▄ ███
|
||||
▄██▀ ██▄ ▀ ▄▄ ▀ ██ ▄██ ███ ▀██▄ ███
|
||||
▄██▀ ▀██▄ ██ ▀██▄ ▄██▀ ███ ▀██ ▀█▀
|
||||
▄██▀ ▀██▄ ▀█ ▀██▄▄▄▄██▀ █▀ ▀██▄
|
||||
|
||||
· · - =[ by YNPUT ]:[ http://ayon.ynput.io ]= - · ·
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class PythonInterpreterRegistry(JSONSettingRegistry):
|
||||
"""Class handling OpenPype general settings registry.
|
||||
|
||||
Attributes:
|
||||
vendor (str): Name used for path construction.
|
||||
product (str): Additional name used for path construction.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.vendor = "Ynput"
|
||||
self.product = "AYON"
|
||||
name = "python_interpreter_tool"
|
||||
path = appdirs.user_data_dir(self.product, self.vendor)
|
||||
super(PythonInterpreterRegistry, self).__init__(name, path)
|
||||
|
||||
|
||||
class StdOEWrap:
|
||||
def __init__(self):
|
||||
self._origin_stdout_write = None
|
||||
self._origin_stderr_write = None
|
||||
self._listening = False
|
||||
self.lines = collections.deque()
|
||||
|
||||
if not sys.stdout:
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
if not sys.stderr:
|
||||
sys.stderr = open(os.devnull, "w")
|
||||
|
||||
if self._origin_stdout_write is None:
|
||||
self._origin_stdout_write = sys.stdout.write
|
||||
|
||||
if self._origin_stderr_write is None:
|
||||
self._origin_stderr_write = sys.stderr.write
|
||||
|
||||
self._listening = True
|
||||
sys.stdout.write = self._stdout_listener
|
||||
sys.stderr.write = self._stderr_listener
|
||||
|
||||
def stop_listen(self):
|
||||
self._listening = False
|
||||
|
||||
def _stdout_listener(self, text):
|
||||
if self._listening:
|
||||
self.lines.append(text)
|
||||
if self._origin_stdout_write is not None:
|
||||
self._origin_stdout_write(text)
|
||||
|
||||
def _stderr_listener(self, text):
|
||||
if self._listening:
|
||||
self.lines.append(text)
|
||||
if self._origin_stderr_write is not None:
|
||||
self._origin_stderr_write(text)
|
||||
|
||||
|
||||
class PythonCodeEditor(QtWidgets.QPlainTextEdit):
|
||||
execute_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(PythonCodeEditor, self).__init__(parent)
|
||||
|
||||
self.setObjectName("PythonCodeEditor")
|
||||
|
||||
self._indent = 4
|
||||
|
||||
def _tab_shift_right(self):
|
||||
cursor = self.textCursor()
|
||||
selected_text = cursor.selectedText()
|
||||
if not selected_text:
|
||||
cursor.insertText(" " * self._indent)
|
||||
return
|
||||
|
||||
sel_start = cursor.selectionStart()
|
||||
sel_end = cursor.selectionEnd()
|
||||
cursor.setPosition(sel_end)
|
||||
end_line = cursor.blockNumber()
|
||||
cursor.setPosition(sel_start)
|
||||
while True:
|
||||
cursor.movePosition(QtGui.QTextCursor.StartOfLine)
|
||||
text = cursor.block().text()
|
||||
spaces = len(text) - len(text.lstrip(" "))
|
||||
new_spaces = spaces % self._indent
|
||||
if not new_spaces:
|
||||
new_spaces = self._indent
|
||||
|
||||
cursor.insertText(" " * new_spaces)
|
||||
if cursor.blockNumber() == end_line:
|
||||
break
|
||||
|
||||
cursor.movePosition(QtGui.QTextCursor.NextBlock)
|
||||
|
||||
def _tab_shift_left(self):
|
||||
tmp_cursor = self.textCursor()
|
||||
sel_start = tmp_cursor.selectionStart()
|
||||
sel_end = tmp_cursor.selectionEnd()
|
||||
|
||||
cursor = QtGui.QTextCursor(self.document())
|
||||
cursor.setPosition(sel_end)
|
||||
end_line = cursor.blockNumber()
|
||||
cursor.setPosition(sel_start)
|
||||
while True:
|
||||
cursor.movePosition(QtGui.QTextCursor.StartOfLine)
|
||||
text = cursor.block().text()
|
||||
spaces = len(text) - len(text.lstrip(" "))
|
||||
if spaces:
|
||||
spaces_to_remove = (spaces % self._indent) or self._indent
|
||||
if spaces_to_remove > spaces:
|
||||
spaces_to_remove = spaces
|
||||
|
||||
cursor.setPosition(
|
||||
cursor.position() + spaces_to_remove,
|
||||
QtGui.QTextCursor.KeepAnchor
|
||||
)
|
||||
cursor.removeSelectedText()
|
||||
|
||||
if cursor.blockNumber() == end_line:
|
||||
break
|
||||
|
||||
cursor.movePosition(QtGui.QTextCursor.NextBlock)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == QtCore.Qt.Key_Backtab:
|
||||
self._tab_shift_left()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if event.key() == QtCore.Qt.Key_Tab:
|
||||
if event.modifiers() == QtCore.Qt.NoModifier:
|
||||
self._tab_shift_right()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if (
|
||||
event.key() == QtCore.Qt.Key_Return
|
||||
and event.modifiers() == QtCore.Qt.ControlModifier
|
||||
):
|
||||
self.execute_requested.emit()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
super(PythonCodeEditor, self).keyPressEvent(event)
|
||||
|
||||
|
||||
class PythonTabWidget(QtWidgets.QWidget):
|
||||
add_tab_requested = QtCore.Signal()
|
||||
before_execute = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, parent):
|
||||
super(PythonTabWidget, self).__init__(parent)
|
||||
|
||||
code_input = PythonCodeEditor(self)
|
||||
|
||||
self.setFocusProxy(code_input)
|
||||
|
||||
add_tab_btn = QtWidgets.QPushButton("Add tab...", self)
|
||||
add_tab_btn.setToolTip("Add new tab")
|
||||
|
||||
execute_btn = QtWidgets.QPushButton("Execute", self)
|
||||
execute_btn.setToolTip("Execute command (Ctrl + Enter)")
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btns_layout.addWidget(add_tab_btn)
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(execute_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(code_input, 1)
|
||||
layout.addLayout(btns_layout, 0)
|
||||
|
||||
add_tab_btn.clicked.connect(self._on_add_tab_clicked)
|
||||
execute_btn.clicked.connect(self._on_execute_clicked)
|
||||
code_input.execute_requested.connect(self.execute)
|
||||
|
||||
self._code_input = code_input
|
||||
self._interpreter = InteractiveInterpreter()
|
||||
|
||||
def _on_add_tab_clicked(self):
|
||||
self.add_tab_requested.emit()
|
||||
|
||||
def _on_execute_clicked(self):
|
||||
self.execute()
|
||||
|
||||
def get_code(self):
|
||||
return self._code_input.toPlainText()
|
||||
|
||||
def set_code(self, code_text):
|
||||
self._code_input.setPlainText(code_text)
|
||||
|
||||
def execute(self):
|
||||
code_text = self._code_input.toPlainText()
|
||||
self.before_execute.emit(code_text)
|
||||
self._interpreter.runcode(code_text)
|
||||
|
||||
|
||||
class TabNameDialog(QtWidgets.QDialog):
|
||||
default_width = 330
|
||||
default_height = 85
|
||||
|
||||
def __init__(self, parent):
|
||||
super(TabNameDialog, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("Enter tab name")
|
||||
|
||||
name_label = QtWidgets.QLabel("Tab name:", self)
|
||||
name_input = QtWidgets.QLineEdit(self)
|
||||
|
||||
inputs_layout = QtWidgets.QHBoxLayout()
|
||||
inputs_layout.addWidget(name_label)
|
||||
inputs_layout.addWidget(name_input)
|
||||
|
||||
ok_btn = QtWidgets.QPushButton("Ok", self)
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel", self)
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(ok_btn)
|
||||
btns_layout.addWidget(cancel_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addLayout(inputs_layout)
|
||||
layout.addStretch(1)
|
||||
layout.addLayout(btns_layout)
|
||||
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
cancel_btn.clicked.connect(self._on_cancel_clicked)
|
||||
|
||||
self._name_input = name_input
|
||||
self._ok_btn = ok_btn
|
||||
self._cancel_btn = cancel_btn
|
||||
|
||||
self._result = None
|
||||
|
||||
self.resize(self.default_width, self.default_height)
|
||||
|
||||
def set_tab_name(self, name):
|
||||
self._name_input.setText(name)
|
||||
|
||||
def result(self):
|
||||
return self._result
|
||||
|
||||
def showEvent(self, event):
|
||||
super(TabNameDialog, self).showEvent(event)
|
||||
btns_width = max(
|
||||
self._ok_btn.width(),
|
||||
self._cancel_btn.width()
|
||||
)
|
||||
|
||||
self._ok_btn.setMinimumWidth(btns_width)
|
||||
self._cancel_btn.setMinimumWidth(btns_width)
|
||||
|
||||
def _on_ok_clicked(self):
|
||||
self._result = self._name_input.text()
|
||||
self.accept()
|
||||
|
||||
def _on_cancel_clicked(self):
|
||||
self._result = None
|
||||
self.reject()
|
||||
|
||||
|
||||
class OutputTextWidget(QtWidgets.QTextEdit):
|
||||
v_max_offset = 4
|
||||
|
||||
def vertical_scroll_at_max(self):
|
||||
v_scroll = self.verticalScrollBar()
|
||||
return v_scroll.value() > v_scroll.maximum() - self.v_max_offset
|
||||
|
||||
def scroll_to_bottom(self):
|
||||
v_scroll = self.verticalScrollBar()
|
||||
return v_scroll.setValue(v_scroll.maximum())
|
||||
|
||||
|
||||
class EnhancedTabBar(QtWidgets.QTabBar):
|
||||
double_clicked = QtCore.Signal(QtCore.QPoint)
|
||||
right_clicked = QtCore.Signal(QtCore.QPoint)
|
||||
mid_clicked = QtCore.Signal(QtCore.QPoint)
|
||||
|
||||
def __init__(self, parent):
|
||||
super(EnhancedTabBar, self).__init__(parent)
|
||||
|
||||
self.setDrawBase(False)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
self.double_clicked.emit(event.globalPos())
|
||||
event.accept()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == QtCore.Qt.RightButton:
|
||||
self.right_clicked.emit(event.globalPos())
|
||||
event.accept()
|
||||
return
|
||||
|
||||
elif event.button() == QtCore.Qt.MidButton:
|
||||
self.mid_clicked.emit(event.globalPos())
|
||||
event.accept()
|
||||
|
||||
else:
|
||||
super(EnhancedTabBar, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class PythonInterpreterWidget(QtWidgets.QWidget):
|
||||
default_width = 1000
|
||||
default_height = 600
|
||||
|
||||
def __init__(self, allow_save_registry=True, parent=None):
|
||||
super(PythonInterpreterWidget, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("AYON Console")
|
||||
self.setWindowIcon(QtGui.QIcon(resources.get_ayon_icon_filepath()))
|
||||
|
||||
self.ansi_escape = re.compile(
|
||||
r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]"
|
||||
)
|
||||
|
||||
self._tabs = []
|
||||
|
||||
self._stdout_err_wrapper = StdOEWrap()
|
||||
|
||||
output_widget = OutputTextWidget(self)
|
||||
output_widget.setObjectName("PythonInterpreterOutput")
|
||||
output_widget.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
|
||||
output_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||
|
||||
tab_widget = QtWidgets.QTabWidget(self)
|
||||
tab_bar = EnhancedTabBar(tab_widget)
|
||||
tab_widget.setTabBar(tab_bar)
|
||||
tab_widget.setTabsClosable(False)
|
||||
tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
widgets_splitter = QtWidgets.QSplitter(self)
|
||||
widgets_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
widgets_splitter.addWidget(output_widget)
|
||||
widgets_splitter.addWidget(tab_widget)
|
||||
widgets_splitter.setStretchFactor(0, 1)
|
||||
widgets_splitter.setStretchFactor(1, 1)
|
||||
height = int(self.default_height / 2)
|
||||
widgets_splitter.setSizes([height, self.default_height - height])
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(widgets_splitter)
|
||||
|
||||
line_check_timer = QtCore.QTimer()
|
||||
line_check_timer.setInterval(200)
|
||||
|
||||
line_check_timer.timeout.connect(self._on_timer_timeout)
|
||||
tab_bar.right_clicked.connect(self._on_tab_right_click)
|
||||
tab_bar.double_clicked.connect(self._on_tab_double_click)
|
||||
tab_bar.mid_clicked.connect(self._on_tab_mid_click)
|
||||
tab_widget.tabCloseRequested.connect(self._on_tab_close_req)
|
||||
|
||||
self._widgets_splitter = widgets_splitter
|
||||
self._output_widget = output_widget
|
||||
self._tab_widget = tab_widget
|
||||
self._line_check_timer = line_check_timer
|
||||
|
||||
self._append_lines([ayon_art])
|
||||
|
||||
self._first_show = True
|
||||
self._splitter_size_ratio = None
|
||||
self._allow_save_registry = allow_save_registry
|
||||
self._registry_saved = True
|
||||
|
||||
self._init_from_registry()
|
||||
|
||||
if self._tab_widget.count() < 1:
|
||||
self.add_tab("Python")
|
||||
|
||||
def _init_from_registry(self):
|
||||
setting_registry = PythonInterpreterRegistry()
|
||||
width = None
|
||||
height = None
|
||||
try:
|
||||
width = setting_registry.get_item("width")
|
||||
height = setting_registry.get_item("height")
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if width is None or width < 200:
|
||||
width = self.default_width
|
||||
|
||||
if height is None or height < 200:
|
||||
height = self.default_height
|
||||
|
||||
self.resize(width, height)
|
||||
|
||||
try:
|
||||
self._splitter_size_ratio = (
|
||||
setting_registry.get_item("splitter_sizes")
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
tab_defs = setting_registry.get_item("tabs") or []
|
||||
for tab_def in tab_defs:
|
||||
widget = self.add_tab(tab_def["name"])
|
||||
widget.set_code(tab_def["code"])
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def save_registry(self):
|
||||
# Window was not showed
|
||||
if not self._allow_save_registry or self._registry_saved:
|
||||
return
|
||||
|
||||
self._registry_saved = True
|
||||
setting_registry = PythonInterpreterRegistry()
|
||||
|
||||
setting_registry.set_item("width", self.width())
|
||||
setting_registry.set_item("height", self.height())
|
||||
|
||||
setting_registry.set_item(
|
||||
"splitter_sizes", self._widgets_splitter.sizes()
|
||||
)
|
||||
|
||||
tabs = []
|
||||
for tab_idx in range(self._tab_widget.count()):
|
||||
widget = self._tab_widget.widget(tab_idx)
|
||||
tab_code = widget.get_code()
|
||||
tab_name = self._tab_widget.tabText(tab_idx)
|
||||
tabs.append({
|
||||
"name": tab_name,
|
||||
"code": tab_code
|
||||
})
|
||||
|
||||
setting_registry.set_item("tabs", tabs)
|
||||
|
||||
def _on_tab_right_click(self, global_point):
|
||||
point = self._tab_widget.mapFromGlobal(global_point)
|
||||
tab_bar = self._tab_widget.tabBar()
|
||||
tab_idx = tab_bar.tabAt(point)
|
||||
last_index = tab_bar.count() - 1
|
||||
if tab_idx < 0 or tab_idx > last_index:
|
||||
return
|
||||
|
||||
menu = QtWidgets.QMenu(self._tab_widget)
|
||||
|
||||
add_tab_action = QtWidgets.QAction("Add tab...", menu)
|
||||
add_tab_action.setToolTip("Add new tab")
|
||||
|
||||
rename_tab_action = QtWidgets.QAction("Rename...", menu)
|
||||
rename_tab_action.setToolTip("Rename tab")
|
||||
|
||||
duplicate_tab_action = QtWidgets.QAction("Duplicate...", menu)
|
||||
duplicate_tab_action.setToolTip("Duplicate code to new tab")
|
||||
|
||||
close_tab_action = QtWidgets.QAction("Close", menu)
|
||||
close_tab_action.setToolTip("Close tab and lose content")
|
||||
close_tab_action.setEnabled(self._tab_widget.tabsClosable())
|
||||
|
||||
menu.addAction(add_tab_action)
|
||||
menu.addAction(rename_tab_action)
|
||||
menu.addAction(duplicate_tab_action)
|
||||
menu.addAction(close_tab_action)
|
||||
|
||||
result = menu.exec_(global_point)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
if result is rename_tab_action:
|
||||
self._rename_tab_req(tab_idx)
|
||||
|
||||
elif result is add_tab_action:
|
||||
self._on_add_requested()
|
||||
|
||||
elif result is duplicate_tab_action:
|
||||
self._duplicate_requested(tab_idx)
|
||||
|
||||
elif result is close_tab_action:
|
||||
self._on_tab_close_req(tab_idx)
|
||||
|
||||
def _rename_tab_req(self, tab_idx):
|
||||
dialog = TabNameDialog(self)
|
||||
dialog.set_tab_name(self._tab_widget.tabText(tab_idx))
|
||||
dialog.exec_()
|
||||
tab_name = dialog.result()
|
||||
if tab_name:
|
||||
self._tab_widget.setTabText(tab_idx, tab_name)
|
||||
|
||||
def _duplicate_requested(self, tab_idx=None):
|
||||
if tab_idx is None:
|
||||
tab_idx = self._tab_widget.currentIndex()
|
||||
|
||||
src_widget = self._tab_widget.widget(tab_idx)
|
||||
dst_widget = self._add_tab()
|
||||
if dst_widget is None:
|
||||
return
|
||||
dst_widget.set_code(src_widget.get_code())
|
||||
|
||||
def _on_tab_mid_click(self, global_point):
|
||||
point = self._tab_widget.mapFromGlobal(global_point)
|
||||
tab_bar = self._tab_widget.tabBar()
|
||||
tab_idx = tab_bar.tabAt(point)
|
||||
last_index = tab_bar.count() - 1
|
||||
if tab_idx < 0 or tab_idx > last_index:
|
||||
return
|
||||
|
||||
self._on_tab_close_req(tab_idx)
|
||||
|
||||
def _on_tab_double_click(self, global_point):
|
||||
point = self._tab_widget.mapFromGlobal(global_point)
|
||||
tab_bar = self._tab_widget.tabBar()
|
||||
tab_idx = tab_bar.tabAt(point)
|
||||
last_index = tab_bar.count() - 1
|
||||
if tab_idx < 0 or tab_idx > last_index:
|
||||
return
|
||||
|
||||
self._rename_tab_req(tab_idx)
|
||||
|
||||
def _on_tab_close_req(self, tab_index):
|
||||
if self._tab_widget.count() == 1:
|
||||
return
|
||||
|
||||
widget = self._tab_widget.widget(tab_index)
|
||||
if widget in self._tabs:
|
||||
self._tabs.remove(widget)
|
||||
self._tab_widget.removeTab(tab_index)
|
||||
|
||||
if self._tab_widget.count() == 1:
|
||||
self._tab_widget.setTabsClosable(False)
|
||||
|
||||
def _append_lines(self, lines):
|
||||
at_max = self._output_widget.vertical_scroll_at_max()
|
||||
tmp_cursor = QtGui.QTextCursor(self._output_widget.document())
|
||||
tmp_cursor.movePosition(QtGui.QTextCursor.End)
|
||||
for line in lines:
|
||||
tmp_cursor.insertText(line)
|
||||
|
||||
if at_max:
|
||||
self._output_widget.scroll_to_bottom()
|
||||
|
||||
def _on_timer_timeout(self):
|
||||
if self._stdout_err_wrapper.lines:
|
||||
lines = []
|
||||
while self._stdout_err_wrapper.lines:
|
||||
line = self._stdout_err_wrapper.lines.popleft()
|
||||
lines.append(self.ansi_escape.sub("", line))
|
||||
self._append_lines(lines)
|
||||
|
||||
def _on_add_requested(self):
|
||||
self._add_tab()
|
||||
|
||||
def _add_tab(self):
|
||||
dialog = TabNameDialog(self)
|
||||
dialog.exec_()
|
||||
tab_name = dialog.result()
|
||||
if tab_name:
|
||||
return self.add_tab(tab_name)
|
||||
|
||||
return None
|
||||
|
||||
def _on_before_execute(self, code_text):
|
||||
at_max = self._output_widget.vertical_scroll_at_max()
|
||||
document = self._output_widget.document()
|
||||
tmp_cursor = QtGui.QTextCursor(document)
|
||||
tmp_cursor.movePosition(QtGui.QTextCursor.End)
|
||||
tmp_cursor.insertText("{}\nExecuting command:\n".format(20 * "-"))
|
||||
|
||||
code_block_format = QtGui.QTextFrameFormat()
|
||||
code_block_format.setBackground(QtGui.QColor(27, 27, 27))
|
||||
code_block_format.setPadding(4)
|
||||
|
||||
tmp_cursor.insertFrame(code_block_format)
|
||||
char_format = tmp_cursor.charFormat()
|
||||
char_format.setForeground(
|
||||
QtGui.QBrush(QtGui.QColor(114, 224, 198))
|
||||
)
|
||||
tmp_cursor.setCharFormat(char_format)
|
||||
tmp_cursor.insertText(code_text)
|
||||
|
||||
# Create new cursor
|
||||
tmp_cursor = QtGui.QTextCursor(document)
|
||||
tmp_cursor.movePosition(QtGui.QTextCursor.End)
|
||||
tmp_cursor.insertText("{}\n".format(20 * "-"))
|
||||
|
||||
if at_max:
|
||||
self._output_widget.scroll_to_bottom()
|
||||
|
||||
def add_tab(self, tab_name, index=None):
|
||||
widget = PythonTabWidget(self)
|
||||
widget.before_execute.connect(self._on_before_execute)
|
||||
widget.add_tab_requested.connect(self._on_add_requested)
|
||||
if index is None:
|
||||
if self._tab_widget.count() > 0:
|
||||
index = self._tab_widget.currentIndex() + 1
|
||||
else:
|
||||
index = 0
|
||||
|
||||
self._tabs.append(widget)
|
||||
self._tab_widget.insertTab(index, widget, tab_name)
|
||||
self._tab_widget.setCurrentIndex(index)
|
||||
|
||||
if self._tab_widget.count() > 1:
|
||||
self._tab_widget.setTabsClosable(True)
|
||||
widget.setFocus()
|
||||
return widget
|
||||
|
||||
def showEvent(self, event):
|
||||
self._line_check_timer.start()
|
||||
self._registry_saved = False
|
||||
super(PythonInterpreterWidget, self).showEvent(event)
|
||||
# First show setup
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
||||
self._output_widget.scroll_to_bottom()
|
||||
|
||||
def _on_first_show(self):
|
||||
# Change stylesheet
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
# Check if splitter size ratio is set
|
||||
# - first store value to local variable and then unset it
|
||||
splitter_size_ratio = self._splitter_size_ratio
|
||||
self._splitter_size_ratio = None
|
||||
# Skip if is not set
|
||||
if not splitter_size_ratio:
|
||||
return
|
||||
|
||||
# Skip if number of size items does not match to splitter
|
||||
splitters_count = len(self._widgets_splitter.sizes())
|
||||
if len(splitter_size_ratio) == splitters_count:
|
||||
self._widgets_splitter.setSizes(splitter_size_ratio)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.save_registry()
|
||||
super(PythonInterpreterWidget, self).closeEvent(event)
|
||||
self._line_check_timer.stop()
|
||||
|
|
@ -7,6 +7,10 @@ from .constants import (
|
|||
|
||||
from .anatomy import Anatomy
|
||||
|
||||
from .tempdir import get_temp_dir
|
||||
|
||||
from .staging_dir import get_staging_dir_info
|
||||
|
||||
from .create import (
|
||||
BaseCreator,
|
||||
Creator,
|
||||
|
|
@ -117,6 +121,12 @@ __all__ = (
|
|||
# --- Anatomy ---
|
||||
"Anatomy",
|
||||
|
||||
# --- Temp dir ---
|
||||
"get_temp_dir",
|
||||
|
||||
# --- Staging dir ---
|
||||
"get_staging_dir_info",
|
||||
|
||||
# --- Create ---
|
||||
"BaseCreator",
|
||||
"Creator",
|
||||
|
|
|
|||
|
|
@ -585,9 +585,6 @@ def version_up_current_workfile():
|
|||
"""Function to increment and save workfile
|
||||
"""
|
||||
host = registered_host()
|
||||
if not host.has_unsaved_changes():
|
||||
print("No unsaved changes, skipping file save..")
|
||||
return
|
||||
|
||||
project_name = get_current_project_name()
|
||||
folder_path = get_current_folder_path()
|
||||
|
|
|
|||
|
|
@ -1283,12 +1283,16 @@ class CreateContext:
|
|||
|
||||
@contextmanager
|
||||
def bulk_pre_create_attr_defs_change(self, sender=None):
|
||||
with self._bulk_context("pre_create_attrs_change", sender) as bulk_info:
|
||||
with self._bulk_context(
|
||||
"pre_create_attrs_change", sender
|
||||
) as bulk_info:
|
||||
yield bulk_info
|
||||
|
||||
@contextmanager
|
||||
def bulk_create_attr_defs_change(self, sender=None):
|
||||
with self._bulk_context("create_attrs_change", sender) as bulk_info:
|
||||
with self._bulk_context(
|
||||
"create_attrs_change", sender
|
||||
) as bulk_info:
|
||||
yield bulk_info
|
||||
|
||||
@contextmanager
|
||||
|
|
@ -1946,9 +1950,9 @@ class CreateContext:
|
|||
creator are just removed from context.
|
||||
|
||||
Args:
|
||||
instances (List[CreatedInstance]): Instances that should be removed.
|
||||
Remove logic is done using creator, which may require to
|
||||
do other cleanup than just remove instance from context.
|
||||
instances (List[CreatedInstance]): Instances that should be
|
||||
removed. Remove logic is done using creator, which may require
|
||||
to do other cleanup than just remove instance from context.
|
||||
sender (Optional[str]): Sender of the event.
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import copy
|
||||
import collections
|
||||
from typing import TYPE_CHECKING, Optional, Dict, Any
|
||||
|
|
@ -6,7 +7,7 @@ from typing import TYPE_CHECKING, Optional, Dict, Any
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.lib import Logger, get_version_from_path
|
||||
from ayon_core.pipeline.plugin_discover import (
|
||||
discover,
|
||||
register_plugin,
|
||||
|
|
@ -14,6 +15,7 @@ from ayon_core.pipeline.plugin_discover import (
|
|||
deregister_plugin,
|
||||
deregister_plugin_path
|
||||
)
|
||||
from ayon_core.pipeline import get_staging_dir_info
|
||||
|
||||
from .constants import DEFAULT_VARIANT_VALUE
|
||||
from .product_name import get_product_name
|
||||
|
|
@ -831,6 +833,95 @@ class Creator(BaseCreator):
|
|||
"""
|
||||
return self.pre_create_attr_defs
|
||||
|
||||
def get_staging_dir(self, instance):
|
||||
"""Return the staging dir and persistence from instance.
|
||||
|
||||
Args:
|
||||
instance (CreatedInstance): Instance for which should be staging
|
||||
dir gathered.
|
||||
|
||||
Returns:
|
||||
Optional[namedtuple]: Staging dir path and persistence or None
|
||||
"""
|
||||
create_ctx = self.create_context
|
||||
product_name = instance.get("productName")
|
||||
product_type = instance.get("productType")
|
||||
folder_path = instance.get("folderPath")
|
||||
|
||||
# this can only work if product name and folder path are available
|
||||
if not product_name or not folder_path:
|
||||
return None
|
||||
|
||||
publish_settings = self.project_settings["core"]["publish"]
|
||||
follow_workfile_version = (
|
||||
publish_settings
|
||||
["CollectAnatomyInstanceData"]
|
||||
["follow_workfile_version"]
|
||||
)
|
||||
|
||||
# Gather version number provided from the instance.
|
||||
version = instance.get("version")
|
||||
|
||||
# If follow workfile, gather version from workfile path.
|
||||
if version is None and follow_workfile_version:
|
||||
current_workfile = self.create_context.get_current_workfile_path()
|
||||
workfile_version = get_version_from_path(current_workfile)
|
||||
version = int(workfile_version)
|
||||
|
||||
# Fill-up version with next version available.
|
||||
elif version is None:
|
||||
versions = self.get_next_versions_for_instances(
|
||||
[instance]
|
||||
)
|
||||
version, = tuple(versions.values())
|
||||
|
||||
template_data = {"version": version}
|
||||
|
||||
staging_dir_info = get_staging_dir_info(
|
||||
create_ctx.get_current_project_entity(),
|
||||
create_ctx.get_folder_entity(folder_path),
|
||||
create_ctx.get_task_entity(folder_path, instance.get("task")),
|
||||
product_type,
|
||||
product_name,
|
||||
create_ctx.host_name,
|
||||
anatomy=create_ctx.get_current_project_anatomy(),
|
||||
project_settings=create_ctx.get_current_project_settings(),
|
||||
always_return_path=False,
|
||||
logger=self.log,
|
||||
template_data=template_data,
|
||||
)
|
||||
|
||||
return staging_dir_info or None
|
||||
|
||||
def apply_staging_dir(self, instance):
|
||||
"""Apply staging dir with persistence to instance's transient data.
|
||||
|
||||
Method is called on instance creation and on instance update.
|
||||
|
||||
Args:
|
||||
instance (CreatedInstance): Instance for which should be staging
|
||||
dir applied.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Staging dir path or None if not applied.
|
||||
"""
|
||||
staging_dir_info = self.get_staging_dir(instance)
|
||||
if staging_dir_info is None:
|
||||
return None
|
||||
|
||||
# path might be already created by get_staging_dir_info
|
||||
staging_dir_path = staging_dir_info.directory
|
||||
os.makedirs(staging_dir_path, exist_ok=True)
|
||||
|
||||
instance.transient_data.update({
|
||||
"stagingDir": staging_dir_path,
|
||||
"stagingDir_persistent": staging_dir_info.persistent,
|
||||
})
|
||||
|
||||
self.log.info(f"Applied staging dir to instance: {staging_dir_path}")
|
||||
|
||||
return staging_dir_path
|
||||
|
||||
def _pre_create_attr_defs_changed(self):
|
||||
"""Called when pre-create attribute definitions change.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import ayon_api
|
||||
from ayon_core.lib import StringTemplate, filter_profiles, prepare_template_data
|
||||
from ayon_core.lib import (
|
||||
StringTemplate,
|
||||
filter_profiles,
|
||||
prepare_template_data,
|
||||
)
|
||||
from ayon_core.settings import get_project_settings
|
||||
|
||||
from .constants import DEFAULT_PRODUCT_TEMPLATE
|
||||
|
|
|
|||
|
|
@ -429,11 +429,18 @@ class CreatedInstance:
|
|||
__immutable_keys = (
|
||||
"id",
|
||||
"instance_id",
|
||||
"product_type",
|
||||
"productType",
|
||||
"creator_identifier",
|
||||
"creator_attributes",
|
||||
"publish_attributes"
|
||||
)
|
||||
# Keys that can be changed, but should not be removed from instance
|
||||
__required_keys = {
|
||||
"folderPath": None,
|
||||
"task": None,
|
||||
"productName": None,
|
||||
"active": True,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -515,6 +522,9 @@ class CreatedInstance:
|
|||
if data:
|
||||
self._data.update(data)
|
||||
|
||||
for key, default in self.__required_keys.items():
|
||||
self._data.setdefault(key, default)
|
||||
|
||||
if not self._data.get("instance_id"):
|
||||
self._data["instance_id"] = str(uuid4())
|
||||
|
||||
|
|
@ -567,6 +577,8 @@ class CreatedInstance:
|
|||
has_key = key in self._data
|
||||
output = self._data.pop(key, *args, **kwargs)
|
||||
if has_key:
|
||||
if key in self.__required_keys:
|
||||
self._data[key] = self.__required_keys[key]
|
||||
self._create_context.instance_values_changed(
|
||||
self.id, {key: None}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@ def get_representations_delivery_template_data(
|
|||
# convert representation entity. Fixed in 'ayon_api' 1.0.10.
|
||||
if isinstance(template_data, str):
|
||||
con = ayon_api.get_server_api_connection()
|
||||
repre_entity = con._representation_conversion(repre_entity)
|
||||
con._representation_conversion(repre_entity)
|
||||
template_data = repre_entity["context"]
|
||||
|
||||
template_data.update(copy.deepcopy(general_template_data))
|
||||
|
|
|
|||
|
|
@ -222,6 +222,9 @@ def remap_range_on_file_sequence(otio_clip, in_out_range):
|
|||
source_range = otio_clip.source_range
|
||||
available_range_rate = available_range.start_time.rate
|
||||
media_in = available_range.start_time.value
|
||||
available_range_start_frame = (
|
||||
available_range.start_time.to_frames()
|
||||
)
|
||||
|
||||
# Temporary.
|
||||
# Some AYON custom OTIO exporter were implemented with relative
|
||||
|
|
@ -230,7 +233,7 @@ def remap_range_on_file_sequence(otio_clip, in_out_range):
|
|||
# while we are updating those.
|
||||
if (
|
||||
is_clip_from_media_sequence(otio_clip)
|
||||
and otio_clip.available_range().start_time.to_frames() == media_ref.start_frame
|
||||
and available_range_start_frame == media_ref.start_frame
|
||||
and source_range.start_time.to_frames() < media_ref.start_frame
|
||||
):
|
||||
media_in = 0
|
||||
|
|
@ -303,8 +306,12 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
|
|||
rounded_av_rate = round(available_range_rate, 2)
|
||||
rounded_src_rate = round(source_range.start_time.rate, 2)
|
||||
if rounded_av_rate != rounded_src_rate:
|
||||
conformed_src_in = source_range.start_time.rescaled_to(available_range_rate)
|
||||
conformed_src_duration = source_range.duration.rescaled_to(available_range_rate)
|
||||
conformed_src_in = source_range.start_time.rescaled_to(
|
||||
available_range_rate
|
||||
)
|
||||
conformed_src_duration = source_range.duration.rescaled_to(
|
||||
available_range_rate
|
||||
)
|
||||
conformed_source_range = otio.opentime.TimeRange(
|
||||
start_time=conformed_src_in,
|
||||
duration=conformed_src_duration
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ def parse_ayon_entity_uri(uri: str) -> Optional[dict]:
|
|||
|
||||
Example:
|
||||
>>> parse_ayon_entity_uri(
|
||||
>>> "ayon://test/char/villain?product=modelMain&version=2&representation=usd" # noqa: E501
|
||||
>>> "ayon://test/char/villain?product=modelMain&version=2&representation=usd"
|
||||
>>> )
|
||||
{'project': 'test', 'folderPath': '/char/villain',
|
||||
'product': 'modelMain', 'version': 1,
|
||||
'representation': 'usd'}
|
||||
>>> parse_ayon_entity_uri(
|
||||
>>> "ayon+entity://project/folder?product=renderMain&version=3&representation=exr" # noqa: E501
|
||||
>>> "ayon+entity://project/folder?product=renderMain&version=3&representation=exr"
|
||||
>>> )
|
||||
{'project': 'project', 'folderPath': '/folder',
|
||||
'product': 'renderMain', 'version': 3,
|
||||
|
|
@ -34,7 +34,7 @@ def parse_ayon_entity_uri(uri: str) -> Optional[dict]:
|
|||
dict[str, Union[str, int]]: The individual key with their values as
|
||||
found in the ayon entity URI.
|
||||
|
||||
"""
|
||||
""" # noqa: E501
|
||||
|
||||
if not (uri.startswith("ayon+entity://") or uri.startswith("ayon://")):
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ import attr
|
|||
import ayon_api
|
||||
import clique
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.pipeline import get_current_project_name, get_representation_path
|
||||
from ayon_core.pipeline import (
|
||||
get_current_project_name,
|
||||
get_representation_path,
|
||||
)
|
||||
from ayon_core.pipeline.create import get_product_name
|
||||
from ayon_core.pipeline.farm.patterning import match_aov_pattern
|
||||
from ayon_core.pipeline.publish import KnownPublishError
|
||||
|
|
@ -295,11 +298,17 @@ def _add_review_families(families):
|
|||
return families
|
||||
|
||||
|
||||
def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter,
|
||||
skip_integration_repre_list,
|
||||
do_not_add_review,
|
||||
context,
|
||||
color_managed_plugin):
|
||||
def prepare_representations(
|
||||
skeleton_data,
|
||||
exp_files,
|
||||
anatomy,
|
||||
aov_filter,
|
||||
skip_integration_repre_list,
|
||||
do_not_add_review,
|
||||
context,
|
||||
color_managed_plugin,
|
||||
frames_to_render=None
|
||||
):
|
||||
"""Create representations for file sequences.
|
||||
|
||||
This will return representations of expected files if they are not
|
||||
|
|
@ -315,6 +324,8 @@ def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter,
|
|||
skip_integration_repre_list (list): exclude specific extensions,
|
||||
do_not_add_review (bool): explicitly skip review
|
||||
color_managed_plugin (publish.ColormanagedPyblishPluginMixin)
|
||||
frames_to_render (str): implicit or explicit range of frames to render
|
||||
this value is sent to Deadline in JobInfo.Frames
|
||||
Returns:
|
||||
list of representations
|
||||
|
||||
|
|
@ -325,6 +336,14 @@ def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter,
|
|||
|
||||
log = Logger.get_logger("farm_publishing")
|
||||
|
||||
if frames_to_render is not None:
|
||||
frames_to_render = _get_real_frames_to_render(frames_to_render)
|
||||
else:
|
||||
# Backwards compatibility for older logic
|
||||
frame_start = int(skeleton_data.get("frameStartHandle"))
|
||||
frame_end = int(skeleton_data.get("frameEndHandle"))
|
||||
frames_to_render = list(range(frame_start, frame_end + 1))
|
||||
|
||||
# create representation for every collected sequence
|
||||
for collection in collections:
|
||||
ext = collection.tail.lstrip(".")
|
||||
|
|
@ -361,18 +380,21 @@ def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter,
|
|||
" This may cause issues on farm."
|
||||
).format(staging))
|
||||
|
||||
frame_start = int(skeleton_data.get("frameStartHandle"))
|
||||
frame_start = frames_to_render[0]
|
||||
frame_end = frames_to_render[-1]
|
||||
if skeleton_data.get("slate"):
|
||||
frame_start -= 1
|
||||
frames_to_render.insert(0, frame_start)
|
||||
|
||||
files = _get_real_files_to_render(collection, frames_to_render)
|
||||
# explicitly disable review by user
|
||||
preview = preview and not do_not_add_review
|
||||
rep = {
|
||||
"name": ext,
|
||||
"ext": ext,
|
||||
"files": [os.path.basename(f) for f in list(collection)],
|
||||
"files": files,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": int(skeleton_data.get("frameEndHandle")),
|
||||
"frameEnd": frame_end,
|
||||
# If expectedFile are absolute, we need only filenames
|
||||
"stagingDir": staging,
|
||||
"fps": skeleton_data.get("fps"),
|
||||
|
|
@ -453,6 +475,61 @@ def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter,
|
|||
return representations
|
||||
|
||||
|
||||
def _get_real_frames_to_render(frames):
|
||||
"""Returns list of frames that should be rendered.
|
||||
|
||||
Artists could want to selectively render only particular frames
|
||||
"""
|
||||
frames_to_render = []
|
||||
for frame in frames.split(","):
|
||||
if "-" in frame:
|
||||
splitted = frame.split("-")
|
||||
frames_to_render.extend(
|
||||
range(int(splitted[0]), int(splitted[1])+1))
|
||||
else:
|
||||
frames_to_render.append(int(frame))
|
||||
frames_to_render.sort()
|
||||
return frames_to_render
|
||||
|
||||
|
||||
def _get_real_files_to_render(collection, frames_to_render):
|
||||
"""Filter files with frames that should be really rendered.
|
||||
|
||||
'expected_files' are collected from DCC based on timeline setting. This is
|
||||
being calculated differently in each DCC. Filtering here is on single place
|
||||
|
||||
But artists might explicitly set frames they want to render in Publisher UI
|
||||
This range would override and filter previously prepared expected files
|
||||
from DCC.
|
||||
|
||||
Args:
|
||||
collection (clique.Collection): absolute paths
|
||||
frames_to_render (list[int]): of int 1001
|
||||
Returns:
|
||||
(list[str])
|
||||
|
||||
Example:
|
||||
--------
|
||||
|
||||
expectedFiles = [
|
||||
"foo_v01.0001.exr",
|
||||
"foo_v01.0002.exr",
|
||||
]
|
||||
frames_to_render = 1
|
||||
>>
|
||||
["foo_v01.0001.exr"] - only explicitly requested frame returned
|
||||
"""
|
||||
included_frames = set(collection.indexes).intersection(frames_to_render)
|
||||
real_collection = clique.Collection(
|
||||
collection.head,
|
||||
collection.tail,
|
||||
collection.padding,
|
||||
indexes=included_frames
|
||||
)
|
||||
real_full_paths = list(real_collection)
|
||||
return [os.path.basename(file_url) for file_url in real_full_paths]
|
||||
|
||||
|
||||
def create_instances_for_aov(instance, skeleton, aov_filter,
|
||||
skip_integration_repre_list,
|
||||
do_not_add_review):
|
||||
|
|
@ -702,9 +779,14 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
|
|||
|
||||
project_settings = instance.context.data.get("project_settings")
|
||||
|
||||
use_legacy_product_name = True
|
||||
try:
|
||||
use_legacy_product_name = project_settings["core"]["tools"]["creator"]["use_legacy_product_names_for_renders"] # noqa: E501
|
||||
use_legacy_product_name = (
|
||||
project_settings
|
||||
["core"]
|
||||
["tools"]
|
||||
["creator"]
|
||||
["use_legacy_product_names_for_renders"]
|
||||
)
|
||||
except KeyError:
|
||||
warnings.warn(
|
||||
("use_legacy_for_renders not found in project settings. "
|
||||
|
|
@ -720,7 +802,9 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
|
|||
dynamic_data=dynamic_data)
|
||||
|
||||
else:
|
||||
product_name, group_name = get_product_name_and_group_from_template(
|
||||
(
|
||||
product_name, group_name
|
||||
) = get_product_name_and_group_from_template(
|
||||
task_entity=instance.data["taskEntity"],
|
||||
project_name=instance.context.data["projectName"],
|
||||
host_name=instance.context.data["hostName"],
|
||||
|
|
@ -863,7 +947,7 @@ def _collect_expected_files_for_aov(files):
|
|||
# but we really expect only one collection.
|
||||
# Nothing else make sense.
|
||||
if len(cols) != 1:
|
||||
raise ValueError("Only one image sequence type is expected.") # noqa: E501
|
||||
raise ValueError("Only one image sequence type is expected.")
|
||||
return list(cols[0])
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -465,7 +465,9 @@ def update_container(container, version=-1):
|
|||
from ayon_core.pipeline import get_current_project_name
|
||||
|
||||
# Compute the different version from 'representation'
|
||||
project_name = get_current_project_name()
|
||||
project_name = container.get("project_name")
|
||||
if project_name is None:
|
||||
project_name = get_current_project_name()
|
||||
repre_id = container["representation"]
|
||||
if not _is_valid_representation_id(repre_id):
|
||||
raise ValueError(
|
||||
|
|
@ -542,9 +544,6 @@ def update_container(container, version=-1):
|
|||
)
|
||||
)
|
||||
|
||||
path = get_representation_path(new_representation)
|
||||
if not path or not os.path.exists(path):
|
||||
raise ValueError("Path {} doesn't exist".format(path))
|
||||
project_entity = ayon_api.get_project(project_name)
|
||||
context = {
|
||||
"project": project_entity,
|
||||
|
|
@ -553,6 +552,9 @@ def update_container(container, version=-1):
|
|||
"version": new_version,
|
||||
"representation": new_representation,
|
||||
}
|
||||
path = get_representation_path_from_context(context)
|
||||
if not path or not os.path.exists(path):
|
||||
raise ValueError("Path {} doesn't exist".format(path))
|
||||
|
||||
return Loader().update(container, context)
|
||||
|
||||
|
|
@ -588,7 +590,9 @@ def switch_container(container, representation, loader_plugin=None):
|
|||
)
|
||||
|
||||
# Get the new representation to switch to
|
||||
project_name = get_current_project_name()
|
||||
project_name = container.get("project_name")
|
||||
if project_name is None:
|
||||
project_name = get_current_project_name()
|
||||
|
||||
context = get_representation_context(
|
||||
project_name, representation["id"]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from .constants import (
|
|||
ValidateContentsOrder,
|
||||
ValidateSceneOrder,
|
||||
ValidateMeshOrder,
|
||||
FARM_JOB_ENV_DATA_KEY,
|
||||
)
|
||||
|
||||
from .publish_plugins import (
|
||||
|
|
@ -59,6 +60,7 @@ __all__ = (
|
|||
"ValidateContentsOrder",
|
||||
"ValidateSceneOrder",
|
||||
"ValidateMeshOrder",
|
||||
"FARM_JOB_ENV_DATA_KEY",
|
||||
|
||||
"AbstractMetaInstancePlugin",
|
||||
"AbstractMetaContextPlugin",
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3
|
|||
|
||||
DEFAULT_PUBLISH_TEMPLATE = "default"
|
||||
DEFAULT_HERO_PUBLISH_TEMPLATE = "default"
|
||||
TRANSIENT_DIR_TEMPLATE = "default"
|
||||
|
||||
FARM_JOB_ENV_DATA_KEY: str = "farmJobEnv"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import os
|
|||
import sys
|
||||
import inspect
|
||||
import copy
|
||||
import tempfile
|
||||
import warnings
|
||||
import xml.etree.ElementTree
|
||||
from typing import Optional, Union, List
|
||||
|
||||
|
|
@ -18,15 +18,11 @@ from ayon_core.lib import (
|
|||
)
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.addon import AddonsManager
|
||||
from ayon_core.pipeline import (
|
||||
tempdir,
|
||||
Anatomy
|
||||
)
|
||||
from ayon_core.pipeline import get_staging_dir_info
|
||||
from ayon_core.pipeline.plugin_discover import DiscoverResult
|
||||
from .constants import (
|
||||
DEFAULT_PUBLISH_TEMPLATE,
|
||||
DEFAULT_HERO_PUBLISH_TEMPLATE,
|
||||
TRANSIENT_DIR_TEMPLATE
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -581,58 +577,6 @@ def context_plugin_should_run(plugin, context):
|
|||
return False
|
||||
|
||||
|
||||
def get_instance_staging_dir(instance):
|
||||
"""Unified way how staging dir is stored and created on instances.
|
||||
|
||||
First check if 'stagingDir' is already set in instance data.
|
||||
In case there already is new tempdir will not be created.
|
||||
|
||||
It also supports `AYON_TMPDIR`, so studio can define own temp
|
||||
shared repository per project or even per more granular context.
|
||||
Template formatting is supported also with optional keys. Folder is
|
||||
created in case it doesn't exists.
|
||||
|
||||
Available anatomy formatting keys:
|
||||
- root[work | <root name key>]
|
||||
- project[name | code]
|
||||
|
||||
Note:
|
||||
Staging dir does not have to be necessarily in tempdir so be careful
|
||||
about its usage.
|
||||
|
||||
Args:
|
||||
instance (pyblish.lib.Instance): Instance for which we want to get
|
||||
staging dir.
|
||||
|
||||
Returns:
|
||||
str: Path to staging dir of instance.
|
||||
"""
|
||||
staging_dir = instance.data.get('stagingDir')
|
||||
if staging_dir:
|
||||
return staging_dir
|
||||
|
||||
anatomy = instance.context.data.get("anatomy")
|
||||
|
||||
# get customized tempdir path from `AYON_TMPDIR` env var
|
||||
custom_temp_dir = tempdir.create_custom_tempdir(
|
||||
anatomy.project_name, anatomy)
|
||||
|
||||
if custom_temp_dir:
|
||||
staging_dir = os.path.normpath(
|
||||
tempfile.mkdtemp(
|
||||
prefix="pyblish_tmp_",
|
||||
dir=custom_temp_dir
|
||||
)
|
||||
)
|
||||
else:
|
||||
staging_dir = os.path.normpath(
|
||||
tempfile.mkdtemp(prefix="pyblish_tmp_")
|
||||
)
|
||||
instance.data['stagingDir'] = staging_dir
|
||||
|
||||
return staging_dir
|
||||
|
||||
|
||||
def get_publish_repre_path(instance, repre, only_published=False):
|
||||
"""Get representation path that can be used for integration.
|
||||
|
||||
|
|
@ -685,6 +629,8 @@ def get_publish_repre_path(instance, repre, only_published=False):
|
|||
return None
|
||||
|
||||
|
||||
# deprecated: backward compatibility only (2024-09-12)
|
||||
# TODO: remove in the future
|
||||
def get_custom_staging_dir_info(
|
||||
project_name,
|
||||
host_name,
|
||||
|
|
@ -694,67 +640,87 @@ def get_custom_staging_dir_info(
|
|||
product_name,
|
||||
project_settings=None,
|
||||
anatomy=None,
|
||||
log=None
|
||||
log=None,
|
||||
):
|
||||
"""Checks profiles if context should use special custom dir as staging.
|
||||
from ayon_core.pipeline.staging_dir import get_staging_dir_config
|
||||
warnings.warn(
|
||||
(
|
||||
"Function 'get_custom_staging_dir_info' in"
|
||||
" 'ayon_core.pipeline.publish' is deprecated. Please use"
|
||||
" 'get_custom_staging_dir_info'"
|
||||
" in 'ayon_core.pipeline.stagingdir'."
|
||||
),
|
||||
DeprecationWarning,
|
||||
)
|
||||
tr_data = get_staging_dir_config(
|
||||
project_name,
|
||||
task_type,
|
||||
task_name,
|
||||
product_type,
|
||||
product_name,
|
||||
host_name,
|
||||
project_settings=project_settings,
|
||||
anatomy=anatomy,
|
||||
log=log,
|
||||
)
|
||||
|
||||
Args:
|
||||
project_name (str)
|
||||
host_name (str)
|
||||
product_type (str)
|
||||
task_name (str)
|
||||
task_type (str)
|
||||
product_name (str)
|
||||
project_settings(Dict[str, Any]): Prepared project settings.
|
||||
anatomy (Dict[str, Any])
|
||||
log (Logger) (optional)
|
||||
if not tr_data:
|
||||
return None, None
|
||||
|
||||
return tr_data["template"], tr_data["persistence"]
|
||||
|
||||
|
||||
def get_instance_staging_dir(instance):
|
||||
"""Unified way how staging dir is stored and created on instances.
|
||||
|
||||
First check if 'stagingDir' is already set in instance data.
|
||||
In case there already is new tempdir will not be created.
|
||||
|
||||
Returns:
|
||||
(tuple)
|
||||
Raises:
|
||||
ValueError - if misconfigured template should be used
|
||||
str: Path to staging dir
|
||||
"""
|
||||
settings = project_settings or get_project_settings(project_name)
|
||||
custom_staging_dir_profiles = (settings["core"]
|
||||
["tools"]
|
||||
["publish"]
|
||||
["custom_staging_dir_profiles"])
|
||||
if not custom_staging_dir_profiles:
|
||||
return None, None
|
||||
staging_dir = instance.data.get("stagingDir")
|
||||
|
||||
if not log:
|
||||
log = Logger.get_logger("get_custom_staging_dir_info")
|
||||
if staging_dir:
|
||||
return staging_dir
|
||||
|
||||
filtering_criteria = {
|
||||
"hosts": host_name,
|
||||
"families": product_type,
|
||||
"task_names": task_name,
|
||||
"task_types": task_type,
|
||||
"subsets": product_name
|
||||
}
|
||||
profile = filter_profiles(custom_staging_dir_profiles,
|
||||
filtering_criteria,
|
||||
logger=log)
|
||||
anatomy_data = instance.data["anatomyData"]
|
||||
template_data = copy.deepcopy(anatomy_data)
|
||||
|
||||
if not profile or not profile["active"]:
|
||||
return None, None
|
||||
# context data based variables
|
||||
context = instance.context
|
||||
|
||||
if not anatomy:
|
||||
anatomy = Anatomy(project_name)
|
||||
# add current file as workfile name into formatting data
|
||||
current_file = context.data.get("currentFile")
|
||||
if current_file:
|
||||
workfile = os.path.basename(current_file)
|
||||
workfile_name, _ = os.path.splitext(workfile)
|
||||
template_data["workfile_name"] = workfile_name
|
||||
|
||||
template_name = profile["template_name"] or TRANSIENT_DIR_TEMPLATE
|
||||
|
||||
custom_staging_dir = anatomy.get_template_item(
|
||||
"staging", template_name, "directory", default=None
|
||||
staging_dir_info = get_staging_dir_info(
|
||||
context.data["projectEntity"],
|
||||
instance.data.get("folderEntity"),
|
||||
instance.data.get("taskEntity"),
|
||||
instance.data["productType"],
|
||||
instance.data["productName"],
|
||||
context.data["hostName"],
|
||||
anatomy=context.data["anatomy"],
|
||||
project_settings=context.data["project_settings"],
|
||||
template_data=template_data,
|
||||
always_return_path=True,
|
||||
)
|
||||
if custom_staging_dir is None:
|
||||
raise ValueError((
|
||||
"Anatomy of project \"{}\" does not have set"
|
||||
" \"{}\" template key!"
|
||||
).format(project_name, template_name))
|
||||
is_persistent = profile["custom_staging_dir_persistent"]
|
||||
|
||||
return custom_staging_dir.template, is_persistent
|
||||
staging_dir_path = staging_dir_info.directory
|
||||
|
||||
# path might be already created by get_staging_dir_info
|
||||
os.makedirs(staging_dir_path, exist_ok=True)
|
||||
instance.data.update({
|
||||
"stagingDir": staging_dir_path,
|
||||
"stagingDir_persistent": staging_dir_info.persistent,
|
||||
"stagingDir_custom": staging_dir_info.custom
|
||||
})
|
||||
|
||||
return staging_dir_path
|
||||
|
||||
|
||||
def get_published_workfile_instance(context):
|
||||
|
|
@ -799,7 +765,7 @@ def replace_with_published_scene_path(instance, replace_in_path=True):
|
|||
return
|
||||
|
||||
# determine published path from Anatomy.
|
||||
template_data = workfile_instance.data.get("anatomyData")
|
||||
template_data = copy.deepcopy(workfile_instance.data["anatomyData"])
|
||||
rep = workfile_instance.data["representations"][0]
|
||||
template_data["representation"] = rep.get("name")
|
||||
template_data["ext"] = rep.get("ext")
|
||||
|
|
|
|||
|
|
@ -205,9 +205,9 @@ class AYONPyblishPluginMixin:
|
|||
if not cls.__instanceEnabled__:
|
||||
return False
|
||||
|
||||
for _ in pyblish.logic.plugins_by_families(
|
||||
[cls], [instance.product_type]
|
||||
):
|
||||
families = [instance.product_type]
|
||||
families.extend(instance.get("families", []))
|
||||
for _ in pyblish.logic.plugins_by_families([cls], families):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
|
|||
225
client/ayon_core/pipeline/staging_dir.py
Normal file
225
client/ayon_core/pipeline/staging_dir.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from ayon_core.lib import Logger, filter_profiles
|
||||
from ayon_core.settings import get_project_settings
|
||||
|
||||
from .template_data import get_template_data
|
||||
from .anatomy import Anatomy
|
||||
from .tempdir import get_temp_dir
|
||||
|
||||
|
||||
@dataclass
|
||||
class StagingDir:
|
||||
directory: str
|
||||
persistent: bool
|
||||
custom: bool # Whether the staging dir is a custom staging dir
|
||||
|
||||
|
||||
def get_staging_dir_config(
|
||||
project_name,
|
||||
task_type,
|
||||
task_name,
|
||||
product_type,
|
||||
product_name,
|
||||
host_name,
|
||||
project_settings=None,
|
||||
anatomy=None,
|
||||
log=None,
|
||||
):
|
||||
"""Get matching staging dir profile.
|
||||
|
||||
Args:
|
||||
host_name (str): Name of host.
|
||||
project_name (str): Name of project.
|
||||
task_type (Optional[str]): Type of task.
|
||||
task_name (Optional[str]): Name of task.
|
||||
product_type (str): Type of product.
|
||||
product_name (str): Name of product.
|
||||
project_settings(Dict[str, Any]): Prepared project settings.
|
||||
anatomy (Dict[str, Any])
|
||||
log (Optional[logging.Logger])
|
||||
|
||||
Returns:
|
||||
Dict or None: Data with directory template and is_persistent or None
|
||||
|
||||
Raises:
|
||||
KeyError - if misconfigured template should be used
|
||||
|
||||
"""
|
||||
settings = project_settings or get_project_settings(project_name)
|
||||
|
||||
staging_dir_profiles = settings["core"]["tools"]["publish"][
|
||||
"custom_staging_dir_profiles"
|
||||
]
|
||||
|
||||
if not staging_dir_profiles:
|
||||
return None
|
||||
|
||||
if not log:
|
||||
log = Logger.get_logger("get_staging_dir_config")
|
||||
|
||||
filtering_criteria = {
|
||||
"hosts": host_name,
|
||||
"task_types": task_type,
|
||||
"task_names": task_name,
|
||||
"product_types": product_type,
|
||||
"product_names": product_name,
|
||||
}
|
||||
profile = filter_profiles(
|
||||
staging_dir_profiles, filtering_criteria, logger=log)
|
||||
|
||||
if not profile or not profile["active"]:
|
||||
return None
|
||||
|
||||
if not anatomy:
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
# get template from template name
|
||||
template_name = profile["template_name"]
|
||||
_validate_template_name(project_name, template_name, anatomy)
|
||||
|
||||
template = anatomy.get_template_item("staging", template_name)
|
||||
|
||||
if not template:
|
||||
# template should always be found either from anatomy or from profile
|
||||
raise KeyError(
|
||||
f"Staging template '{template_name}' was not found."
|
||||
"Check project anatomy or settings at: "
|
||||
"'ayon+settings://core/tools/publish/custom_staging_dir_profiles'"
|
||||
)
|
||||
|
||||
data_persistence = profile["custom_staging_dir_persistent"]
|
||||
|
||||
return {"template": template, "persistence": data_persistence}
|
||||
|
||||
|
||||
def _validate_template_name(project_name, template_name, anatomy):
|
||||
"""Check that staging dir section with appropriate template exist.
|
||||
|
||||
Raises:
|
||||
ValueError - if misconfigured template
|
||||
"""
|
||||
if template_name not in anatomy.templates["staging"]:
|
||||
raise ValueError(
|
||||
f'Anatomy of project "{project_name}" does not have set'
|
||||
f' "{template_name}" template key at Staging Dir category!'
|
||||
)
|
||||
|
||||
|
||||
def get_staging_dir_info(
|
||||
project_entity,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
product_type,
|
||||
product_name,
|
||||
host_name,
|
||||
anatomy=None,
|
||||
project_settings=None,
|
||||
template_data=None,
|
||||
always_return_path=True,
|
||||
force_tmp_dir=False,
|
||||
logger=None,
|
||||
prefix=None,
|
||||
suffix=None,
|
||||
):
|
||||
"""Get staging dir info data.
|
||||
|
||||
If `force_temp` is set, staging dir will be created as tempdir.
|
||||
If `always_get_some_dir` is set, staging dir will be created as tempdir if
|
||||
no staging dir profile is found.
|
||||
If `prefix` or `suffix` is not set, default values will be used.
|
||||
|
||||
Arguments:
|
||||
project_entity (Dict[str, Any]): Project entity.
|
||||
folder_entity (Optional[Dict[str, Any]]): Folder entity.
|
||||
task_entity (Optional[Dict[str, Any]]): Task entity.
|
||||
product_type (str): Type of product.
|
||||
product_name (str): Name of product.
|
||||
host_name (str): Name of host.
|
||||
anatomy (Optional[Anatomy]): Anatomy object.
|
||||
project_settings (Optional[Dict[str, Any]]): Prepared project settings.
|
||||
template_data (Optional[Dict[str, Any]]): Additional data for
|
||||
formatting staging dir template.
|
||||
always_return_path (Optional[bool]): If True, staging dir will be
|
||||
created as tempdir if no staging dir profile is found. Input value
|
||||
False will return None if no staging dir profile is found.
|
||||
force_tmp_dir (Optional[bool]): If True, staging dir will be created as
|
||||
tempdir.
|
||||
logger (Optional[logging.Logger]): Logger instance.
|
||||
prefix (Optional[str]) Optional prefix for staging dir name.
|
||||
suffix (Optional[str]): Optional suffix for staging dir name.
|
||||
|
||||
Returns:
|
||||
Optional[StagingDir]: Staging dir info data
|
||||
|
||||
"""
|
||||
log = logger or Logger.get_logger("get_staging_dir_info")
|
||||
|
||||
if anatomy is None:
|
||||
anatomy = Anatomy(
|
||||
project_entity["name"], project_entity=project_entity
|
||||
)
|
||||
|
||||
if force_tmp_dir:
|
||||
return get_temp_dir(
|
||||
project_name=project_entity["name"],
|
||||
anatomy=anatomy,
|
||||
prefix=prefix,
|
||||
suffix=suffix,
|
||||
)
|
||||
|
||||
# making few queries to database
|
||||
ctx_data = get_template_data(
|
||||
project_entity, folder_entity, task_entity, host_name
|
||||
)
|
||||
|
||||
# add additional data
|
||||
ctx_data["product"] = {
|
||||
"type": product_type,
|
||||
"name": product_name
|
||||
}
|
||||
|
||||
# add additional template formatting data
|
||||
if template_data:
|
||||
ctx_data.update(template_data)
|
||||
|
||||
task_name = task_type = None
|
||||
if task_entity:
|
||||
task_name = task_entity["name"]
|
||||
task_type = task_entity["taskType"]
|
||||
|
||||
# get staging dir config
|
||||
staging_dir_config = get_staging_dir_config(
|
||||
project_entity["name"],
|
||||
task_type,
|
||||
task_name ,
|
||||
product_type,
|
||||
product_name,
|
||||
host_name,
|
||||
project_settings=project_settings,
|
||||
anatomy=anatomy,
|
||||
log=log,
|
||||
)
|
||||
|
||||
if staging_dir_config:
|
||||
dir_template = staging_dir_config["template"]["directory"]
|
||||
return StagingDir(
|
||||
dir_template.format_strict(ctx_data),
|
||||
persistent=staging_dir_config["persistence"],
|
||||
custom=True
|
||||
)
|
||||
|
||||
# no config found but force an output
|
||||
if always_return_path:
|
||||
return StagingDir(
|
||||
get_temp_dir(
|
||||
project_name=project_entity["name"],
|
||||
anatomy=anatomy,
|
||||
prefix=prefix,
|
||||
suffix=suffix,
|
||||
),
|
||||
persistent=False,
|
||||
custom=False
|
||||
)
|
||||
|
||||
return None
|
||||
|
|
@ -3,11 +3,90 @@ Temporary folder operations
|
|||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import warnings
|
||||
|
||||
from ayon_core.lib import StringTemplate
|
||||
from ayon_core.pipeline import Anatomy
|
||||
|
||||
|
||||
def get_temp_dir(
|
||||
project_name, anatomy=None, prefix=None, suffix=None, use_local_temp=False
|
||||
):
|
||||
"""Get temporary dir path.
|
||||
|
||||
If `use_local_temp` is set, tempdir will be created in local tempdir.
|
||||
If `anatomy` is not set, default anatomy will be used.
|
||||
If `prefix` or `suffix` is not set, default values will be used.
|
||||
|
||||
It also supports `AYON_TMPDIR`, so studio can define own temp
|
||||
shared repository per project or even per more granular context.
|
||||
Template formatting is supported also with optional keys. Folder is
|
||||
created in case it doesn't exists.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project.
|
||||
anatomy (Optional[Anatomy]): Project Anatomy object.
|
||||
suffix (Optional[str]): Suffix for tempdir.
|
||||
prefix (Optional[str]): Prefix for tempdir.
|
||||
use_local_temp (Optional[bool]): If True, temp dir will be created in
|
||||
local tempdir.
|
||||
|
||||
Returns:
|
||||
str: Path to staging dir of instance.
|
||||
|
||||
"""
|
||||
if prefix is None:
|
||||
prefix = "ay_tmp_"
|
||||
suffix = suffix or ""
|
||||
|
||||
if use_local_temp:
|
||||
return _create_local_staging_dir(prefix, suffix)
|
||||
|
||||
# make sure anatomy is set
|
||||
if not anatomy:
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
# get customized tempdir path from `OPENPYPE_TMPDIR` env var
|
||||
custom_temp_dir = _create_custom_tempdir(anatomy.project_name, anatomy)
|
||||
|
||||
return _create_local_staging_dir(prefix, suffix, dirpath=custom_temp_dir)
|
||||
|
||||
|
||||
def _create_local_staging_dir(prefix, suffix, dirpath=None):
|
||||
"""Create local staging dir
|
||||
|
||||
Args:
|
||||
prefix (str): prefix for tempdir
|
||||
suffix (str): suffix for tempdir
|
||||
dirpath (Optional[str]): path to tempdir
|
||||
|
||||
Returns:
|
||||
str: path to tempdir
|
||||
"""
|
||||
# use pathlib for creating tempdir
|
||||
return tempfile.mkdtemp(
|
||||
prefix=prefix, suffix=suffix, dir=dirpath
|
||||
)
|
||||
|
||||
|
||||
def create_custom_tempdir(project_name, anatomy=None):
|
||||
"""Backward compatibility deprecated since 2024/12/09.
|
||||
"""
|
||||
warnings.warn(
|
||||
"Used deprecated 'create_custom_tempdir' "
|
||||
"use 'ayon_core.pipeline.tempdir.get_temp_dir' instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
if anatomy is None:
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
return _create_custom_tempdir(project_name, anatomy)
|
||||
|
||||
|
||||
def _create_custom_tempdir(project_name, anatomy):
|
||||
""" Create custom tempdir
|
||||
|
||||
Template path formatting is supporting:
|
||||
|
|
@ -18,42 +97,35 @@ def create_custom_tempdir(project_name, anatomy=None):
|
|||
|
||||
Args:
|
||||
project_name (str): project name
|
||||
anatomy (ayon_core.pipeline.Anatomy)[optional]: Anatomy object
|
||||
anatomy (ayon_core.pipeline.Anatomy): Anatomy object
|
||||
|
||||
Returns:
|
||||
str | None: formatted path or None
|
||||
"""
|
||||
env_tmpdir = os.getenv("AYON_TMPDIR")
|
||||
if not env_tmpdir:
|
||||
return
|
||||
return None
|
||||
|
||||
custom_tempdir = None
|
||||
if "{" in env_tmpdir:
|
||||
if anatomy is None:
|
||||
anatomy = Anatomy(project_name)
|
||||
# create base formate data
|
||||
data = {
|
||||
template_data = {
|
||||
"root": anatomy.roots,
|
||||
"project": {
|
||||
"name": anatomy.project_name,
|
||||
"code": anatomy.project_code,
|
||||
}
|
||||
},
|
||||
}
|
||||
# path is anatomy template
|
||||
custom_tempdir = StringTemplate.format_template(
|
||||
env_tmpdir, data).normalized()
|
||||
env_tmpdir, template_data)
|
||||
|
||||
custom_tempdir_path = Path(custom_tempdir)
|
||||
|
||||
else:
|
||||
# path is absolute
|
||||
custom_tempdir = env_tmpdir
|
||||
custom_tempdir_path = Path(env_tmpdir)
|
||||
|
||||
# create the dir path if it doesn't exists
|
||||
if not os.path.exists(custom_tempdir):
|
||||
try:
|
||||
# create it if it doesn't exists
|
||||
os.makedirs(custom_tempdir)
|
||||
except IOError as error:
|
||||
raise IOError(
|
||||
"Path couldn't be created: {}".format(error))
|
||||
custom_tempdir_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return custom_tempdir
|
||||
return custom_tempdir_path.as_posix()
|
||||
|
|
|
|||
|
|
@ -87,14 +87,13 @@ def get_folder_template_data(folder_entity, project_name):
|
|||
"""
|
||||
|
||||
path = folder_entity["path"]
|
||||
hierarchy_parts = path.split("/")
|
||||
# Remove empty string from the beginning
|
||||
hierarchy_parts.pop(0)
|
||||
# Remove empty string from the beginning and split by '/'
|
||||
parents = path.lstrip("/").split("/")
|
||||
# Remove last part which is folder name
|
||||
folder_name = hierarchy_parts.pop(-1)
|
||||
hierarchy = "/".join(hierarchy_parts)
|
||||
if hierarchy_parts:
|
||||
parent_name = hierarchy_parts[-1]
|
||||
folder_name = parents.pop(-1)
|
||||
hierarchy = "/".join(parents)
|
||||
if parents:
|
||||
parent_name = parents[-1]
|
||||
else:
|
||||
parent_name = project_name
|
||||
|
||||
|
|
@ -103,6 +102,7 @@ def get_folder_template_data(folder_entity, project_name):
|
|||
"name": folder_name,
|
||||
"type": folder_entity["folderType"],
|
||||
"path": path,
|
||||
"parents": parents,
|
||||
},
|
||||
"asset": folder_name,
|
||||
"hierarchy": hierarchy,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import re
|
||||
import copy
|
||||
import platform
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
import ayon_api
|
||||
|
||||
|
|
@ -16,12 +17,12 @@ from ayon_core.pipeline.template_data import get_template_data
|
|||
|
||||
|
||||
def get_workfile_template_key_from_context(
|
||||
project_name,
|
||||
folder_path,
|
||||
task_name,
|
||||
host_name,
|
||||
project_settings=None
|
||||
):
|
||||
project_name: str,
|
||||
folder_path: str,
|
||||
task_name: str,
|
||||
host_name: str,
|
||||
project_settings: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""Helper function to get template key for workfile template.
|
||||
|
||||
Do the same as `get_workfile_template_key` but returns value for "session
|
||||
|
|
@ -34,15 +35,23 @@ def get_workfile_template_key_from_context(
|
|||
host_name (str): Host name.
|
||||
project_settings (Dict[str, Any]): Project settings for passed
|
||||
'project_name'. Not required at all but makes function faster.
|
||||
"""
|
||||
|
||||
Returns:
|
||||
str: Workfile template name.
|
||||
|
||||
"""
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name, folder_path, fields={"id"}
|
||||
project_name,
|
||||
folder_path,
|
||||
fields={"id"},
|
||||
)
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name, folder_entity["id"], task_name
|
||||
project_name,
|
||||
folder_entity["id"],
|
||||
task_name,
|
||||
fields={"taskType"},
|
||||
)
|
||||
task_type = task_entity.get("type")
|
||||
task_type = task_entity.get("taskType")
|
||||
|
||||
return get_workfile_template_key(
|
||||
project_name, task_type, host_name, project_settings
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class OpenTaskPath(LauncherAction):
|
|||
if platform_name == "windows":
|
||||
args = ["start", path]
|
||||
elif platform_name == "darwin":
|
||||
args = ["open", "-na", path]
|
||||
args = ["open", "-R", path]
|
||||
elif platform_name == "linux":
|
||||
args = ["xdg-open", path]
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -413,14 +413,16 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
# Backwards compatible (Deprecated since 24/06/06)
|
||||
or instance.data.get("newAssetPublishing")
|
||||
):
|
||||
hierarchy = instance.data["hierarchy"]
|
||||
anatomy_data["hierarchy"] = hierarchy
|
||||
folder_path = instance.data["folderPath"]
|
||||
parents = folder_path.lstrip("/").split("/")
|
||||
folder_name = parents.pop(-1)
|
||||
|
||||
parent_name = project_entity["name"]
|
||||
if hierarchy:
|
||||
parent_name = hierarchy.split("/")[-1]
|
||||
hierarchy = ""
|
||||
if parents:
|
||||
parent_name = parents[-1]
|
||||
hierarchy = "/".join(parents)
|
||||
|
||||
folder_name = instance.data["folderPath"].split("/")[-1]
|
||||
anatomy_data.update({
|
||||
"asset": folder_name,
|
||||
"hierarchy": hierarchy,
|
||||
|
|
@ -432,6 +434,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
# Using 'Shot' is current default behavior of editorial
|
||||
# (or 'newHierarchyIntegration') publishing.
|
||||
"type": "Shot",
|
||||
"parents": parents,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
"""
|
||||
Requires:
|
||||
anatomy
|
||||
|
||||
|
||||
Provides:
|
||||
instance.data -> stagingDir (folder path)
|
||||
-> stagingDir_persistent (bool)
|
||||
"""
|
||||
import copy
|
||||
import os.path
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline.publish.lib import get_custom_staging_dir_info
|
||||
|
||||
|
||||
class CollectCustomStagingDir(pyblish.api.InstancePlugin):
|
||||
"""Looks through profiles if stagingDir should be persistent and in special
|
||||
location.
|
||||
|
||||
Transient staging dir could be useful in specific use cases where is
|
||||
desirable to have temporary renders in specific, persistent folders, could
|
||||
be on disks optimized for speed for example.
|
||||
|
||||
It is studio responsibility to clean up obsolete folders with data.
|
||||
|
||||
Location of the folder is configured in `project_anatomy/templates/others`.
|
||||
('transient' key is expected, with 'folder' key)
|
||||
|
||||
Which family/task type/product is applicable is configured in:
|
||||
`project_settings/global/tools/publish/custom_staging_dir_profiles`
|
||||
|
||||
"""
|
||||
label = "Collect Custom Staging Directory"
|
||||
order = pyblish.api.CollectorOrder + 0.4990
|
||||
|
||||
template_key = "transient"
|
||||
|
||||
def process(self, instance):
|
||||
product_type = instance.data["productType"]
|
||||
product_name = instance.data["productName"]
|
||||
host_name = instance.context.data["hostName"]
|
||||
project_name = instance.context.data["projectName"]
|
||||
project_settings = instance.context.data["project_settings"]
|
||||
anatomy = instance.context.data["anatomy"]
|
||||
task = instance.data["anatomyData"].get("task", {})
|
||||
|
||||
transient_tml, is_persistent = get_custom_staging_dir_info(
|
||||
project_name,
|
||||
host_name,
|
||||
product_type,
|
||||
product_name,
|
||||
task.get("name"),
|
||||
task.get("type"),
|
||||
project_settings=project_settings,
|
||||
anatomy=anatomy,
|
||||
log=self.log)
|
||||
|
||||
if transient_tml:
|
||||
anatomy_data = copy.deepcopy(instance.data["anatomyData"])
|
||||
anatomy_data["root"] = anatomy.roots
|
||||
scene_name = instance.context.data.get("currentFile")
|
||||
if scene_name:
|
||||
anatomy_data["scene_name"] = os.path.basename(scene_name)
|
||||
transient_dir = transient_tml.format(**anatomy_data)
|
||||
instance.data["stagingDir"] = transient_dir
|
||||
|
||||
instance.data["stagingDir_persistent"] = is_persistent
|
||||
result_str = "Adding '{}' as".format(transient_dir)
|
||||
else:
|
||||
result_str = "Not adding"
|
||||
|
||||
self.log.debug("{} custom staging dir for instance with '{}'".format(
|
||||
result_str, product_type
|
||||
))
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.lib import get_ayon_username
|
||||
from ayon_core.pipeline.publish import FARM_JOB_ENV_DATA_KEY
|
||||
|
||||
|
||||
class CollectCoreJobEnvVars(pyblish.api.ContextPlugin):
|
||||
"""Collect set of environment variables to submit with deadline jobs"""
|
||||
order = pyblish.api.CollectorOrder - 0.45
|
||||
label = "AYON core Farm Environment Variables"
|
||||
targets = ["local"]
|
||||
|
||||
def process(self, context):
|
||||
env = context.data.setdefault(FARM_JOB_ENV_DATA_KEY, {})
|
||||
|
||||
# Disable colored logs on farm
|
||||
for key, value in (
|
||||
("AYON_LOG_NO_COLORS", "1"),
|
||||
("AYON_PROJECT_NAME", context.data["projectName"]),
|
||||
("AYON_FOLDER_PATH", context.data.get("folderPath")),
|
||||
("AYON_TASK_NAME", context.data.get("task")),
|
||||
# NOTE we should use 'context.data["user"]' but that has higher
|
||||
# order.
|
||||
("AYON_USERNAME", get_ayon_username()),
|
||||
):
|
||||
if value:
|
||||
self.log.debug(f"Setting job env: {key}: {value}")
|
||||
env[key] = value
|
||||
|
||||
for key in [
|
||||
"AYON_BUNDLE_NAME",
|
||||
"AYON_DEFAULT_SETTINGS_VARIANT",
|
||||
"AYON_IN_TESTS",
|
||||
# NOTE Not sure why workdir is needed?
|
||||
"AYON_WORKDIR",
|
||||
]:
|
||||
value = os.getenv(key)
|
||||
if value:
|
||||
self.log.debug(f"Setting job env: {key}: {value}")
|
||||
env[key] = value
|
||||
|
||||
|
|
@ -13,8 +13,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
|
|||
|
||||
label = "Collect Hierarchy"
|
||||
order = pyblish.api.CollectorOrder - 0.076
|
||||
families = ["shot"]
|
||||
hosts = ["resolve", "hiero", "flame"]
|
||||
hosts = ["resolve", "hiero", "flame", "traypublisher"]
|
||||
|
||||
def process(self, context):
|
||||
project_name = context.data["projectName"]
|
||||
|
|
@ -32,36 +31,50 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
|
|||
product_type = instance.data["productType"]
|
||||
families = instance.data["families"]
|
||||
|
||||
# exclude other families then self.families with intersection
|
||||
if not set(self.families).intersection(
|
||||
set(families + [product_type])
|
||||
):
|
||||
# exclude other families then "shot" with intersection
|
||||
if "shot" not in (families + [product_type]):
|
||||
self.log.debug("Skipping not a shot: {}".format(families))
|
||||
continue
|
||||
|
||||
# exclude if not masterLayer True
|
||||
# Skip if is not a hero track
|
||||
if not instance.data.get("heroTrack"):
|
||||
self.log.debug("Skipping not a shot from hero track")
|
||||
continue
|
||||
|
||||
shot_data = {
|
||||
"entity_type": "folder",
|
||||
# WARNING Default folder type is hardcoded
|
||||
# suppose that all instances are Shots
|
||||
"folder_type": "Shot",
|
||||
# WARNING unless overwritten, default folder type is hardcoded
|
||||
# to shot
|
||||
"folder_type": instance.data.get("folder_type") or "Shot",
|
||||
"tasks": instance.data.get("tasks") or {},
|
||||
"comments": instance.data.get("comments", []),
|
||||
"attributes": {
|
||||
"handleStart": instance.data["handleStart"],
|
||||
"handleEnd": instance.data["handleEnd"],
|
||||
"frameStart": instance.data["frameStart"],
|
||||
"frameEnd": instance.data["frameEnd"],
|
||||
"clipIn": instance.data["clipIn"],
|
||||
"clipOut": instance.data["clipOut"],
|
||||
"fps": instance.data["fps"],
|
||||
"resolutionWidth": instance.data["resolutionWidth"],
|
||||
"resolutionHeight": instance.data["resolutionHeight"],
|
||||
"pixelAspect": instance.data["pixelAspect"],
|
||||
},
|
||||
}
|
||||
|
||||
shot_data["attributes"] = {}
|
||||
SHOT_ATTRS = (
|
||||
"handleStart",
|
||||
"handleEnd",
|
||||
"frameStart",
|
||||
"frameEnd",
|
||||
"clipIn",
|
||||
"clipOut",
|
||||
"fps",
|
||||
"resolutionWidth",
|
||||
"resolutionHeight",
|
||||
"pixelAspect",
|
||||
)
|
||||
for shot_attr in SHOT_ATTRS:
|
||||
attr_value = instance.data.get(shot_attr)
|
||||
if attr_value is None:
|
||||
# Shot attribute might not be defined (e.g. CSV ingest)
|
||||
self.log.debug(
|
||||
"%s shot attribute is not defined for instance.",
|
||||
shot_attr
|
||||
)
|
||||
continue
|
||||
|
||||
shot_data["attributes"][shot_attr] = attr_value
|
||||
|
||||
# Split by '/' for AYON where asset is a path
|
||||
name = instance.data["folderPath"].split("/")[-1]
|
||||
actual = {name: shot_data}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
"""
|
||||
Requires:
|
||||
anatomy
|
||||
|
||||
|
||||
Provides:
|
||||
instance.data -> stagingDir (folder path)
|
||||
-> stagingDir_persistent (bool)
|
||||
"""
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline.publish import get_instance_staging_dir
|
||||
|
||||
|
||||
class CollectManagedStagingDir(pyblish.api.InstancePlugin):
|
||||
"""Apply matching Staging Dir profile to a instance.
|
||||
|
||||
Apply Staging dir via profiles could be useful in specific use cases
|
||||
where is desirable to have temporary renders in specific,
|
||||
persistent folders, could be on disks optimized for speed for example.
|
||||
|
||||
It is studio's responsibility to clean up obsolete folders with data.
|
||||
|
||||
Location of the folder is configured in:
|
||||
`ayon+anatomy://_/templates/staging`.
|
||||
|
||||
Which family/task type/subset is applicable is configured in:
|
||||
`ayon+settings://core/tools/publish/custom_staging_dir_profiles`
|
||||
"""
|
||||
|
||||
label = "Collect Managed Staging Directory"
|
||||
order = pyblish.api.CollectorOrder + 0.4990
|
||||
|
||||
def process(self, instance):
|
||||
""" Collect the staging data and stores it to the instance.
|
||||
|
||||
Args:
|
||||
instance (object): The instance to inspect.
|
||||
"""
|
||||
staging_dir_path = get_instance_staging_dir(instance)
|
||||
persistance = instance.data.get("stagingDir_persistent", False)
|
||||
|
||||
self.log.info((
|
||||
f"Instance staging dir was set to `{staging_dir_path}` "
|
||||
f"and persistence is set to `{persistance}`"
|
||||
))
|
||||
|
|
@ -29,6 +29,10 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
|
|||
otio_range_with_handles
|
||||
)
|
||||
|
||||
if not instance.data.get("otioClip"):
|
||||
self.log.debug("Skipping collect OTIO frame range.")
|
||||
return
|
||||
|
||||
# get basic variables
|
||||
otio_clip = instance.data["otioClip"]
|
||||
workfile_start = instance.data["workfileFrameStart"]
|
||||
|
|
|
|||
|
|
@ -95,9 +95,42 @@ class CollectOtioReview(pyblish.api.InstancePlugin):
|
|||
instance.data["label"] = label + " (review)"
|
||||
instance.data["families"] += ["review", "ftrack"]
|
||||
instance.data["otioReviewClips"] = otio_review_clips
|
||||
|
||||
self.log.info(
|
||||
"Creating review track: {}".format(otio_review_clips))
|
||||
|
||||
# get colorspace from metadata if available
|
||||
# get metadata from first clip with media reference
|
||||
r_otio_cl = next(
|
||||
(
|
||||
clip
|
||||
for clip in otio_review_clips
|
||||
if (
|
||||
isinstance(clip, otio.schema.Clip)
|
||||
and clip.media_reference
|
||||
)
|
||||
),
|
||||
None
|
||||
)
|
||||
if r_otio_cl is not None:
|
||||
media_ref = r_otio_cl.media_reference
|
||||
media_metadata = media_ref.metadata
|
||||
|
||||
# TODO: we might need some alternative method since
|
||||
# native OTIO exports do not support ayon metadata
|
||||
review_colorspace = media_metadata.get(
|
||||
"ayon.source.colorspace"
|
||||
)
|
||||
if review_colorspace is None:
|
||||
# Backwards compatibility for older scenes
|
||||
review_colorspace = media_metadata.get(
|
||||
"openpype.source.colourtransform"
|
||||
)
|
||||
if review_colorspace:
|
||||
instance.data["reviewColorspace"] = review_colorspace
|
||||
self.log.info(
|
||||
"Review colorspace: {}".format(review_colorspace))
|
||||
|
||||
self.log.debug(
|
||||
"_ instance.data: {}".format(pformat(instance.data)))
|
||||
self.log.debug(
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@ import os
|
|||
import clique
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.pipeline.publish import (
|
||||
get_publish_template_name
|
||||
)
|
||||
|
||||
|
||||
class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
|
||||
class CollectOtioSubsetResources(
|
||||
pyblish.api.InstancePlugin,
|
||||
publish.ColormanagedPyblishPluginMixin
|
||||
):
|
||||
"""Get Resources for a product version"""
|
||||
|
||||
label = "Collect OTIO Subset Resources"
|
||||
|
|
@ -145,6 +149,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
|
|||
|
||||
self.log.info(
|
||||
"frame_start-frame_end: {}-{}".format(frame_start, frame_end))
|
||||
review_repre = None
|
||||
|
||||
if is_sequence:
|
||||
# file sequence way
|
||||
|
|
@ -173,6 +178,11 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
|
|||
repre = self._create_representation(
|
||||
frame_start, frame_end, collection=collection)
|
||||
|
||||
if "review" in instance.data["families"]:
|
||||
review_repre = self._create_representation(
|
||||
frame_start, frame_end, collection=collection,
|
||||
delete=True, review=True)
|
||||
|
||||
else:
|
||||
_trim = False
|
||||
dirname, filename = os.path.split(media_ref.target_url)
|
||||
|
|
@ -187,12 +197,25 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
|
|||
repre = self._create_representation(
|
||||
frame_start, frame_end, file=filename, trim=_trim)
|
||||
|
||||
if "review" in instance.data["families"]:
|
||||
review_repre = self._create_representation(
|
||||
frame_start, frame_end,
|
||||
file=filename, delete=True, review=True)
|
||||
|
||||
instance.data["originalDirname"] = self.staging_dir
|
||||
|
||||
# add representation to instance data
|
||||
if repre:
|
||||
# add representation to instance data
|
||||
colorspace = instance.data.get("colorspace")
|
||||
# add colorspace data to representation
|
||||
self.set_representation_colorspace(
|
||||
repre, instance.context, colorspace)
|
||||
|
||||
instance.data["representations"].append(repre)
|
||||
self.log.debug(">>>>>>>> {}".format(repre))
|
||||
|
||||
# add review representation to instance data
|
||||
if review_repre:
|
||||
instance.data["representations"].append(review_repre)
|
||||
|
||||
self.log.debug(instance.data)
|
||||
|
||||
|
|
@ -213,7 +236,8 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
|
|||
representation_data = {
|
||||
"frameStart": start,
|
||||
"frameEnd": end,
|
||||
"stagingDir": self.staging_dir
|
||||
"stagingDir": self.staging_dir,
|
||||
"tags": [],
|
||||
}
|
||||
|
||||
if kwargs.get("collection"):
|
||||
|
|
@ -239,8 +263,10 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
|
|||
"frameEnd": end,
|
||||
})
|
||||
|
||||
if kwargs.get("trim") is True:
|
||||
representation_data["tags"] = ["trim"]
|
||||
for tag_name in ("trim", "delete", "review"):
|
||||
if kwargs.get(tag_name) is True:
|
||||
representation_data["tags"].append(tag_name)
|
||||
|
||||
return representation_data
|
||||
|
||||
def get_template_name(self, instance):
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ import clique
|
|||
import pyblish.api
|
||||
|
||||
from ayon_core import resources, AYON_CORE_ROOT
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.pipeline import (
|
||||
publish,
|
||||
get_temp_dir
|
||||
)
|
||||
from ayon_core.lib import (
|
||||
run_ayon_launcher_process,
|
||||
|
||||
get_transcode_temp_directory,
|
||||
convert_input_paths_for_ffmpeg,
|
||||
should_convert_for_ffmpeg
|
||||
)
|
||||
|
|
@ -250,7 +252,10 @@ class ExtractBurnin(publish.Extractor):
|
|||
# - change staging dir of source representation
|
||||
# - must be set back after output definitions processing
|
||||
if do_convert:
|
||||
new_staging_dir = get_transcode_temp_directory()
|
||||
new_staging_dir = get_temp_dir(
|
||||
project_name=instance.context.data["projectName"],
|
||||
use_local_temp=True,
|
||||
)
|
||||
repre["stagingDir"] = new_staging_dir
|
||||
|
||||
convert_input_paths_for_ffmpeg(
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ import copy
|
|||
import clique
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.pipeline import (
|
||||
publish,
|
||||
get_temp_dir
|
||||
)
|
||||
from ayon_core.lib import (
|
||||
|
||||
is_oiio_supported,
|
||||
)
|
||||
|
||||
from ayon_core.lib.transcoding import (
|
||||
convert_colorspace,
|
||||
get_transcode_temp_directory,
|
||||
)
|
||||
|
||||
from ayon_core.lib.profiles_filtering import filter_profiles
|
||||
|
|
@ -104,7 +104,10 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
new_repre = copy.deepcopy(repre)
|
||||
|
||||
original_staging_dir = new_repre["stagingDir"]
|
||||
new_staging_dir = get_transcode_temp_directory()
|
||||
new_staging_dir = get_temp_dir(
|
||||
project_name=instance.context.data["projectName"],
|
||||
use_local_temp=True,
|
||||
)
|
||||
new_repre["stagingDir"] = new_staging_dir
|
||||
|
||||
if isinstance(new_repre["files"], list):
|
||||
|
|
@ -154,12 +157,15 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
|
||||
files_to_convert = self._translate_to_sequence(
|
||||
files_to_convert)
|
||||
self.log.debug("Files to convert: {}".format(files_to_convert))
|
||||
for file_name in files_to_convert:
|
||||
self.log.debug("Transcoding file: `{}`".format(file_name))
|
||||
input_path = os.path.join(original_staging_dir,
|
||||
file_name)
|
||||
output_path = self._get_output_file_path(input_path,
|
||||
new_staging_dir,
|
||||
output_extension)
|
||||
|
||||
convert_colorspace(
|
||||
input_path,
|
||||
output_path,
|
||||
|
|
@ -263,7 +269,7 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
(list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr]
|
||||
"""
|
||||
pattern = [clique.PATTERNS["frames"]]
|
||||
collections, remainder = clique.assemble(
|
||||
collections, _ = clique.assemble(
|
||||
files_to_convert, patterns=pattern,
|
||||
assume_padded_when_ambiguous=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ class ExtractColorspaceData(publish.Extractor,
|
|||
# get colorspace settings
|
||||
context = instance.context
|
||||
|
||||
# colorspace name could be kept in instance.data
|
||||
colorspace = instance.data.get("colorspace")
|
||||
|
||||
# loop representations
|
||||
for representation in representations:
|
||||
# skip if colorspaceData is already at representation
|
||||
|
|
@ -44,5 +47,4 @@ class ExtractColorspaceData(publish.Extractor,
|
|||
continue
|
||||
|
||||
self.set_representation_colorspace(
|
||||
representation, context
|
||||
)
|
||||
representation, context, colorspace)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin):
|
|||
|
||||
order = pyblish.api.ExtractorOrder - 0.01
|
||||
label = "Extract Hierarchy To AYON"
|
||||
families = ["clip", "shot"]
|
||||
|
||||
def process(self, context):
|
||||
if not context.data.get("hierarchyContext"):
|
||||
|
|
|
|||
|
|
@ -71,20 +71,18 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
name = inst.data["folderPath"]
|
||||
|
||||
recycling_file = [f for f in created_files if name in f]
|
||||
|
||||
# frameranges
|
||||
timeline_in_h = inst.data["clipInH"]
|
||||
timeline_out_h = inst.data["clipOutH"]
|
||||
fps = inst.data["fps"]
|
||||
|
||||
# create duration
|
||||
duration = (timeline_out_h - timeline_in_h) + 1
|
||||
audio_clip = inst.data["otioClip"]
|
||||
audio_range = audio_clip.range_in_parent()
|
||||
duration = audio_range.duration.to_frames()
|
||||
|
||||
# ffmpeg generate new file only if doesn't exists already
|
||||
if not recycling_file:
|
||||
# convert to seconds
|
||||
start_sec = float(timeline_in_h / fps)
|
||||
duration_sec = float(duration / fps)
|
||||
parent_track = audio_clip.parent()
|
||||
parent_track_start = parent_track.range_in_parent().start_time
|
||||
relative_start_time = (
|
||||
audio_range.start_time - parent_track_start)
|
||||
start_sec = relative_start_time.to_seconds()
|
||||
duration_sec = audio_range.duration.to_seconds()
|
||||
|
||||
# temp audio file
|
||||
audio_fpath = self.create_temp_file(name)
|
||||
|
|
@ -163,34 +161,36 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
|
||||
output = []
|
||||
# go trough all audio tracks
|
||||
for otio_track in otio_timeline.tracks:
|
||||
if "Audio" not in otio_track.kind:
|
||||
continue
|
||||
for otio_track in otio_timeline.audio_tracks():
|
||||
self.log.debug("_" * 50)
|
||||
playhead = 0
|
||||
for otio_clip in otio_track:
|
||||
self.log.debug(otio_clip)
|
||||
if isinstance(otio_clip, otio.schema.Gap):
|
||||
playhead += otio_clip.source_range.duration.value
|
||||
elif isinstance(otio_clip, otio.schema.Clip):
|
||||
start = otio_clip.source_range.start_time.value
|
||||
duration = otio_clip.source_range.duration.value
|
||||
fps = otio_clip.source_range.start_time.rate
|
||||
if (isinstance(otio_clip, otio.schema.Clip) and
|
||||
not otio_clip.media_reference.is_missing_reference):
|
||||
media_av_start = otio_clip.available_range().start_time
|
||||
clip_start = otio_clip.source_range.start_time
|
||||
fps = clip_start.rate
|
||||
conformed_av_start = media_av_start.rescaled_to(fps)
|
||||
# ffmpeg ignores embedded tc
|
||||
start = clip_start - conformed_av_start
|
||||
duration = otio_clip.source_range.duration
|
||||
media_path = otio_clip.media_reference.target_url
|
||||
input = {
|
||||
"mediaPath": media_path,
|
||||
"delayFrame": playhead,
|
||||
"startFrame": start,
|
||||
"durationFrame": duration,
|
||||
"startFrame": start.to_frames(),
|
||||
"durationFrame": duration.to_frames(),
|
||||
"delayMilSec": int(float(playhead / fps) * 1000),
|
||||
"startSec": float(start / fps),
|
||||
"durationSec": float(duration / fps),
|
||||
"fps": fps
|
||||
"startSec": start.to_seconds(),
|
||||
"durationSec": duration.to_seconds(),
|
||||
"fps": float(fps)
|
||||
}
|
||||
if input not in output:
|
||||
output.append(input)
|
||||
self.log.debug("__ input: {}".format(input))
|
||||
playhead += otio_clip.source_range.duration.value
|
||||
|
||||
playhead += otio_clip.source_range.duration.value
|
||||
|
||||
return output
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ from ayon_core.lib import (
|
|||
from ayon_core.pipeline import publish
|
||||
|
||||
|
||||
class ExtractOTIOReview(publish.Extractor):
|
||||
class ExtractOTIOReview(
|
||||
publish.Extractor,
|
||||
publish.ColormanagedPyblishPluginMixin
|
||||
):
|
||||
"""
|
||||
Extract OTIO timeline into one concuted image sequence file.
|
||||
|
||||
|
|
@ -68,17 +71,24 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
# TODO: convert resulting image sequence to mp4
|
||||
|
||||
# get otio clip and other time info from instance clip
|
||||
otio_review_clips = instance.data.get("otioReviewClips")
|
||||
|
||||
if otio_review_clips is None:
|
||||
self.log.info(f"Instance `{instance}` has no otioReviewClips")
|
||||
return
|
||||
|
||||
# TODO: what if handles are different in `versionData`?
|
||||
handle_start = instance.data["handleStart"]
|
||||
handle_end = instance.data["handleEnd"]
|
||||
otio_review_clips = instance.data["otioReviewClips"]
|
||||
|
||||
# add plugin wide attributes
|
||||
self.representation_files = []
|
||||
self.used_frames = []
|
||||
self.workfile_start = int(instance.data.get(
|
||||
"workfileFrameStart", 1001)) - handle_start
|
||||
self.padding = len(str(self.workfile_start))
|
||||
# NOTE: padding has to be converted from
|
||||
# end frame since start could be lower then 1000
|
||||
self.padding = len(str(instance.data.get("frameEnd", 1001)))
|
||||
self.used_frames.append(self.workfile_start)
|
||||
self.to_width = instance.data.get(
|
||||
"resolutionWidth") or self.to_width
|
||||
|
|
@ -86,8 +96,10 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
"resolutionHeight") or self.to_height
|
||||
|
||||
# skip instance if no reviewable data available
|
||||
if (not isinstance(otio_review_clips[0], otio.schema.Clip)) \
|
||||
and (len(otio_review_clips) == 1):
|
||||
if (
|
||||
not isinstance(otio_review_clips[0], otio.schema.Clip)
|
||||
and len(otio_review_clips) == 1
|
||||
):
|
||||
self.log.warning(
|
||||
"Instance `{}` has nothing to process".format(instance))
|
||||
return
|
||||
|
|
@ -119,26 +131,33 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
res_data[key] = value
|
||||
break
|
||||
|
||||
self.to_width, self.to_height = res_data["width"], res_data["height"]
|
||||
self.log.debug("> self.to_width x self.to_height: {} x {}".format(
|
||||
self.to_width, self.to_height
|
||||
))
|
||||
self.to_width, self.to_height = (
|
||||
res_data["width"], res_data["height"]
|
||||
)
|
||||
self.log.debug(
|
||||
"> self.to_width x self.to_height:"
|
||||
f" {self.to_width} x {self.to_height}"
|
||||
)
|
||||
|
||||
available_range = r_otio_cl.available_range()
|
||||
available_range_start_frame = (
|
||||
available_range.start_time.to_frames()
|
||||
)
|
||||
processing_range = None
|
||||
self.actual_fps = available_range.duration.rate
|
||||
start = src_range.start_time.rescaled_to(self.actual_fps)
|
||||
duration = src_range.duration.rescaled_to(self.actual_fps)
|
||||
src_frame_start = src_range.start_time.to_frames()
|
||||
|
||||
# Temporary.
|
||||
# Some AYON custom OTIO exporter were implemented with relative
|
||||
# source range for image sequence. Following code maintain
|
||||
# backward-compatibility by adjusting available range
|
||||
# Some AYON custom OTIO exporter were implemented with
|
||||
# relative source range for image sequence. Following code
|
||||
# maintain backward-compatibility by adjusting available range
|
||||
# while we are updating those.
|
||||
if (
|
||||
is_clip_from_media_sequence(r_otio_cl)
|
||||
and available_range.start_time.to_frames() == media_ref.start_frame
|
||||
and src_range.start_time.to_frames() < media_ref.start_frame
|
||||
and available_range_start_frame == media_ref.start_frame
|
||||
and src_frame_start < media_ref.start_frame
|
||||
):
|
||||
available_range = otio.opentime.TimeRange(
|
||||
otio.opentime.RationalTime(0, rate=self.actual_fps),
|
||||
|
|
@ -168,7 +187,7 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
start -= clip_handle_start
|
||||
duration += clip_handle_start
|
||||
elif len(otio_review_clips) > 1 \
|
||||
and (index == len(otio_review_clips) - 1):
|
||||
and (index == len(otio_review_clips) - 1):
|
||||
# more clips | last clip reframing with handle
|
||||
duration += clip_handle_end
|
||||
elif len(otio_review_clips) == 1:
|
||||
|
|
@ -236,7 +255,8 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
# Extraction via FFmpeg.
|
||||
else:
|
||||
path = media_ref.target_url
|
||||
# Set extract range from 0 (FFmpeg ignores embedded timecode).
|
||||
# Set extract range from 0 (FFmpeg ignores
|
||||
# embedded timecode).
|
||||
extract_range = otio.opentime.TimeRange(
|
||||
otio.opentime.RationalTime(
|
||||
(
|
||||
|
|
@ -263,6 +283,13 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
|
||||
# creating and registering representation
|
||||
representation = self._create_representation(start, duration)
|
||||
|
||||
# add colorspace data to representation
|
||||
if colorspace := instance.data.get("reviewColorspace"):
|
||||
self.set_representation_colorspace(
|
||||
representation, instance.context, colorspace
|
||||
)
|
||||
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info("Adding representation: {}".format(representation))
|
||||
|
||||
|
|
@ -397,7 +424,8 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
to defined image sequence format.
|
||||
|
||||
Args:
|
||||
sequence (list): input dir path string, collection object, fps in list
|
||||
sequence (list): input dir path string, collection object,
|
||||
fps in list.
|
||||
video (list)[optional]: video_path string, otio_range in list
|
||||
gap (int)[optional]: gap duration
|
||||
end_offset (int)[optional]: offset gap frame start in frames
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ from ayon_core.lib.transcoding import (
|
|||
should_convert_for_ffmpeg,
|
||||
get_review_layer_name,
|
||||
convert_input_paths_for_ffmpeg,
|
||||
get_transcode_temp_directory,
|
||||
)
|
||||
from ayon_core.pipeline import get_temp_dir
|
||||
from ayon_core.pipeline.publish import (
|
||||
KnownPublishError,
|
||||
get_publish_instance_label,
|
||||
|
|
@ -310,7 +310,10 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
# - change staging dir of source representation
|
||||
# - must be set back after output definitions processing
|
||||
if do_convert:
|
||||
new_staging_dir = get_transcode_temp_directory()
|
||||
new_staging_dir = get_temp_dir(
|
||||
project_name=instance.context.data["projectName"],
|
||||
use_local_temp=True,
|
||||
)
|
||||
repre["stagingDir"] = new_staging_dir
|
||||
|
||||
convert_input_paths_for_ffmpeg(
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
"substancepainter",
|
||||
"nuke",
|
||||
"aftereffects",
|
||||
"unreal"
|
||||
"unreal",
|
||||
"houdini"
|
||||
]
|
||||
enabled = False
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ class ValidateProductUniqueness(pyblish.api.ContextPlugin):
|
|||
"""Validate all product names are unique.
|
||||
|
||||
This only validates whether the instances currently set to publish from
|
||||
the workfile overlap one another for the folder + product they are publishing
|
||||
to.
|
||||
the workfile overlap one another for the folder + product they are
|
||||
publishing to.
|
||||
|
||||
This does not perform any check against existing publishes in the database
|
||||
since it is allowed to publish into existing products resulting in
|
||||
|
|
@ -72,8 +72,10 @@ class ValidateProductUniqueness(pyblish.api.ContextPlugin):
|
|||
# All is ok
|
||||
return
|
||||
|
||||
msg = ("Instance product names {} are not unique. ".format(non_unique) +
|
||||
"Please remove or rename duplicates.")
|
||||
msg = (
|
||||
f"Instance product names {non_unique} are not unique."
|
||||
" Please remove or rename duplicates."
|
||||
)
|
||||
formatting_data = {
|
||||
"non_unique": ",".join(non_unique)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,8 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
- Datatypes explanation:
|
||||
<color> string format must be supported by FFmpeg.
|
||||
Examples: "#000000", "0x000000", "black"
|
||||
<font> must be accesible by ffmpeg = name of registered Font in system or path to font file.
|
||||
<font> must be accesible by ffmpeg = name of registered Font in system
|
||||
or path to font file.
|
||||
Examples: "Arial", "C:/Windows/Fonts/arial.ttf"
|
||||
|
||||
- Possible keys:
|
||||
|
|
@ -87,17 +88,21 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
"bg_opacity" - Opacity of background (box around text) - <float, Range:0-1>
|
||||
"bg_color" - Background color - <color>
|
||||
"bg_padding" - Background padding in pixels - <int>
|
||||
"x_offset" - offsets burnin vertically by entered pixels from border - <int>
|
||||
"y_offset" - offsets burnin horizontally by entered pixels from border - <int>
|
||||
"x_offset" - offsets burnin vertically by entered pixels
|
||||
from border - <int>
|
||||
"y_offset" - offsets burnin horizontally by entered pixels
|
||||
from border - <int>
|
||||
- x_offset & y_offset should be set at least to same value as bg_padding!!
|
||||
"font" - Font Family for text - <font>
|
||||
"font_size" - Font size in pixels - <int>
|
||||
"font_color" - Color of text - <color>
|
||||
"frame_offset" - Default start frame - <int>
|
||||
- required IF start frame is not set when using frames or timecode burnins
|
||||
- required IF start frame is not set when using frames
|
||||
or timecode burnins
|
||||
|
||||
On initializing class can be set General options through "options_init" arg.
|
||||
General can be overridden when adding burnin
|
||||
On initializing class can be set General options through
|
||||
"options_init" arg.
|
||||
General options can be overridden when adding burnin.
|
||||
|
||||
'''
|
||||
TOP_CENTERED = ffmpeg_burnins.TOP_CENTERED
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ def get_current_project_settings():
|
|||
project_name = os.environ.get("AYON_PROJECT_NAME")
|
||||
if not project_name:
|
||||
raise ValueError(
|
||||
"Missing context project in environemt variable `AYON_PROJECT_NAME`."
|
||||
"Missing context project in environment"
|
||||
" variable `AYON_PROJECT_NAME`."
|
||||
)
|
||||
return get_project_settings(project_name)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,11 @@
|
|||
"icon-alert-tools": "#AA5050",
|
||||
"icon-entity-default": "#bfccd6",
|
||||
"icon-entity-disabled": "#808080",
|
||||
|
||||
"font-entity-deprecated": "#666666",
|
||||
|
||||
"font-overridden": "#91CDFC",
|
||||
|
||||
"overlay-messages": {
|
||||
"close-btn": "#D3D8DE",
|
||||
"bg-success": "#458056",
|
||||
|
|
|
|||
|
|
@ -1585,6 +1585,10 @@ CreateNextPageOverlay {
|
|||
}
|
||||
|
||||
/* Attribute Definition widgets */
|
||||
AttributeDefinitionsLabel[overridden="1"] {
|
||||
color: {color:font-overridden};
|
||||
}
|
||||
|
||||
AttributeDefinitionsWidget QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit {
|
||||
padding: 1px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from .widgets import (
|
||||
create_widget_for_attr_def,
|
||||
AttributeDefinitionsWidget,
|
||||
AttributeDefinitionsLabel,
|
||||
)
|
||||
|
||||
from .dialog import (
|
||||
|
|
@ -11,6 +12,7 @@ from .dialog import (
|
|||
__all__ = (
|
||||
"create_widget_for_attr_def",
|
||||
"AttributeDefinitionsWidget",
|
||||
"AttributeDefinitionsLabel",
|
||||
|
||||
"AttributeDefinitionsDialog",
|
||||
)
|
||||
|
|
|
|||
1
client/ayon_core/tools/attribute_defs/_constants.py
Normal file
1
client/ayon_core/tools/attribute_defs/_constants.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
REVERT_TO_DEFAULT_LABEL = "Revert to default"
|
||||
|
|
@ -17,6 +17,8 @@ from ayon_core.tools.utils import (
|
|||
PixmapLabel
|
||||
)
|
||||
|
||||
from ._constants import REVERT_TO_DEFAULT_LABEL
|
||||
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
ITEM_LABEL_ROLE = QtCore.Qt.UserRole + 2
|
||||
ITEM_ICON_ROLE = QtCore.Qt.UserRole + 3
|
||||
|
|
@ -252,7 +254,7 @@ class FilesModel(QtGui.QStandardItemModel):
|
|||
"""Make sure that removed items are removed from items mapping.
|
||||
|
||||
Connected with '_on_insert'. When user drag item and drop it to same
|
||||
view the item is actually removed and creted again but it happens in
|
||||
view the item is actually removed and created again but it happens in
|
||||
inner calls of Qt.
|
||||
"""
|
||||
|
||||
|
|
@ -598,7 +600,7 @@ class FilesView(QtWidgets.QListView):
|
|||
"""View showing instances and their groups."""
|
||||
|
||||
remove_requested = QtCore.Signal()
|
||||
context_menu_requested = QtCore.Signal(QtCore.QPoint)
|
||||
context_menu_requested = QtCore.Signal(QtCore.QPoint, bool)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FilesView, self).__init__(*args, **kwargs)
|
||||
|
|
@ -690,9 +692,8 @@ class FilesView(QtWidgets.QListView):
|
|||
|
||||
def _on_context_menu_request(self, pos):
|
||||
index = self.indexAt(pos)
|
||||
if index.isValid():
|
||||
point = self.viewport().mapToGlobal(pos)
|
||||
self.context_menu_requested.emit(point)
|
||||
point = self.viewport().mapToGlobal(pos)
|
||||
self.context_menu_requested.emit(point, index.isValid())
|
||||
|
||||
def _on_selection_change(self):
|
||||
self._remove_btn.setEnabled(self.has_selected_item_ids())
|
||||
|
|
@ -721,27 +722,34 @@ class FilesView(QtWidgets.QListView):
|
|||
|
||||
class FilesWidget(QtWidgets.QFrame):
|
||||
value_changed = QtCore.Signal()
|
||||
revert_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, single_item, allow_sequences, extensions_label, parent):
|
||||
super(FilesWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
wrapper_widget = QtWidgets.QWidget(self)
|
||||
|
||||
empty_widget = DropEmpty(
|
||||
single_item, allow_sequences, extensions_label, self
|
||||
single_item, allow_sequences, extensions_label, wrapper_widget
|
||||
)
|
||||
|
||||
files_model = FilesModel(single_item, allow_sequences)
|
||||
files_proxy_model = FilesProxyModel()
|
||||
files_proxy_model.setSourceModel(files_model)
|
||||
files_view = FilesView(self)
|
||||
files_view = FilesView(wrapper_widget)
|
||||
files_view.setModel(files_proxy_model)
|
||||
|
||||
layout = QtWidgets.QStackedLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setStackingMode(QtWidgets.QStackedLayout.StackAll)
|
||||
layout.addWidget(empty_widget)
|
||||
layout.addWidget(files_view)
|
||||
layout.setCurrentWidget(empty_widget)
|
||||
wrapper_layout = QtWidgets.QStackedLayout(wrapper_widget)
|
||||
wrapper_layout.setContentsMargins(0, 0, 0, 0)
|
||||
wrapper_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll)
|
||||
wrapper_layout.addWidget(empty_widget)
|
||||
wrapper_layout.addWidget(files_view)
|
||||
wrapper_layout.setCurrentWidget(empty_widget)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(wrapper_widget, 1)
|
||||
|
||||
files_proxy_model.rowsInserted.connect(self._on_rows_inserted)
|
||||
files_proxy_model.rowsRemoved.connect(self._on_rows_removed)
|
||||
|
|
@ -761,7 +769,11 @@ class FilesWidget(QtWidgets.QFrame):
|
|||
|
||||
self._widgets_by_id = {}
|
||||
|
||||
self._layout = layout
|
||||
self._wrapper_widget = wrapper_widget
|
||||
self._wrapper_layout = wrapper_layout
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
def _set_multivalue(self, multivalue):
|
||||
if self._multivalue is multivalue:
|
||||
|
|
@ -770,7 +782,7 @@ class FilesWidget(QtWidgets.QFrame):
|
|||
self._files_view.set_multivalue(multivalue)
|
||||
self._files_model.set_multivalue(multivalue)
|
||||
self._files_proxy_model.set_multivalue(multivalue)
|
||||
self.setEnabled(not multivalue)
|
||||
self._wrapper_widget.setEnabled(not multivalue)
|
||||
|
||||
def set_value(self, value, multivalue):
|
||||
self._in_set_value = True
|
||||
|
|
@ -829,7 +841,7 @@ class FilesWidget(QtWidgets.QFrame):
|
|||
self._multivalue
|
||||
)
|
||||
widget.context_menu_requested.connect(
|
||||
self._on_context_menu_requested
|
||||
self._on_item_context_menu_request
|
||||
)
|
||||
self._files_view.setIndexWidget(index, widget)
|
||||
self._files_proxy_model.setData(
|
||||
|
|
@ -847,7 +859,7 @@ class FilesWidget(QtWidgets.QFrame):
|
|||
for row in range(self._files_proxy_model.rowCount()):
|
||||
index = self._files_proxy_model.index(row, 0)
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
available_item_ids.add(index.data(ITEM_ID_ROLE))
|
||||
available_item_ids.add(item_id)
|
||||
|
||||
widget_ids = set(self._widgets_by_id.keys())
|
||||
for item_id in available_item_ids:
|
||||
|
|
@ -888,22 +900,31 @@ class FilesWidget(QtWidgets.QFrame):
|
|||
if items_to_delete:
|
||||
self._remove_item_by_ids(items_to_delete)
|
||||
|
||||
def _on_context_menu_requested(self, pos):
|
||||
if self._multivalue:
|
||||
return
|
||||
def _on_context_menu(self, pos):
|
||||
self._on_context_menu_requested(pos, False)
|
||||
|
||||
def _on_context_menu_requested(self, pos, valid_index):
|
||||
menu = QtWidgets.QMenu(self._files_view)
|
||||
if valid_index and not self._multivalue:
|
||||
if self._files_view.has_selected_sequence():
|
||||
split_action = QtWidgets.QAction("Split sequence", menu)
|
||||
split_action.triggered.connect(self._on_split_request)
|
||||
menu.addAction(split_action)
|
||||
|
||||
if self._files_view.has_selected_sequence():
|
||||
split_action = QtWidgets.QAction("Split sequence", menu)
|
||||
split_action.triggered.connect(self._on_split_request)
|
||||
menu.addAction(split_action)
|
||||
remove_action = QtWidgets.QAction("Remove", menu)
|
||||
remove_action.triggered.connect(self._on_remove_requested)
|
||||
menu.addAction(remove_action)
|
||||
|
||||
remove_action = QtWidgets.QAction("Remove", menu)
|
||||
remove_action.triggered.connect(self._on_remove_requested)
|
||||
menu.addAction(remove_action)
|
||||
if not valid_index:
|
||||
revert_action = QtWidgets.QAction(REVERT_TO_DEFAULT_LABEL, menu)
|
||||
revert_action.triggered.connect(self.revert_requested)
|
||||
menu.addAction(revert_action)
|
||||
|
||||
menu.popup(pos)
|
||||
if menu.actions():
|
||||
menu.popup(pos)
|
||||
|
||||
def _on_item_context_menu_request(self, pos):
|
||||
self._on_context_menu_requested(pos, True)
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
if self._multivalue:
|
||||
|
|
@ -1011,5 +1032,5 @@ class FilesWidget(QtWidgets.QFrame):
|
|||
current_widget = self._files_view
|
||||
else:
|
||||
current_widget = self._empty_widget
|
||||
self._layout.setCurrentWidget(current_widget)
|
||||
self._wrapper_layout.setCurrentWidget(current_widget)
|
||||
self._files_view.update_remove_btn_visibility()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import copy
|
||||
import typing
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
|
|
@ -20,14 +22,25 @@ from ayon_core.tools.utils import (
|
|||
FocusSpinBox,
|
||||
FocusDoubleSpinBox,
|
||||
MultiSelectionComboBox,
|
||||
set_style_property,
|
||||
)
|
||||
from ayon_core.tools.utils import NiceCheckbox
|
||||
|
||||
from ._constants import REVERT_TO_DEFAULT_LABEL
|
||||
from .files_widget import FilesWidget
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Union
|
||||
|
||||
def create_widget_for_attr_def(attr_def, parent=None):
|
||||
widget = _create_widget_for_attr_def(attr_def, parent)
|
||||
|
||||
def create_widget_for_attr_def(
|
||||
attr_def: AbstractAttrDef,
|
||||
parent: Optional[QtWidgets.QWidget] = None,
|
||||
handle_revert_to_default: Optional[bool] = True,
|
||||
):
|
||||
widget = _create_widget_for_attr_def(
|
||||
attr_def, parent, handle_revert_to_default
|
||||
)
|
||||
if not attr_def.visible:
|
||||
widget.setVisible(False)
|
||||
|
||||
|
|
@ -36,42 +49,96 @@ def create_widget_for_attr_def(attr_def, parent=None):
|
|||
return widget
|
||||
|
||||
|
||||
def _create_widget_for_attr_def(attr_def, parent=None):
|
||||
def _create_widget_for_attr_def(
|
||||
attr_def: AbstractAttrDef,
|
||||
parent: "Union[QtWidgets.QWidget, None]",
|
||||
handle_revert_to_default: bool,
|
||||
):
|
||||
if not isinstance(attr_def, AbstractAttrDef):
|
||||
raise TypeError("Unexpected type \"{}\" expected \"{}\"".format(
|
||||
str(type(attr_def)), AbstractAttrDef
|
||||
))
|
||||
|
||||
cls = None
|
||||
if isinstance(attr_def, NumberDef):
|
||||
return NumberAttrWidget(attr_def, parent)
|
||||
cls = NumberAttrWidget
|
||||
|
||||
if isinstance(attr_def, TextDef):
|
||||
return TextAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, TextDef):
|
||||
cls = TextAttrWidget
|
||||
|
||||
if isinstance(attr_def, EnumDef):
|
||||
return EnumAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, EnumDef):
|
||||
cls = EnumAttrWidget
|
||||
|
||||
if isinstance(attr_def, BoolDef):
|
||||
return BoolAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, BoolDef):
|
||||
cls = BoolAttrWidget
|
||||
|
||||
if isinstance(attr_def, UnknownDef):
|
||||
return UnknownAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, UnknownDef):
|
||||
cls = UnknownAttrWidget
|
||||
|
||||
if isinstance(attr_def, HiddenDef):
|
||||
return HiddenAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, HiddenDef):
|
||||
cls = HiddenAttrWidget
|
||||
|
||||
if isinstance(attr_def, FileDef):
|
||||
return FileAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, FileDef):
|
||||
cls = FileAttrWidget
|
||||
|
||||
if isinstance(attr_def, UISeparatorDef):
|
||||
return SeparatorAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, UISeparatorDef):
|
||||
cls = SeparatorAttrWidget
|
||||
|
||||
if isinstance(attr_def, UILabelDef):
|
||||
return LabelAttrWidget(attr_def, parent)
|
||||
elif isinstance(attr_def, UILabelDef):
|
||||
cls = LabelAttrWidget
|
||||
|
||||
raise ValueError("Unknown attribute definition \"{}\"".format(
|
||||
str(type(attr_def))
|
||||
))
|
||||
if cls is None:
|
||||
raise ValueError("Unknown attribute definition \"{}\"".format(
|
||||
str(type(attr_def))
|
||||
))
|
||||
|
||||
return cls(attr_def, parent, handle_revert_to_default)
|
||||
|
||||
|
||||
class AttributeDefinitionsLabel(QtWidgets.QLabel):
|
||||
"""Label related to value attribute definition.
|
||||
|
||||
Label is used to show attribute definition label and to show if value
|
||||
is overridden.
|
||||
|
||||
Label can be right-clicked to revert value to default.
|
||||
"""
|
||||
revert_to_default_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attr_id: str,
|
||||
label: str,
|
||||
parent: QtWidgets.QWidget,
|
||||
):
|
||||
super().__init__(label, parent)
|
||||
|
||||
self._attr_id = attr_id
|
||||
self._overridden = False
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
self.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
def set_overridden(self, overridden: bool):
|
||||
if self._overridden == overridden:
|
||||
return
|
||||
self._overridden = overridden
|
||||
set_style_property(
|
||||
self,
|
||||
"overridden",
|
||||
"1" if overridden else ""
|
||||
)
|
||||
|
||||
def _on_context_menu(self, point: QtCore.QPoint):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
action = QtWidgets.QAction(menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self._request_revert_to_default)
|
||||
menu.addAction(action)
|
||||
menu.exec_(self.mapToGlobal(point))
|
||||
|
||||
def _request_revert_to_default(self):
|
||||
self.revert_to_default_requested.emit(self._attr_id)
|
||||
|
||||
|
||||
class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
||||
|
|
@ -83,16 +150,18 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
|
||||
def __init__(self, attr_defs=None, parent=None):
|
||||
super(AttributeDefinitionsWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self._widgets = []
|
||||
self._widgets_by_id = {}
|
||||
self._labels_by_id = {}
|
||||
self._current_keys = set()
|
||||
|
||||
self.set_attr_defs(attr_defs)
|
||||
|
||||
def clear_attr_defs(self):
|
||||
"""Remove all existing widgets and reset layout if needed."""
|
||||
self._widgets = []
|
||||
self._widgets_by_id = {}
|
||||
self._labels_by_id = {}
|
||||
self._current_keys = set()
|
||||
|
||||
layout = self.layout()
|
||||
|
|
@ -133,7 +202,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
|
||||
self._current_keys.add(attr_def.key)
|
||||
widget = create_widget_for_attr_def(attr_def, self)
|
||||
self._widgets.append(widget)
|
||||
self._widgets_by_id[attr_def.id] = widget
|
||||
|
||||
if not attr_def.visible:
|
||||
continue
|
||||
|
|
@ -145,7 +214,13 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
col_num = 2 - expand_cols
|
||||
|
||||
if attr_def.is_value_def and attr_def.label:
|
||||
label_widget = QtWidgets.QLabel(attr_def.label, self)
|
||||
label_widget = AttributeDefinitionsLabel(
|
||||
attr_def.id, attr_def.label, self
|
||||
)
|
||||
label_widget.revert_to_default_requested.connect(
|
||||
self._on_revert_request
|
||||
)
|
||||
self._labels_by_id[attr_def.id] = label_widget
|
||||
tooltip = attr_def.tooltip
|
||||
if tooltip:
|
||||
label_widget.setToolTip(tooltip)
|
||||
|
|
@ -160,6 +235,9 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
if not attr_def.is_label_horizontal:
|
||||
row += 1
|
||||
|
||||
if attr_def.is_value_def:
|
||||
widget.value_changed.connect(self._on_value_change)
|
||||
|
||||
layout.addWidget(
|
||||
widget, row, col_num, 1, expand_cols
|
||||
)
|
||||
|
|
@ -168,7 +246,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
def set_value(self, value):
|
||||
new_value = copy.deepcopy(value)
|
||||
unused_keys = set(new_value.keys())
|
||||
for widget in self._widgets:
|
||||
for widget in self._widgets_by_id.values():
|
||||
attr_def = widget.attr_def
|
||||
if attr_def.key not in new_value:
|
||||
continue
|
||||
|
|
@ -181,22 +259,42 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
|
||||
def current_value(self):
|
||||
output = {}
|
||||
for widget in self._widgets:
|
||||
for widget in self._widgets_by_id.values():
|
||||
attr_def = widget.attr_def
|
||||
if not isinstance(attr_def, UIDef):
|
||||
output[attr_def.key] = widget.current_value()
|
||||
|
||||
return output
|
||||
|
||||
def _on_revert_request(self, attr_id):
|
||||
widget = self._widgets_by_id.get(attr_id)
|
||||
if widget is not None:
|
||||
widget.set_value(widget.attr_def.default)
|
||||
|
||||
def _on_value_change(self, value, attr_id):
|
||||
widget = self._widgets_by_id.get(attr_id)
|
||||
if widget is None:
|
||||
return
|
||||
label = self._labels_by_id.get(attr_id)
|
||||
if label is not None:
|
||||
label.set_overridden(value != widget.attr_def.default)
|
||||
|
||||
|
||||
class _BaseAttrDefWidget(QtWidgets.QWidget):
|
||||
# Type 'object' may not work with older PySide versions
|
||||
value_changed = QtCore.Signal(object, str)
|
||||
revert_to_default_requested = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, attr_def, parent):
|
||||
super(_BaseAttrDefWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self,
|
||||
attr_def: AbstractAttrDef,
|
||||
parent: "Union[QtWidgets.QWidget, None]",
|
||||
handle_revert_to_default: Optional[bool] = True,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.attr_def = attr_def
|
||||
self.attr_def: AbstractAttrDef = attr_def
|
||||
self._handle_revert_to_default: bool = handle_revert_to_default
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -205,6 +303,15 @@ class _BaseAttrDefWidget(QtWidgets.QWidget):
|
|||
|
||||
self._ui_init()
|
||||
|
||||
def revert_to_default_value(self):
|
||||
if not self.attr_def.is_value_def:
|
||||
return
|
||||
|
||||
if self._handle_revert_to_default:
|
||||
self.set_value(self.attr_def.default)
|
||||
else:
|
||||
self.revert_to_default_requested.emit(self.attr_def.id)
|
||||
|
||||
def _ui_init(self):
|
||||
raise NotImplementedError(
|
||||
"Method '_ui_init' is not implemented. {}".format(
|
||||
|
|
@ -255,7 +362,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit):
|
|||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, text, parent):
|
||||
super(ClickableLineEdit, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setText(text)
|
||||
self.setReadOnly(True)
|
||||
|
||||
|
|
@ -264,7 +371,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit):
|
|||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._mouse_pressed = True
|
||||
super(ClickableLineEdit, self).mousePressEvent(event)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._mouse_pressed:
|
||||
|
|
@ -272,7 +379,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit):
|
|||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
|
||||
super(ClickableLineEdit, self).mouseReleaseEvent(event)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class NumberAttrWidget(_BaseAttrDefWidget):
|
||||
|
|
@ -284,6 +391,9 @@ class NumberAttrWidget(_BaseAttrDefWidget):
|
|||
else:
|
||||
input_widget = FocusSpinBox(self)
|
||||
|
||||
# Override context menu event to add revert to default action
|
||||
input_widget.contextMenuEvent = self._input_widget_context_event
|
||||
|
||||
if self.attr_def.tooltip:
|
||||
input_widget.setToolTip(self.attr_def.tooltip)
|
||||
|
||||
|
|
@ -321,6 +431,16 @@ class NumberAttrWidget(_BaseAttrDefWidget):
|
|||
self._set_multiselection_visible(True)
|
||||
return False
|
||||
|
||||
def _input_widget_context_event(self, event):
|
||||
line_edit = self._input_widget.lineEdit()
|
||||
menu = line_edit.createStandardContextMenu()
|
||||
menu.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
action = QtWidgets.QAction(menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self.revert_to_default_value)
|
||||
menu.addAction(action)
|
||||
menu.popup(event.globalPos())
|
||||
|
||||
def current_value(self):
|
||||
return self._input_widget.value()
|
||||
|
||||
|
|
@ -386,6 +506,9 @@ class TextAttrWidget(_BaseAttrDefWidget):
|
|||
else:
|
||||
input_widget = QtWidgets.QLineEdit(self)
|
||||
|
||||
# Override context menu event to add revert to default action
|
||||
input_widget.contextMenuEvent = self._input_widget_context_event
|
||||
|
||||
if (
|
||||
self.attr_def.placeholder
|
||||
and hasattr(input_widget, "setPlaceholderText")
|
||||
|
|
@ -407,6 +530,15 @@ class TextAttrWidget(_BaseAttrDefWidget):
|
|||
|
||||
self.main_layout.addWidget(input_widget, 0)
|
||||
|
||||
def _input_widget_context_event(self, event):
|
||||
menu = self._input_widget.createStandardContextMenu()
|
||||
menu.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
action = QtWidgets.QAction(menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self.revert_to_default_value)
|
||||
menu.addAction(action)
|
||||
menu.popup(event.globalPos())
|
||||
|
||||
def _on_value_change(self):
|
||||
if self.multiline:
|
||||
new_value = self._input_widget.toPlainText()
|
||||
|
|
@ -459,6 +591,20 @@ class BoolAttrWidget(_BaseAttrDefWidget):
|
|||
self.main_layout.addWidget(input_widget, 0)
|
||||
self.main_layout.addStretch(1)
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
def _on_context_menu(self, pos):
|
||||
self._menu = QtWidgets.QMenu(self)
|
||||
|
||||
action = QtWidgets.QAction(self._menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self.revert_to_default_value)
|
||||
self._menu.addAction(action)
|
||||
|
||||
global_pos = self.mapToGlobal(pos)
|
||||
self._menu.exec_(global_pos)
|
||||
|
||||
def _on_value_change(self):
|
||||
new_value = self._input_widget.isChecked()
|
||||
self.value_changed.emit(new_value, self.attr_def.id)
|
||||
|
|
@ -487,7 +633,7 @@ class BoolAttrWidget(_BaseAttrDefWidget):
|
|||
class EnumAttrWidget(_BaseAttrDefWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._multivalue = False
|
||||
super(EnumAttrWidget, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def multiselection(self):
|
||||
|
|
@ -522,6 +668,20 @@ class EnumAttrWidget(_BaseAttrDefWidget):
|
|||
|
||||
self.main_layout.addWidget(input_widget, 0)
|
||||
|
||||
input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
input_widget.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
def _on_context_menu(self, pos):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
action = QtWidgets.QAction(menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self.revert_to_default_value)
|
||||
menu.addAction(action)
|
||||
|
||||
global_pos = self.mapToGlobal(pos)
|
||||
menu.exec_(global_pos)
|
||||
|
||||
def _on_value_change(self):
|
||||
new_value = self.current_value()
|
||||
if self._multivalue:
|
||||
|
|
@ -614,7 +774,7 @@ class HiddenAttrWidget(_BaseAttrDefWidget):
|
|||
def setVisible(self, visible):
|
||||
if visible:
|
||||
visible = False
|
||||
super(HiddenAttrWidget, self).setVisible(visible)
|
||||
super().setVisible(visible)
|
||||
|
||||
def current_value(self):
|
||||
if self._multivalue:
|
||||
|
|
@ -650,10 +810,25 @@ class FileAttrWidget(_BaseAttrDefWidget):
|
|||
|
||||
self.main_layout.addWidget(input_widget, 0)
|
||||
|
||||
input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
input_widget.customContextMenuRequested.connect(self._on_context_menu)
|
||||
input_widget.revert_requested.connect(self.revert_to_default_value)
|
||||
|
||||
def _on_value_change(self):
|
||||
new_value = self.current_value()
|
||||
self.value_changed.emit(new_value, self.attr_def.id)
|
||||
|
||||
def _on_context_menu(self, pos):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
action = QtWidgets.QAction(menu)
|
||||
action.setText(REVERT_TO_DEFAULT_LABEL)
|
||||
action.triggered.connect(self.revert_to_default_value)
|
||||
menu.addAction(action)
|
||||
|
||||
global_pos = self.mapToGlobal(pos)
|
||||
menu.exec_(global_pos)
|
||||
|
||||
def current_value(self):
|
||||
return self._input_widget.current_value()
|
||||
|
||||
|
|
|
|||
8
client/ayon_core/tools/console_interpreter/__init__.py
Normal file
8
client/ayon_core/tools/console_interpreter/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from .abstract import AbstractInterpreterController
|
||||
from .control import InterpreterController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"AbstractInterpreterController",
|
||||
"InterpreterController",
|
||||
)
|
||||
33
client/ayon_core/tools/console_interpreter/abstract.py
Normal file
33
client/ayon_core/tools/console_interpreter/abstract.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class TabItem:
|
||||
name: str
|
||||
code: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class InterpreterConfig:
|
||||
width: Optional[int]
|
||||
height: Optional[int]
|
||||
splitter_sizes: List[int] = field(default_factory=list)
|
||||
tabs: List[TabItem] = field(default_factory=list)
|
||||
|
||||
|
||||
class AbstractInterpreterController(ABC):
|
||||
@abstractmethod
|
||||
def get_config(self) -> InterpreterConfig:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_config(
|
||||
self,
|
||||
width: int,
|
||||
height: int,
|
||||
splitter_sizes: List[int],
|
||||
tabs: List[Dict[str, str]],
|
||||
):
|
||||
pass
|
||||
63
client/ayon_core/tools/console_interpreter/control.py
Normal file
63
client/ayon_core/tools/console_interpreter/control.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from typing import List, Dict
|
||||
|
||||
from ayon_core.lib import JSONSettingRegistry
|
||||
from ayon_core.lib.local_settings import get_launcher_local_dir
|
||||
|
||||
from .abstract import (
|
||||
AbstractInterpreterController,
|
||||
TabItem,
|
||||
InterpreterConfig,
|
||||
)
|
||||
|
||||
|
||||
class InterpreterController(AbstractInterpreterController):
|
||||
def __init__(self):
|
||||
self._registry = JSONSettingRegistry(
|
||||
"python_interpreter_tool",
|
||||
get_launcher_local_dir(),
|
||||
)
|
||||
|
||||
def get_config(self):
|
||||
width = None
|
||||
height = None
|
||||
splitter_sizes = []
|
||||
tabs = []
|
||||
try:
|
||||
width = self._registry.get_item("width")
|
||||
height = self._registry.get_item("height")
|
||||
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
||||
try:
|
||||
splitter_sizes = self._registry.get_item("splitter_sizes")
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
||||
try:
|
||||
tab_defs = self._registry.get_item("tabs") or []
|
||||
for tab_def in tab_defs:
|
||||
tab_name = tab_def.get("name")
|
||||
if not tab_name:
|
||||
continue
|
||||
code = tab_def.get("code") or ""
|
||||
tabs.append(TabItem(tab_name, code))
|
||||
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
||||
return InterpreterConfig(
|
||||
width, height, splitter_sizes, tabs
|
||||
)
|
||||
|
||||
def save_config(
|
||||
self,
|
||||
width: int,
|
||||
height: int,
|
||||
splitter_sizes: List[int],
|
||||
tabs: List[Dict[str, str]],
|
||||
):
|
||||
self._registry.set_item("width", width)
|
||||
self._registry.set_item("height", height)
|
||||
self._registry.set_item("splitter_sizes", splitter_sizes)
|
||||
self._registry.set_item("tabs", tabs)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from .window import (
|
||||
ConsoleInterpreterWindow
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ConsoleInterpreterWindow",
|
||||
)
|
||||
42
client/ayon_core/tools/console_interpreter/ui/utils.py
Normal file
42
client/ayon_core/tools/console_interpreter/ui/utils.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import os
|
||||
import sys
|
||||
import collections
|
||||
|
||||
|
||||
class StdOEWrap:
|
||||
def __init__(self):
|
||||
self._origin_stdout_write = None
|
||||
self._origin_stderr_write = None
|
||||
self._listening = False
|
||||
self.lines = collections.deque()
|
||||
|
||||
if not sys.stdout:
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
if not sys.stderr:
|
||||
sys.stderr = open(os.devnull, "w")
|
||||
|
||||
if self._origin_stdout_write is None:
|
||||
self._origin_stdout_write = sys.stdout.write
|
||||
|
||||
if self._origin_stderr_write is None:
|
||||
self._origin_stderr_write = sys.stderr.write
|
||||
|
||||
self._listening = True
|
||||
sys.stdout.write = self._stdout_listener
|
||||
sys.stderr.write = self._stderr_listener
|
||||
|
||||
def stop_listen(self):
|
||||
self._listening = False
|
||||
|
||||
def _stdout_listener(self, text):
|
||||
if self._listening:
|
||||
self.lines.append(text)
|
||||
if self._origin_stdout_write is not None:
|
||||
self._origin_stdout_write(text)
|
||||
|
||||
def _stderr_listener(self, text):
|
||||
if self._listening:
|
||||
self.lines.append(text)
|
||||
if self._origin_stderr_write is not None:
|
||||
self._origin_stderr_write(text)
|
||||
251
client/ayon_core/tools/console_interpreter/ui/widgets.py
Normal file
251
client/ayon_core/tools/console_interpreter/ui/widgets.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
from code import InteractiveInterpreter
|
||||
|
||||
from qtpy import QtCore, QtWidgets, QtGui
|
||||
|
||||
|
||||
class PythonCodeEditor(QtWidgets.QPlainTextEdit):
|
||||
execute_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("PythonCodeEditor")
|
||||
|
||||
self._indent = 4
|
||||
|
||||
def _tab_shift_right(self):
|
||||
cursor = self.textCursor()
|
||||
selected_text = cursor.selectedText()
|
||||
if not selected_text:
|
||||
cursor.insertText(" " * self._indent)
|
||||
return
|
||||
|
||||
sel_start = cursor.selectionStart()
|
||||
sel_end = cursor.selectionEnd()
|
||||
cursor.setPosition(sel_end)
|
||||
end_line = cursor.blockNumber()
|
||||
cursor.setPosition(sel_start)
|
||||
while True:
|
||||
cursor.movePosition(QtGui.QTextCursor.StartOfLine)
|
||||
text = cursor.block().text()
|
||||
spaces = len(text) - len(text.lstrip(" "))
|
||||
new_spaces = spaces % self._indent
|
||||
if not new_spaces:
|
||||
new_spaces = self._indent
|
||||
|
||||
cursor.insertText(" " * new_spaces)
|
||||
if cursor.blockNumber() == end_line:
|
||||
break
|
||||
|
||||
cursor.movePosition(QtGui.QTextCursor.NextBlock)
|
||||
|
||||
def _tab_shift_left(self):
|
||||
tmp_cursor = self.textCursor()
|
||||
sel_start = tmp_cursor.selectionStart()
|
||||
sel_end = tmp_cursor.selectionEnd()
|
||||
|
||||
cursor = QtGui.QTextCursor(self.document())
|
||||
cursor.setPosition(sel_end)
|
||||
end_line = cursor.blockNumber()
|
||||
cursor.setPosition(sel_start)
|
||||
while True:
|
||||
cursor.movePosition(QtGui.QTextCursor.StartOfLine)
|
||||
text = cursor.block().text()
|
||||
spaces = len(text) - len(text.lstrip(" "))
|
||||
if spaces:
|
||||
spaces_to_remove = (spaces % self._indent) or self._indent
|
||||
if spaces_to_remove > spaces:
|
||||
spaces_to_remove = spaces
|
||||
|
||||
cursor.setPosition(
|
||||
cursor.position() + spaces_to_remove,
|
||||
QtGui.QTextCursor.KeepAnchor
|
||||
)
|
||||
cursor.removeSelectedText()
|
||||
|
||||
if cursor.blockNumber() == end_line:
|
||||
break
|
||||
|
||||
cursor.movePosition(QtGui.QTextCursor.NextBlock)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if event.key() == QtCore.Qt.Key_Backtab:
|
||||
self._tab_shift_left()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if event.key() == QtCore.Qt.Key_Tab:
|
||||
if event.modifiers() == QtCore.Qt.NoModifier:
|
||||
self._tab_shift_right()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if (
|
||||
event.key() == QtCore.Qt.Key_Return
|
||||
and event.modifiers() == QtCore.Qt.ControlModifier
|
||||
):
|
||||
self.execute_requested.emit()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
super().keyPressEvent(event)
|
||||
|
||||
|
||||
class PythonTabWidget(QtWidgets.QWidget):
|
||||
add_tab_requested = QtCore.Signal()
|
||||
before_execute = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
code_input = PythonCodeEditor(self)
|
||||
|
||||
self.setFocusProxy(code_input)
|
||||
|
||||
add_tab_btn = QtWidgets.QPushButton("Add tab...", self)
|
||||
add_tab_btn.setDefault(False)
|
||||
add_tab_btn.setToolTip("Add new tab")
|
||||
|
||||
execute_btn = QtWidgets.QPushButton("Execute", self)
|
||||
execute_btn.setDefault(False)
|
||||
execute_btn.setToolTip("Execute command (Ctrl + Enter)")
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btns_layout.addWidget(add_tab_btn)
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(execute_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(code_input, 1)
|
||||
layout.addLayout(btns_layout, 0)
|
||||
|
||||
add_tab_btn.clicked.connect(self._on_add_tab_clicked)
|
||||
execute_btn.clicked.connect(self._on_execute_clicked)
|
||||
code_input.execute_requested.connect(self.execute)
|
||||
|
||||
self._code_input = code_input
|
||||
self._interpreter = InteractiveInterpreter()
|
||||
|
||||
def _on_add_tab_clicked(self):
|
||||
self.add_tab_requested.emit()
|
||||
|
||||
def _on_execute_clicked(self):
|
||||
self.execute()
|
||||
|
||||
def get_code(self):
|
||||
return self._code_input.toPlainText()
|
||||
|
||||
def set_code(self, code_text):
|
||||
self._code_input.setPlainText(code_text)
|
||||
|
||||
def execute(self):
|
||||
code_text = self._code_input.toPlainText()
|
||||
self.before_execute.emit(code_text)
|
||||
self._interpreter.runcode(code_text)
|
||||
|
||||
|
||||
class TabNameDialog(QtWidgets.QDialog):
|
||||
default_width = 330
|
||||
default_height = 85
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowTitle("Enter tab name")
|
||||
|
||||
name_label = QtWidgets.QLabel("Tab name:", self)
|
||||
name_input = QtWidgets.QLineEdit(self)
|
||||
|
||||
inputs_layout = QtWidgets.QHBoxLayout()
|
||||
inputs_layout.addWidget(name_label)
|
||||
inputs_layout.addWidget(name_input)
|
||||
|
||||
ok_btn = QtWidgets.QPushButton("Ok", self)
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel", self)
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(ok_btn)
|
||||
btns_layout.addWidget(cancel_btn)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addLayout(inputs_layout)
|
||||
layout.addStretch(1)
|
||||
layout.addLayout(btns_layout)
|
||||
|
||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||
cancel_btn.clicked.connect(self._on_cancel_clicked)
|
||||
|
||||
self._name_input = name_input
|
||||
self._ok_btn = ok_btn
|
||||
self._cancel_btn = cancel_btn
|
||||
|
||||
self._result = None
|
||||
|
||||
self.resize(self.default_width, self.default_height)
|
||||
|
||||
def set_tab_name(self, name):
|
||||
self._name_input.setText(name)
|
||||
|
||||
def result(self):
|
||||
return self._result
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
btns_width = max(
|
||||
self._ok_btn.width(),
|
||||
self._cancel_btn.width()
|
||||
)
|
||||
|
||||
self._ok_btn.setMinimumWidth(btns_width)
|
||||
self._cancel_btn.setMinimumWidth(btns_width)
|
||||
|
||||
def _on_ok_clicked(self):
|
||||
self._result = self._name_input.text()
|
||||
self.accept()
|
||||
|
||||
def _on_cancel_clicked(self):
|
||||
self._result = None
|
||||
self.reject()
|
||||
|
||||
|
||||
class OutputTextWidget(QtWidgets.QTextEdit):
|
||||
v_max_offset = 4
|
||||
|
||||
def vertical_scroll_at_max(self):
|
||||
v_scroll = self.verticalScrollBar()
|
||||
return v_scroll.value() > v_scroll.maximum() - self.v_max_offset
|
||||
|
||||
def scroll_to_bottom(self):
|
||||
v_scroll = self.verticalScrollBar()
|
||||
return v_scroll.setValue(v_scroll.maximum())
|
||||
|
||||
|
||||
class EnhancedTabBar(QtWidgets.QTabBar):
|
||||
double_clicked = QtCore.Signal(QtCore.QPoint)
|
||||
right_clicked = QtCore.Signal(QtCore.QPoint)
|
||||
mid_clicked = QtCore.Signal(QtCore.QPoint)
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setDrawBase(False)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
self.double_clicked.emit(event.globalPos())
|
||||
event.accept()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == QtCore.Qt.RightButton:
|
||||
self.right_clicked.emit(event.globalPos())
|
||||
event.accept()
|
||||
return
|
||||
|
||||
elif event.button() == QtCore.Qt.MidButton:
|
||||
self.mid_clicked.emit(event.globalPos())
|
||||
event.accept()
|
||||
|
||||
else:
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
324
client/ayon_core/tools/console_interpreter/ui/window.py
Normal file
324
client/ayon_core/tools/console_interpreter/ui/window.py
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import re
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
from ayon_core import resources
|
||||
from ayon_core.style import load_stylesheet
|
||||
from ayon_core.tools.console_interpreter import (
|
||||
AbstractInterpreterController,
|
||||
InterpreterController,
|
||||
)
|
||||
|
||||
from .utils import StdOEWrap
|
||||
from .widgets import (
|
||||
PythonTabWidget,
|
||||
OutputTextWidget,
|
||||
EnhancedTabBar,
|
||||
TabNameDialog,
|
||||
)
|
||||
|
||||
ANSI_ESCAPE = re.compile(
|
||||
r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]"
|
||||
)
|
||||
AYON_ART = r"""
|
||||
|
||||
▄██▄
|
||||
▄███▄ ▀██▄ ▀██▀ ▄██▀ ▄██▀▀▀██▄ ▀███▄ █▄
|
||||
▄▄ ▀██▄ ▀██▄ ▄██▀ ██▀ ▀██▄ ▄ ▀██▄ ███
|
||||
▄██▀ ██▄ ▀ ▄▄ ▀ ██ ▄██ ███ ▀██▄ ███
|
||||
▄██▀ ▀██▄ ██ ▀██▄ ▄██▀ ███ ▀██ ▀█▀
|
||||
▄██▀ ▀██▄ ▀█ ▀██▄▄▄▄██▀ █▀ ▀██▄
|
||||
|
||||
· · - =[ by YNPUT ]:[ http://ayon.ynput.io ]= - · ·
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ConsoleInterpreterWindow(QtWidgets.QWidget):
|
||||
default_width = 1000
|
||||
default_height = 600
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller: Optional[AbstractInterpreterController] = None,
|
||||
parent: Optional[QtWidgets.QWidget] = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowTitle("AYON Console")
|
||||
self.setWindowIcon(QtGui.QIcon(resources.get_ayon_icon_filepath()))
|
||||
|
||||
if controller is None:
|
||||
controller = InterpreterController()
|
||||
|
||||
output_widget = OutputTextWidget(self)
|
||||
output_widget.setObjectName("PythonInterpreterOutput")
|
||||
output_widget.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
|
||||
output_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||
|
||||
tab_widget = QtWidgets.QTabWidget(self)
|
||||
tab_bar = EnhancedTabBar(tab_widget)
|
||||
tab_widget.setTabBar(tab_bar)
|
||||
tab_widget.setTabsClosable(False)
|
||||
tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
widgets_splitter = QtWidgets.QSplitter(self)
|
||||
widgets_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
widgets_splitter.addWidget(output_widget)
|
||||
widgets_splitter.addWidget(tab_widget)
|
||||
widgets_splitter.setStretchFactor(0, 1)
|
||||
widgets_splitter.setStretchFactor(1, 1)
|
||||
height = int(self.default_height / 2)
|
||||
widgets_splitter.setSizes([height, self.default_height - height])
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(widgets_splitter)
|
||||
|
||||
line_check_timer = QtCore.QTimer()
|
||||
line_check_timer.setInterval(200)
|
||||
|
||||
line_check_timer.timeout.connect(self._on_timer_timeout)
|
||||
tab_bar.right_clicked.connect(self._on_tab_right_click)
|
||||
tab_bar.double_clicked.connect(self._on_tab_double_click)
|
||||
tab_bar.mid_clicked.connect(self._on_tab_mid_click)
|
||||
tab_widget.tabCloseRequested.connect(self._on_tab_close_req)
|
||||
|
||||
self._tabs = []
|
||||
|
||||
self._stdout_err_wrapper = StdOEWrap()
|
||||
|
||||
self._widgets_splitter = widgets_splitter
|
||||
self._output_widget = output_widget
|
||||
self._tab_widget = tab_widget
|
||||
self._line_check_timer = line_check_timer
|
||||
|
||||
self._append_lines([AYON_ART])
|
||||
|
||||
self._first_show = True
|
||||
self._controller = controller
|
||||
|
||||
def showEvent(self, event):
|
||||
self._line_check_timer.start()
|
||||
super().showEvent(event)
|
||||
# First show setup
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
||||
if self._tab_widget.count() < 1:
|
||||
self.add_tab("Python")
|
||||
|
||||
self._output_widget.scroll_to_bottom()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._save_registry()
|
||||
super().closeEvent(event)
|
||||
self._line_check_timer.stop()
|
||||
|
||||
def add_tab(self, tab_name, index=None):
|
||||
widget = PythonTabWidget(self)
|
||||
widget.before_execute.connect(self._on_before_execute)
|
||||
widget.add_tab_requested.connect(self._on_add_requested)
|
||||
if index is None:
|
||||
if self._tab_widget.count() > 0:
|
||||
index = self._tab_widget.currentIndex() + 1
|
||||
else:
|
||||
index = 0
|
||||
|
||||
self._tabs.append(widget)
|
||||
self._tab_widget.insertTab(index, widget, tab_name)
|
||||
self._tab_widget.setCurrentIndex(index)
|
||||
|
||||
if self._tab_widget.count() > 1:
|
||||
self._tab_widget.setTabsClosable(True)
|
||||
widget.setFocus()
|
||||
return widget
|
||||
|
||||
def _on_first_show(self):
|
||||
config = self._controller.get_config()
|
||||
width = config.width
|
||||
height = config.height
|
||||
if width is None or width < 200:
|
||||
width = self.default_width
|
||||
if height is None or height < 200:
|
||||
height = self.default_height
|
||||
|
||||
for tab_item in config.tabs:
|
||||
widget = self.add_tab(tab_item.name)
|
||||
widget.set_code(tab_item.code)
|
||||
|
||||
self.resize(width, height)
|
||||
# Change stylesheet
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
# Check if splitter sizes are set
|
||||
splitters_count = len(self._widgets_splitter.sizes())
|
||||
if len(config.splitter_sizes) == splitters_count:
|
||||
self._widgets_splitter.setSizes(config.splitter_sizes)
|
||||
|
||||
def _save_registry(self):
|
||||
tabs = []
|
||||
for tab_idx in range(self._tab_widget.count()):
|
||||
widget = self._tab_widget.widget(tab_idx)
|
||||
tabs.append({
|
||||
"name": self._tab_widget.tabText(tab_idx),
|
||||
"code": widget.get_code()
|
||||
})
|
||||
|
||||
self._controller.save_config(
|
||||
self.width(),
|
||||
self.height(),
|
||||
self._widgets_splitter.sizes(),
|
||||
tabs
|
||||
)
|
||||
|
||||
def _on_tab_right_click(self, global_point):
|
||||
point = self._tab_widget.mapFromGlobal(global_point)
|
||||
tab_bar = self._tab_widget.tabBar()
|
||||
tab_idx = tab_bar.tabAt(point)
|
||||
last_index = tab_bar.count() - 1
|
||||
if tab_idx < 0 or tab_idx > last_index:
|
||||
return
|
||||
|
||||
menu = QtWidgets.QMenu(self._tab_widget)
|
||||
|
||||
add_tab_action = QtWidgets.QAction("Add tab...", menu)
|
||||
add_tab_action.setToolTip("Add new tab")
|
||||
|
||||
rename_tab_action = QtWidgets.QAction("Rename...", menu)
|
||||
rename_tab_action.setToolTip("Rename tab")
|
||||
|
||||
duplicate_tab_action = QtWidgets.QAction("Duplicate...", menu)
|
||||
duplicate_tab_action.setToolTip("Duplicate code to new tab")
|
||||
|
||||
close_tab_action = QtWidgets.QAction("Close", menu)
|
||||
close_tab_action.setToolTip("Close tab and lose content")
|
||||
close_tab_action.setEnabled(self._tab_widget.tabsClosable())
|
||||
|
||||
menu.addAction(add_tab_action)
|
||||
menu.addAction(rename_tab_action)
|
||||
menu.addAction(duplicate_tab_action)
|
||||
menu.addAction(close_tab_action)
|
||||
|
||||
result = menu.exec_(global_point)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
if result is rename_tab_action:
|
||||
self._rename_tab_req(tab_idx)
|
||||
|
||||
elif result is add_tab_action:
|
||||
self._on_add_requested()
|
||||
|
||||
elif result is duplicate_tab_action:
|
||||
self._duplicate_requested(tab_idx)
|
||||
|
||||
elif result is close_tab_action:
|
||||
self._on_tab_close_req(tab_idx)
|
||||
|
||||
def _rename_tab_req(self, tab_idx):
|
||||
dialog = TabNameDialog(self)
|
||||
dialog.set_tab_name(self._tab_widget.tabText(tab_idx))
|
||||
dialog.exec_()
|
||||
tab_name = dialog.result()
|
||||
if tab_name:
|
||||
self._tab_widget.setTabText(tab_idx, tab_name)
|
||||
|
||||
def _duplicate_requested(self, tab_idx=None):
|
||||
if tab_idx is None:
|
||||
tab_idx = self._tab_widget.currentIndex()
|
||||
|
||||
src_widget = self._tab_widget.widget(tab_idx)
|
||||
dst_widget = self._add_tab()
|
||||
if dst_widget is None:
|
||||
return
|
||||
dst_widget.set_code(src_widget.get_code())
|
||||
|
||||
def _on_tab_mid_click(self, global_point):
|
||||
point = self._tab_widget.mapFromGlobal(global_point)
|
||||
tab_bar = self._tab_widget.tabBar()
|
||||
tab_idx = tab_bar.tabAt(point)
|
||||
last_index = tab_bar.count() - 1
|
||||
if tab_idx < 0 or tab_idx > last_index:
|
||||
return
|
||||
|
||||
self._on_tab_close_req(tab_idx)
|
||||
|
||||
def _on_tab_double_click(self, global_point):
|
||||
point = self._tab_widget.mapFromGlobal(global_point)
|
||||
tab_bar = self._tab_widget.tabBar()
|
||||
tab_idx = tab_bar.tabAt(point)
|
||||
last_index = tab_bar.count() - 1
|
||||
if tab_idx < 0 or tab_idx > last_index:
|
||||
return
|
||||
|
||||
self._rename_tab_req(tab_idx)
|
||||
|
||||
def _on_tab_close_req(self, tab_index):
|
||||
if self._tab_widget.count() == 1:
|
||||
return
|
||||
|
||||
widget = self._tab_widget.widget(tab_index)
|
||||
if widget in self._tabs:
|
||||
self._tabs.remove(widget)
|
||||
self._tab_widget.removeTab(tab_index)
|
||||
|
||||
if self._tab_widget.count() == 1:
|
||||
self._tab_widget.setTabsClosable(False)
|
||||
|
||||
def _append_lines(self, lines):
|
||||
at_max = self._output_widget.vertical_scroll_at_max()
|
||||
tmp_cursor = QtGui.QTextCursor(self._output_widget.document())
|
||||
tmp_cursor.movePosition(QtGui.QTextCursor.End)
|
||||
for line in lines:
|
||||
tmp_cursor.insertText(line)
|
||||
|
||||
if at_max:
|
||||
self._output_widget.scroll_to_bottom()
|
||||
|
||||
def _on_timer_timeout(self):
|
||||
if self._stdout_err_wrapper.lines:
|
||||
lines = []
|
||||
while self._stdout_err_wrapper.lines:
|
||||
line = self._stdout_err_wrapper.lines.popleft()
|
||||
lines.append(ANSI_ESCAPE.sub("", line))
|
||||
self._append_lines(lines)
|
||||
|
||||
def _on_add_requested(self):
|
||||
self._add_tab()
|
||||
|
||||
def _add_tab(self):
|
||||
dialog = TabNameDialog(self)
|
||||
dialog.exec_()
|
||||
tab_name = dialog.result()
|
||||
if tab_name:
|
||||
return self.add_tab(tab_name)
|
||||
|
||||
return None
|
||||
|
||||
def _on_before_execute(self, code_text):
|
||||
at_max = self._output_widget.vertical_scroll_at_max()
|
||||
document = self._output_widget.document()
|
||||
tmp_cursor = QtGui.QTextCursor(document)
|
||||
tmp_cursor.movePosition(QtGui.QTextCursor.End)
|
||||
tmp_cursor.insertText("{}\nExecuting command:\n".format(20 * "-"))
|
||||
|
||||
code_block_format = QtGui.QTextFrameFormat()
|
||||
code_block_format.setBackground(QtGui.QColor(27, 27, 27))
|
||||
code_block_format.setPadding(4)
|
||||
|
||||
tmp_cursor.insertFrame(code_block_format)
|
||||
char_format = tmp_cursor.charFormat()
|
||||
char_format.setForeground(
|
||||
QtGui.QBrush(QtGui.QColor(114, 224, 198))
|
||||
)
|
||||
tmp_cursor.setCharFormat(char_format)
|
||||
tmp_cursor.insertText(code_text)
|
||||
|
||||
# Create new cursor
|
||||
tmp_cursor = QtGui.QTextCursor(document)
|
||||
tmp_cursor.movePosition(QtGui.QTextCursor.End)
|
||||
tmp_cursor.insertText("{}\n".format(20 * "-"))
|
||||
|
||||
if at_max:
|
||||
self._output_widget.scroll_to_bottom()
|
||||
|
|
@ -104,7 +104,7 @@ class ProductNameValidator(RegularExpressionValidatorClass):
|
|||
|
||||
def validate(self, text, pos):
|
||||
results = super(ProductNameValidator, self).validate(text, pos)
|
||||
if results[0] == self.Invalid:
|
||||
if results[0] == RegularExpressionValidatorClass.Invalid:
|
||||
self.invalid.emit(self.invalid_chars(text))
|
||||
return results
|
||||
|
||||
|
|
@ -217,7 +217,9 @@ class ProductTypeDescriptionWidget(QtWidgets.QWidget):
|
|||
|
||||
product_type_label = QtWidgets.QLabel(self)
|
||||
product_type_label.setObjectName("CreatorProductTypeLabel")
|
||||
product_type_label.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft)
|
||||
product_type_label.setAlignment(
|
||||
QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft
|
||||
)
|
||||
|
||||
help_label = QtWidgets.QLabel(self)
|
||||
help_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from ayon_core.pipeline.actions import (
|
|||
discover_launcher_actions,
|
||||
LauncherAction,
|
||||
LauncherActionSelection,
|
||||
register_launcher_action_path,
|
||||
)
|
||||
from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch
|
||||
|
||||
|
|
@ -21,9 +22,9 @@ except ImportError:
|
|||
|
||||
Application action based on 'ApplicationManager' system.
|
||||
|
||||
Handling of applications in launcher is not ideal and should be completely
|
||||
redone from scratch. This is just a temporary solution to keep backwards
|
||||
compatibility with AYON launcher.
|
||||
Handling of applications in launcher is not ideal and should be
|
||||
completely redone from scratch. This is just a temporary solution
|
||||
to keep backwards compatibility with AYON launcher.
|
||||
|
||||
Todos:
|
||||
Move handling of errors to frontend.
|
||||
|
|
@ -459,6 +460,14 @@ class ActionsModel:
|
|||
|
||||
def _get_discovered_action_classes(self):
|
||||
if self._discovered_actions is None:
|
||||
# NOTE We don't need to register the paths, but that would
|
||||
# require to change discovery logic and deprecate all functions
|
||||
# related to registering and discovering launcher actions.
|
||||
addons_manager = self._get_addons_manager()
|
||||
actions_paths = addons_manager.collect_launcher_action_paths()
|
||||
for path in actions_paths:
|
||||
if path and os.path.exists(path):
|
||||
register_launcher_action_path(path)
|
||||
self._discovered_actions = (
|
||||
discover_launcher_actions()
|
||||
+ self._get_applications_action_classes()
|
||||
|
|
|
|||
|
|
@ -202,8 +202,9 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self._go_to_hierarchy_page(project_name)
|
||||
|
||||
def _on_projects_refresh(self):
|
||||
# There is nothing to do, we're on projects page
|
||||
# Refresh only actions on projects page
|
||||
if self._is_on_projects_page:
|
||||
self._actions_widget.refresh()
|
||||
return
|
||||
|
||||
# No projects were found -> go back to projects page
|
||||
|
|
|
|||
|
|
@ -372,17 +372,17 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
|
||||
repre_ids = set()
|
||||
for container in containers:
|
||||
repre_id = container.get("representation")
|
||||
# Ignore invalid representation ids.
|
||||
# - invalid representation ids may be available if e.g. is
|
||||
# opened scene from OpenPype whe 'ObjectId' was used instead
|
||||
# of 'uuid'.
|
||||
# NOTE: Server call would crash if there is any invalid id.
|
||||
# That would cause crash we won't get any information.
|
||||
try:
|
||||
repre_id = container.get("representation")
|
||||
# Ignore invalid representation ids.
|
||||
# - invalid representation ids may be available if e.g. is
|
||||
# opened scene from OpenPype whe 'ObjectId' was used
|
||||
# instead of 'uuid'.
|
||||
# NOTE: Server call would crash if there is any invalid id.
|
||||
# That would cause crash we won't get any information.
|
||||
uuid.UUID(repre_id)
|
||||
repre_ids.add(repre_id)
|
||||
except ValueError:
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
product_ids = self._products_model.get_product_ids_by_repre_ids(
|
||||
|
|
|
|||
|
|
@ -517,7 +517,11 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox):
|
|||
def setItemCheckState(self, index, state):
|
||||
self.setItemData(index, state, QtCore.Qt.CheckStateRole)
|
||||
|
||||
def set_value(self, values: Optional[Iterable[Any]], role: Optional[int] = None):
|
||||
def set_value(
|
||||
self,
|
||||
values: Optional[Iterable[Any]],
|
||||
role: Optional[int] = None,
|
||||
):
|
||||
if role is None:
|
||||
role = self._value_role
|
||||
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
|
||||
editor = VersionComboBox(product_id, parent)
|
||||
editor.setProperty("itemId", item_id)
|
||||
editor.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
|
||||
editor.value_changed.connect(self._on_editor_change)
|
||||
editor.destroyed.connect(self._on_destroy)
|
||||
|
|
|
|||
|
|
@ -499,8 +499,10 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
version_item.version_id
|
||||
for version_item in last_version_by_product_id.values()
|
||||
}
|
||||
repre_count_by_version_id = self._controller.get_versions_representation_count(
|
||||
project_name, version_ids
|
||||
repre_count_by_version_id = (
|
||||
self._controller.get_versions_representation_count(
|
||||
project_name, version_ids
|
||||
)
|
||||
)
|
||||
sync_availability_by_version_id = (
|
||||
self._controller.get_version_sync_availability(
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
|||
@abstractmethod
|
||||
def get_creator_attribute_definitions(
|
||||
self, instance_ids: Iterable[str]
|
||||
) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]:
|
||||
) -> List[Tuple[AbstractAttrDef, Dict[str, Dict[str, Any]]]]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -375,6 +375,14 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
|||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def revert_instances_create_attr_values(
|
||||
self,
|
||||
instance_ids: List["Union[str, None]"],
|
||||
key: str,
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_publish_attribute_definitions(
|
||||
self,
|
||||
|
|
@ -383,7 +391,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
|||
) -> List[Tuple[
|
||||
str,
|
||||
List[AbstractAttrDef],
|
||||
Dict[str, List[Tuple[str, Any]]]
|
||||
Dict[str, List[Tuple[str, Any, Any]]]
|
||||
]]:
|
||||
pass
|
||||
|
||||
|
|
@ -397,6 +405,15 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
|||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def revert_instances_publish_attr_values(
|
||||
self,
|
||||
instance_ids: List["Union[str, None]"],
|
||||
plugin_name: str,
|
||||
key: str,
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_name(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -412,6 +412,11 @@ class PublisherController(
|
|||
instance_ids, key, value
|
||||
)
|
||||
|
||||
def revert_instances_create_attr_values(self, instance_ids, key):
|
||||
self._create_model.revert_instances_create_attr_values(
|
||||
instance_ids, key
|
||||
)
|
||||
|
||||
def get_publish_attribute_definitions(self, instance_ids, include_context):
|
||||
"""Collect publish attribute definitions for passed instances.
|
||||
|
||||
|
|
@ -432,6 +437,13 @@ class PublisherController(
|
|||
instance_ids, plugin_name, key, value
|
||||
)
|
||||
|
||||
def revert_instances_publish_attr_values(
|
||||
self, instance_ids, plugin_name, key
|
||||
):
|
||||
return self._create_model.revert_instances_publish_attr_values(
|
||||
instance_ids, plugin_name, key
|
||||
)
|
||||
|
||||
def get_product_name(
|
||||
self,
|
||||
creator_identifier,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from ayon_core.tools.publisher.abstract import (
|
|||
)
|
||||
|
||||
CREATE_EVENT_SOURCE = "publisher.create.model"
|
||||
_DEFAULT_VALUE = object()
|
||||
|
||||
|
||||
class CreatorType:
|
||||
|
|
@ -295,7 +296,7 @@ class InstanceItem:
|
|||
return InstanceItem(
|
||||
instance.id,
|
||||
instance.creator_identifier,
|
||||
instance.label,
|
||||
instance.label or "N/A",
|
||||
instance.group_label,
|
||||
instance.product_type,
|
||||
instance.product_name,
|
||||
|
|
@ -752,24 +753,16 @@ class CreateModel:
|
|||
self._remove_instances_from_context(instance_ids)
|
||||
|
||||
def set_instances_create_attr_values(self, instance_ids, key, value):
|
||||
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
|
||||
for instance_id in instance_ids:
|
||||
instance = self._get_instance_by_id(instance_id)
|
||||
creator_attributes = instance["creator_attributes"]
|
||||
attr_def = creator_attributes.get_attr_def(key)
|
||||
if (
|
||||
attr_def is None
|
||||
or not attr_def.is_value_def
|
||||
or not attr_def.visible
|
||||
or not attr_def.enabled
|
||||
or not attr_def.is_value_valid(value)
|
||||
):
|
||||
continue
|
||||
creator_attributes[key] = value
|
||||
self._set_instances_create_attr_values(instance_ids, key, value)
|
||||
|
||||
def revert_instances_create_attr_values(self, instance_ids, key):
|
||||
self._set_instances_create_attr_values(
|
||||
instance_ids, key, _DEFAULT_VALUE
|
||||
)
|
||||
|
||||
def get_creator_attribute_definitions(
|
||||
self, instance_ids: List[str]
|
||||
) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]:
|
||||
) -> List[Tuple[AbstractAttrDef, Dict[str, Dict[str, Any]]]]:
|
||||
"""Collect creator attribute definitions for multuple instances.
|
||||
|
||||
Args:
|
||||
|
|
@ -796,37 +789,38 @@ class CreateModel:
|
|||
|
||||
if found_idx is None:
|
||||
idx = len(output)
|
||||
output.append((attr_def, [instance_id], [value]))
|
||||
output.append((
|
||||
attr_def,
|
||||
{
|
||||
instance_id: {
|
||||
"value": value,
|
||||
"default": attr_def.default
|
||||
}
|
||||
}
|
||||
))
|
||||
_attr_defs[idx] = attr_def
|
||||
else:
|
||||
_, ids, values = output[found_idx]
|
||||
ids.append(instance_id)
|
||||
values.append(value)
|
||||
_, info_by_id = output[found_idx]
|
||||
info_by_id[instance_id] = {
|
||||
"value": value,
|
||||
"default": attr_def.default
|
||||
}
|
||||
|
||||
return output
|
||||
|
||||
def set_instances_publish_attr_values(
|
||||
self, instance_ids, plugin_name, key, value
|
||||
self, instance_ids, plugin_name, key, value
|
||||
):
|
||||
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
|
||||
for instance_id in instance_ids:
|
||||
if instance_id is None:
|
||||
instance = self._create_context
|
||||
else:
|
||||
instance = self._get_instance_by_id(instance_id)
|
||||
plugin_val = instance.publish_attributes[plugin_name]
|
||||
attr_def = plugin_val.get_attr_def(key)
|
||||
# Ignore if attribute is not available or enabled/visible
|
||||
# on the instance, or the value is not valid for definition
|
||||
if (
|
||||
attr_def is None
|
||||
or not attr_def.is_value_def
|
||||
or not attr_def.visible
|
||||
or not attr_def.enabled
|
||||
or not attr_def.is_value_valid(value)
|
||||
):
|
||||
continue
|
||||
self._set_instances_publish_attr_values(
|
||||
instance_ids, plugin_name, key, value
|
||||
)
|
||||
|
||||
plugin_val[key] = value
|
||||
def revert_instances_publish_attr_values(
|
||||
self, instance_ids, plugin_name, key
|
||||
):
|
||||
self._set_instances_publish_attr_values(
|
||||
instance_ids, plugin_name, key, _DEFAULT_VALUE
|
||||
)
|
||||
|
||||
def get_publish_attribute_definitions(
|
||||
self,
|
||||
|
|
@ -835,7 +829,7 @@ class CreateModel:
|
|||
) -> List[Tuple[
|
||||
str,
|
||||
List[AbstractAttrDef],
|
||||
Dict[str, List[Tuple[str, Any]]]
|
||||
Dict[str, List[Tuple[str, Any, Any]]]
|
||||
]]:
|
||||
"""Collect publish attribute definitions for passed instances.
|
||||
|
||||
|
|
@ -865,21 +859,21 @@ class CreateModel:
|
|||
attr_defs = attr_val.attr_defs
|
||||
if not attr_defs:
|
||||
continue
|
||||
|
||||
plugin_attr_defs = all_defs_by_plugin_name.setdefault(
|
||||
plugin_name, []
|
||||
)
|
||||
plugin_attr_defs.append(attr_defs)
|
||||
|
||||
plugin_values = all_plugin_values.setdefault(plugin_name, {})
|
||||
|
||||
plugin_attr_defs.append(attr_defs)
|
||||
|
||||
for attr_def in attr_defs:
|
||||
if isinstance(attr_def, UIDef):
|
||||
continue
|
||||
|
||||
attr_values = plugin_values.setdefault(attr_def.key, [])
|
||||
|
||||
value = attr_val[attr_def.key]
|
||||
attr_values.append((item_id, value))
|
||||
attr_values.append(
|
||||
(item_id, attr_val[attr_def.key], attr_def.default)
|
||||
)
|
||||
|
||||
attr_defs_by_plugin_name = {}
|
||||
for plugin_name, attr_defs in all_defs_by_plugin_name.items():
|
||||
|
|
@ -893,7 +887,7 @@ class CreateModel:
|
|||
output.append((
|
||||
plugin_name,
|
||||
attr_defs_by_plugin_name[plugin_name],
|
||||
all_plugin_values
|
||||
all_plugin_values[plugin_name],
|
||||
))
|
||||
return output
|
||||
|
||||
|
|
@ -1053,6 +1047,53 @@ class CreateModel:
|
|||
CreatorItem.from_creator(creator)
|
||||
)
|
||||
|
||||
def _set_instances_create_attr_values(self, instance_ids, key, value):
|
||||
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
|
||||
for instance_id in instance_ids:
|
||||
instance = self._get_instance_by_id(instance_id)
|
||||
creator_attributes = instance["creator_attributes"]
|
||||
attr_def = creator_attributes.get_attr_def(key)
|
||||
if (
|
||||
attr_def is None
|
||||
or not attr_def.is_value_def
|
||||
or not attr_def.visible
|
||||
or not attr_def.enabled
|
||||
):
|
||||
continue
|
||||
|
||||
if value is _DEFAULT_VALUE:
|
||||
creator_attributes[key] = attr_def.default
|
||||
|
||||
elif attr_def.is_value_valid(value):
|
||||
creator_attributes[key] = value
|
||||
|
||||
def _set_instances_publish_attr_values(
|
||||
self, instance_ids, plugin_name, key, value
|
||||
):
|
||||
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
|
||||
for instance_id in instance_ids:
|
||||
if instance_id is None:
|
||||
instance = self._create_context
|
||||
else:
|
||||
instance = self._get_instance_by_id(instance_id)
|
||||
plugin_val = instance.publish_attributes[plugin_name]
|
||||
attr_def = plugin_val.get_attr_def(key)
|
||||
# Ignore if attribute is not available or enabled/visible
|
||||
# on the instance, or the value is not valid for definition
|
||||
if (
|
||||
attr_def is None
|
||||
or not attr_def.is_value_def
|
||||
or not attr_def.visible
|
||||
or not attr_def.enabled
|
||||
):
|
||||
continue
|
||||
|
||||
if value is _DEFAULT_VALUE:
|
||||
plugin_val[key] = attr_def.default
|
||||
|
||||
elif attr_def.is_value_valid(value):
|
||||
plugin_val[key] = value
|
||||
|
||||
def _cc_added_instance(self, event):
|
||||
instance_ids = {
|
||||
instance.id
|
||||
|
|
|
|||
|
|
@ -484,6 +484,34 @@ class LoadedFilesView(QtWidgets.QTreeView):
|
|||
self._time_delegate = time_delegate
|
||||
self._remove_btn = remove_btn
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
self._model.refresh()
|
||||
header = self.header()
|
||||
header.resizeSections(QtWidgets.QHeaderView.ResizeToContents)
|
||||
self._update_remove_btn()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self._update_remove_btn()
|
||||
|
||||
def add_filepaths(self, filepaths):
|
||||
self._model.add_filepaths(filepaths)
|
||||
self._fill_selection()
|
||||
|
||||
def remove_item_by_id(self, item_id):
|
||||
self._model.remove_item_by_id(item_id)
|
||||
self._fill_selection()
|
||||
|
||||
def get_current_report(self):
|
||||
index = self.currentIndex()
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
return self._model.get_report_by_id(item_id)
|
||||
|
||||
def refresh(self):
|
||||
self._model.refresh()
|
||||
self._fill_selection()
|
||||
|
||||
def _update_remove_btn(self):
|
||||
viewport = self.viewport()
|
||||
height = viewport.height() + self.header().height()
|
||||
|
|
@ -496,28 +524,9 @@ class LoadedFilesView(QtWidgets.QTreeView):
|
|||
header.resizeSections(QtWidgets.QHeaderView.ResizeToContents)
|
||||
self._update_remove_btn()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self._update_remove_btn()
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
self._model.refresh()
|
||||
header = self.header()
|
||||
header.resizeSections(QtWidgets.QHeaderView.ResizeToContents)
|
||||
self._update_remove_btn()
|
||||
|
||||
def _on_selection_change(self):
|
||||
self.selection_changed.emit()
|
||||
|
||||
def add_filepaths(self, filepaths):
|
||||
self._model.add_filepaths(filepaths)
|
||||
self._fill_selection()
|
||||
|
||||
def remove_item_by_id(self, item_id):
|
||||
self._model.remove_item_by_id(item_id)
|
||||
self._fill_selection()
|
||||
|
||||
def _on_remove_clicked(self):
|
||||
index = self.currentIndex()
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
|
|
@ -533,11 +542,6 @@ class LoadedFilesView(QtWidgets.QTreeView):
|
|||
if index.isValid():
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
def get_current_report(self):
|
||||
index = self.currentIndex()
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
return self._model.get_report_by_id(item_id)
|
||||
|
||||
|
||||
class LoadedFilesWidget(QtWidgets.QWidget):
|
||||
report_changed = QtCore.Signal()
|
||||
|
|
@ -577,15 +581,18 @@ class LoadedFilesWidget(QtWidgets.QWidget):
|
|||
self._add_filepaths(filepaths)
|
||||
event.accept()
|
||||
|
||||
def refresh(self):
|
||||
self._view.refresh()
|
||||
|
||||
def get_current_report(self):
|
||||
return self._view.get_current_report()
|
||||
|
||||
def _on_report_change(self):
|
||||
self.report_changed.emit()
|
||||
|
||||
def _add_filepaths(self, filepaths):
|
||||
self._view.add_filepaths(filepaths)
|
||||
|
||||
def get_current_report(self):
|
||||
return self._view.get_current_report()
|
||||
|
||||
|
||||
class PublishReportViewerWindow(QtWidgets.QWidget):
|
||||
default_width = 1200
|
||||
|
|
@ -624,9 +631,12 @@ class PublishReportViewerWindow(QtWidgets.QWidget):
|
|||
self.resize(self.default_width, self.default_height)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
def _on_report_change(self):
|
||||
report = self._loaded_files_widget.get_current_report()
|
||||
self.set_report(report)
|
||||
def refresh(self):
|
||||
self._loaded_files_widget.refresh()
|
||||
|
||||
def set_report(self, report_data):
|
||||
self._main_widget.set_report(report_data)
|
||||
|
||||
def _on_report_change(self):
|
||||
report = self._loaded_files_widget.get_current_report()
|
||||
self.set_report(report)
|
||||
|
|
|
|||
|
|
@ -339,7 +339,9 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
self._change_visibility_for_state()
|
||||
self._product_content_layout.addWidget(self._create_widget, 7)
|
||||
self._product_content_layout.addWidget(self._product_views_widget, 3)
|
||||
self._product_content_layout.addWidget(self._product_attributes_wrap, 7)
|
||||
self._product_content_layout.addWidget(
|
||||
self._product_attributes_wrap, 7
|
||||
)
|
||||
|
||||
def _change_visibility_for_state(self):
|
||||
self._create_widget.setVisible(
|
||||
|
|
|
|||
|
|
@ -1,13 +1,58 @@
|
|||
import typing
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core.lib.attribute_definitions import UnknownDef
|
||||
from ayon_core.tools.attribute_defs import create_widget_for_attr_def
|
||||
from ayon_core.lib.attribute_definitions import AbstractAttrDef, UnknownDef
|
||||
from ayon_core.tools.attribute_defs import (
|
||||
create_widget_for_attr_def,
|
||||
AttributeDefinitionsLabel,
|
||||
)
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
from ayon_core.tools.publisher.constants import (
|
||||
INPUTS_LAYOUT_HSPACING,
|
||||
INPUTS_LAYOUT_VSPACING,
|
||||
)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Union
|
||||
|
||||
|
||||
class _CreateAttrDefInfo:
|
||||
"""Helper class to store information about create attribute definition."""
|
||||
def __init__(
|
||||
self,
|
||||
attr_def: AbstractAttrDef,
|
||||
instance_ids: List["Union[str, None]"],
|
||||
defaults: List[Any],
|
||||
label_widget: "Union[AttributeDefinitionsLabel, None]",
|
||||
):
|
||||
self.attr_def: AbstractAttrDef = attr_def
|
||||
self.instance_ids: List["Union[str, None]"] = instance_ids
|
||||
self.defaults: List[Any] = defaults
|
||||
self.label_widget: "Union[AttributeDefinitionsLabel, None]" = (
|
||||
label_widget
|
||||
)
|
||||
|
||||
|
||||
class _PublishAttrDefInfo:
|
||||
"""Helper class to store information about publish attribute definition."""
|
||||
def __init__(
|
||||
self,
|
||||
attr_def: AbstractAttrDef,
|
||||
plugin_name: str,
|
||||
instance_ids: List["Union[str, None]"],
|
||||
defaults: List[Any],
|
||||
label_widget: "Union[AttributeDefinitionsLabel, None]",
|
||||
):
|
||||
self.attr_def: AbstractAttrDef = attr_def
|
||||
self.plugin_name: str = plugin_name
|
||||
self.instance_ids: List["Union[str, None]"] = instance_ids
|
||||
self.defaults: List[Any] = defaults
|
||||
self.label_widget: "Union[AttributeDefinitionsLabel, None]" = (
|
||||
label_widget
|
||||
)
|
||||
|
||||
|
||||
class CreatorAttrsWidget(QtWidgets.QWidget):
|
||||
"""Widget showing creator specific attributes for selected instances.
|
||||
|
|
@ -51,8 +96,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._scroll_area = scroll_area
|
||||
|
||||
self._attr_def_id_to_instances = {}
|
||||
self._attr_def_id_to_attr_def = {}
|
||||
self._attr_def_info_by_id: Dict[str, _CreateAttrDefInfo] = {}
|
||||
self._current_instance_ids = set()
|
||||
|
||||
# To store content of scroll area to prevent garbage collection
|
||||
|
|
@ -81,8 +125,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
prev_content_widget.deleteLater()
|
||||
|
||||
self._content_widget = None
|
||||
self._attr_def_id_to_instances = {}
|
||||
self._attr_def_id_to_attr_def = {}
|
||||
self._attr_def_info_by_id = {}
|
||||
|
||||
result = self._controller.get_creator_attribute_definitions(
|
||||
self._current_instance_ids
|
||||
|
|
@ -97,9 +140,21 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)
|
||||
|
||||
row = 0
|
||||
for attr_def, instance_ids, values in result:
|
||||
widget = create_widget_for_attr_def(attr_def, content_widget)
|
||||
for attr_def, info_by_id in result:
|
||||
widget = create_widget_for_attr_def(
|
||||
attr_def, content_widget, handle_revert_to_default=False
|
||||
)
|
||||
default_values = []
|
||||
if attr_def.is_value_def:
|
||||
values = []
|
||||
for item in info_by_id.values():
|
||||
values.append(item["value"])
|
||||
# 'set' cannot be used for default values because they can
|
||||
# be unhashable types, e.g. 'list'.
|
||||
default = item["default"]
|
||||
if default not in default_values:
|
||||
default_values.append(default)
|
||||
|
||||
if len(values) == 1:
|
||||
value = values[0]
|
||||
if value is not None:
|
||||
|
|
@ -108,8 +163,13 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
widget.set_value(values, True)
|
||||
|
||||
widget.value_changed.connect(self._input_value_changed)
|
||||
self._attr_def_id_to_instances[attr_def.id] = instance_ids
|
||||
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
|
||||
widget.revert_to_default_requested.connect(
|
||||
self._on_request_revert_to_default
|
||||
)
|
||||
attr_def_info = _CreateAttrDefInfo(
|
||||
attr_def, list(info_by_id), default_values, None
|
||||
)
|
||||
self._attr_def_info_by_id[attr_def.id] = attr_def_info
|
||||
|
||||
if not attr_def.visible:
|
||||
continue
|
||||
|
|
@ -121,10 +181,18 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
col_num = 2 - expand_cols
|
||||
|
||||
label = None
|
||||
is_overriden = False
|
||||
if attr_def.is_value_def:
|
||||
is_overriden = any(
|
||||
item["value"] != item["default"]
|
||||
for item in info_by_id.values()
|
||||
)
|
||||
label = attr_def.label or attr_def.key
|
||||
|
||||
if label:
|
||||
label_widget = QtWidgets.QLabel(label, self)
|
||||
label_widget = AttributeDefinitionsLabel(
|
||||
attr_def.id, label, self
|
||||
)
|
||||
tooltip = attr_def.tooltip
|
||||
if tooltip:
|
||||
label_widget.setToolTip(tooltip)
|
||||
|
|
@ -138,6 +206,11 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
)
|
||||
if not attr_def.is_label_horizontal:
|
||||
row += 1
|
||||
attr_def_info.label_widget = label_widget
|
||||
label_widget.set_overridden(is_overriden)
|
||||
label_widget.revert_to_default_requested.connect(
|
||||
self._on_request_revert_to_default
|
||||
)
|
||||
|
||||
content_layout.addWidget(
|
||||
widget, row, col_num, 1, expand_cols
|
||||
|
|
@ -159,20 +232,37 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
for instance_id, changes in event["instance_changes"].items():
|
||||
if (
|
||||
instance_id in self._current_instance_ids
|
||||
and "creator_attributes" not in changes
|
||||
and "creator_attributes" in changes
|
||||
):
|
||||
self._refresh_content()
|
||||
break
|
||||
|
||||
def _input_value_changed(self, value, attr_id):
|
||||
instance_ids = self._attr_def_id_to_instances.get(attr_id)
|
||||
attr_def = self._attr_def_id_to_attr_def.get(attr_id)
|
||||
if not instance_ids or not attr_def:
|
||||
attr_def_info = self._attr_def_info_by_id.get(attr_id)
|
||||
if attr_def_info is None:
|
||||
return
|
||||
|
||||
if attr_def_info.label_widget is not None:
|
||||
defaults = attr_def_info.defaults
|
||||
is_overriden = len(defaults) != 1 or value not in defaults
|
||||
attr_def_info.label_widget.set_overridden(is_overriden)
|
||||
|
||||
self._controller.set_instances_create_attr_values(
|
||||
instance_ids, attr_def.key, value
|
||||
attr_def_info.instance_ids,
|
||||
attr_def_info.attr_def.key,
|
||||
value
|
||||
)
|
||||
|
||||
def _on_request_revert_to_default(self, attr_id):
|
||||
attr_def_info = self._attr_def_info_by_id.get(attr_id)
|
||||
if attr_def_info is None:
|
||||
return
|
||||
self._controller.revert_instances_create_attr_values(
|
||||
attr_def_info.instance_ids,
|
||||
attr_def_info.attr_def.key,
|
||||
)
|
||||
self._refresh_content()
|
||||
|
||||
|
||||
class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
||||
"""Widget showing publish plugin attributes for selected instances.
|
||||
|
|
@ -223,9 +313,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._scroll_area = scroll_area
|
||||
|
||||
self._attr_def_id_to_instances = {}
|
||||
self._attr_def_id_to_attr_def = {}
|
||||
self._attr_def_id_to_plugin_name = {}
|
||||
self._attr_def_info_by_id: Dict[str, _PublishAttrDefInfo] = {}
|
||||
|
||||
# Store content of scroll area to prevent garbage collection
|
||||
self._content_widget = None
|
||||
|
|
@ -254,9 +342,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
self._content_widget = None
|
||||
|
||||
self._attr_def_id_to_instances = {}
|
||||
self._attr_def_id_to_attr_def = {}
|
||||
self._attr_def_id_to_plugin_name = {}
|
||||
self._attr_def_info_by_id = {}
|
||||
|
||||
result = self._controller.get_publish_attribute_definitions(
|
||||
self._current_instance_ids, self._context_selected
|
||||
|
|
@ -275,12 +361,10 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
content_layout.addStretch(1)
|
||||
|
||||
row = 0
|
||||
for plugin_name, attr_defs, all_plugin_values in result:
|
||||
plugin_values = all_plugin_values[plugin_name]
|
||||
|
||||
for plugin_name, attr_defs, plugin_values in result:
|
||||
for attr_def in attr_defs:
|
||||
widget = create_widget_for_attr_def(
|
||||
attr_def, content_widget
|
||||
attr_def, content_widget, handle_revert_to_default=False
|
||||
)
|
||||
visible_widget = attr_def.visible
|
||||
# Hide unknown values of publish plugins
|
||||
|
|
@ -290,6 +374,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
widget.setVisible(False)
|
||||
visible_widget = False
|
||||
|
||||
label_widget = None
|
||||
if visible_widget:
|
||||
expand_cols = 2
|
||||
if attr_def.is_value_def and attr_def.is_label_horizontal:
|
||||
|
|
@ -300,7 +385,12 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
if attr_def.is_value_def:
|
||||
label = attr_def.label or attr_def.key
|
||||
if label:
|
||||
label_widget = QtWidgets.QLabel(label, content_widget)
|
||||
label_widget = AttributeDefinitionsLabel(
|
||||
attr_def.id, label, content_widget
|
||||
)
|
||||
label_widget.revert_to_default_requested.connect(
|
||||
self._on_request_revert_to_default
|
||||
)
|
||||
tooltip = attr_def.tooltip
|
||||
if tooltip:
|
||||
label_widget.setToolTip(tooltip)
|
||||
|
|
@ -323,38 +413,76 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
continue
|
||||
|
||||
widget.value_changed.connect(self._input_value_changed)
|
||||
widget.revert_to_default_requested.connect(
|
||||
self._on_request_revert_to_default
|
||||
)
|
||||
|
||||
attr_values = plugin_values[attr_def.key]
|
||||
multivalue = len(attr_values) > 1
|
||||
instance_ids = []
|
||||
values = []
|
||||
instances = []
|
||||
for instance, value in attr_values:
|
||||
default_values = []
|
||||
is_overriden = False
|
||||
for (instance_id, value, default_value) in (
|
||||
plugin_values.get(attr_def.key, [])
|
||||
):
|
||||
instance_ids.append(instance_id)
|
||||
values.append(value)
|
||||
instances.append(instance)
|
||||
if not is_overriden and value != default_value:
|
||||
is_overriden = True
|
||||
# 'set' cannot be used for default values because they can
|
||||
# be unhashable types, e.g. 'list'.
|
||||
if default_value not in default_values:
|
||||
default_values.append(default_value)
|
||||
|
||||
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
|
||||
self._attr_def_id_to_instances[attr_def.id] = instances
|
||||
self._attr_def_id_to_plugin_name[attr_def.id] = plugin_name
|
||||
multivalue = len(values) > 1
|
||||
|
||||
self._attr_def_info_by_id[attr_def.id] = _PublishAttrDefInfo(
|
||||
attr_def,
|
||||
plugin_name,
|
||||
instance_ids,
|
||||
default_values,
|
||||
label_widget,
|
||||
)
|
||||
|
||||
if multivalue:
|
||||
widget.set_value(values, multivalue)
|
||||
else:
|
||||
widget.set_value(values[0])
|
||||
|
||||
if label_widget is not None:
|
||||
label_widget.set_overridden(is_overriden)
|
||||
|
||||
self._scroll_area.setWidget(content_widget)
|
||||
self._content_widget = content_widget
|
||||
|
||||
def _input_value_changed(self, value, attr_id):
|
||||
instance_ids = self._attr_def_id_to_instances.get(attr_id)
|
||||
attr_def = self._attr_def_id_to_attr_def.get(attr_id)
|
||||
plugin_name = self._attr_def_id_to_plugin_name.get(attr_id)
|
||||
if not instance_ids or not attr_def or not plugin_name:
|
||||
attr_def_info = self._attr_def_info_by_id.get(attr_id)
|
||||
if attr_def_info is None:
|
||||
return
|
||||
|
||||
if attr_def_info.label_widget is not None:
|
||||
defaults = attr_def_info.defaults
|
||||
is_overriden = len(defaults) != 1 or value not in defaults
|
||||
attr_def_info.label_widget.set_overridden(is_overriden)
|
||||
|
||||
self._controller.set_instances_publish_attr_values(
|
||||
instance_ids, plugin_name, attr_def.key, value
|
||||
attr_def_info.instance_ids,
|
||||
attr_def_info.plugin_name,
|
||||
attr_def_info.attr_def.key,
|
||||
value
|
||||
)
|
||||
|
||||
def _on_request_revert_to_default(self, attr_id):
|
||||
attr_def_info = self._attr_def_info_by_id.get(attr_id)
|
||||
if attr_def_info is None:
|
||||
return
|
||||
|
||||
self._controller.revert_instances_publish_attr_values(
|
||||
attr_def_info.instance_ids,
|
||||
attr_def_info.plugin_name,
|
||||
attr_def_info.attr_def.key,
|
||||
)
|
||||
self._refresh_content()
|
||||
|
||||
def _on_instance_attr_defs_change(self, event):
|
||||
for instance_id in event.data:
|
||||
if (
|
||||
|
|
@ -370,7 +498,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
for instance_id, changes in event["instance_changes"].items():
|
||||
if (
|
||||
instance_id in self._current_instance_ids
|
||||
and "publish_attributes" not in changes
|
||||
and "publish_attributes" in changes
|
||||
):
|
||||
self._refresh_content()
|
||||
break
|
||||
|
|
|
|||
|
|
@ -214,8 +214,8 @@ class TasksCombobox(QtWidgets.QComboBox):
|
|||
Combobox gives ability to select only from intersection of task names for
|
||||
folder paths in selected instances.
|
||||
|
||||
If folder paths in selected instances does not have same tasks then combobox
|
||||
will be empty.
|
||||
If folder paths in selected instances does not have same tasks
|
||||
then combobox will be empty.
|
||||
"""
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
|
|
@ -604,7 +604,7 @@ class VariantInputWidget(PlaceholderLineEdit):
|
|||
|
||||
|
||||
class GlobalAttrsWidget(QtWidgets.QWidget):
|
||||
"""Global attributes mainly to define context and product name of instances.
|
||||
"""Global attributes to define context and product name of instances.
|
||||
|
||||
product name is or may be affected on context. Gives abiity to modify
|
||||
context and product name of instance. This change is not autopromoted but
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
tasks with same names then model is empty too.
|
||||
|
||||
Args:
|
||||
controller (AbstractPublisherFrontend): Controller which handles creation and
|
||||
publishing.
|
||||
controller (AbstractPublisherFrontend): Controller which handles
|
||||
creation and publishing.
|
||||
|
||||
"""
|
||||
def __init__(
|
||||
|
|
|
|||
|
|
@ -998,7 +998,11 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
new_item["label"] = new_item.pop("creator_label")
|
||||
new_item["identifier"] = new_item.pop("creator_identifier")
|
||||
new_failed_info.append(new_item)
|
||||
self.add_error_message_dialog(event["title"], new_failed_info, "Creator:")
|
||||
self.add_error_message_dialog(
|
||||
event["title"],
|
||||
new_failed_info,
|
||||
"Creator:"
|
||||
)
|
||||
|
||||
def _on_convertor_error(self, event):
|
||||
new_failed_info = []
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ class PushToContextController:
|
|||
return False
|
||||
|
||||
if (
|
||||
not self._user_values.new_folder_name
|
||||
self._user_values.new_folder_name is None
|
||||
and not self._selection_model.get_selected_folder_id()
|
||||
):
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from ayon_core.pipeline import Anatomy
|
|||
from ayon_core.pipeline.version_start import get_versioning_start
|
||||
from ayon_core.pipeline.template_data import get_template_data
|
||||
from ayon_core.pipeline.publish import get_publish_template_name
|
||||
from ayon_core.pipeline.create import get_product_name
|
||||
from ayon_core.pipeline.create import get_product_name, TaskNotSetError
|
||||
|
||||
UNKNOWN = object()
|
||||
|
||||
|
|
@ -823,15 +823,25 @@ class ProjectPushItemProcess:
|
|||
task_name = task_info["name"]
|
||||
task_type = task_info["taskType"]
|
||||
|
||||
product_name = get_product_name(
|
||||
self._item.dst_project_name,
|
||||
task_name,
|
||||
task_type,
|
||||
self.host_name,
|
||||
product_type,
|
||||
self._item.variant,
|
||||
project_settings=self._project_settings
|
||||
)
|
||||
try:
|
||||
product_name = get_product_name(
|
||||
self._item.dst_project_name,
|
||||
task_name,
|
||||
task_type,
|
||||
self.host_name,
|
||||
product_type,
|
||||
self._item.variant,
|
||||
project_settings=self._project_settings
|
||||
)
|
||||
except TaskNotSetError:
|
||||
self._status.set_failed(
|
||||
"Target product name template requires task name. To continue"
|
||||
" you have to select target task or change settings"
|
||||
" <b>ayon+settings://core/tools/creator/product_name_profiles"
|
||||
f"?project={self._item.dst_project_name}</b>."
|
||||
)
|
||||
raise PushToProjectError(self._status.fail_reason)
|
||||
|
||||
self._log_info(
|
||||
f"Push will be integrating to product with name '{product_name}'"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -84,8 +84,11 @@ class UserPublishValuesModel:
|
|||
return
|
||||
|
||||
self._new_folder_name = folder_name
|
||||
is_valid = True
|
||||
if folder_name:
|
||||
if folder_name is None:
|
||||
is_valid = True
|
||||
elif not folder_name:
|
||||
is_valid = False
|
||||
else:
|
||||
is_valid = (
|
||||
self.folder_name_regex.match(folder_name) is not None
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,12 +8,69 @@ from ayon_core.tools.utils import (
|
|||
ProjectsCombobox,
|
||||
FoldersWidget,
|
||||
TasksWidget,
|
||||
NiceCheckbox,
|
||||
)
|
||||
from ayon_core.tools.push_to_project.control import (
|
||||
PushToContextController,
|
||||
)
|
||||
|
||||
|
||||
class ErrorDetailDialog(QtWidgets.QDialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowTitle("Error detail")
|
||||
self.setWindowIcon(QtGui.QIcon(get_app_icon_path()))
|
||||
|
||||
title_label = QtWidgets.QLabel(self)
|
||||
|
||||
sep_1 = SeparatorWidget(parent=self)
|
||||
|
||||
detail_widget = QtWidgets.QTextBrowser(self)
|
||||
detail_widget.setReadOnly(True)
|
||||
detail_widget.setTextInteractionFlags(
|
||||
QtCore.Qt.TextBrowserInteraction
|
||||
)
|
||||
|
||||
sep_2 = SeparatorWidget(parent=self)
|
||||
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
|
||||
copy_btn = QtWidgets.QPushButton("Copy", btns_widget)
|
||||
close_btn = QtWidgets.QPushButton("Close", btns_widget)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(copy_btn, 0)
|
||||
btns_layout.addWidget(close_btn, 0)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(10, 10, 10, 10)
|
||||
main_layout.addWidget(title_label, 0)
|
||||
main_layout.addWidget(sep_1, 0)
|
||||
main_layout.addWidget(detail_widget, 1)
|
||||
main_layout.addWidget(sep_2, 0)
|
||||
main_layout.addWidget(btns_widget, 0)
|
||||
|
||||
copy_btn.clicked.connect(self._on_copy_click)
|
||||
close_btn.clicked.connect(self._on_close_click)
|
||||
|
||||
self._title_label = title_label
|
||||
self._detail_widget = detail_widget
|
||||
|
||||
def set_detail(self, title, detail):
|
||||
self._title_label.setText(title)
|
||||
self._detail_widget.setText(detail)
|
||||
|
||||
def _on_copy_click(self):
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
clipboard.setText(self._detail_widget.toPlainText())
|
||||
|
||||
def _on_close_click(self):
|
||||
self.close()
|
||||
|
||||
|
||||
class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||
def __init__(self, controller=None):
|
||||
super(PushToContextSelectWindow, self).__init__()
|
||||
|
|
@ -66,9 +123,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
# --- Inputs widget ---
|
||||
inputs_widget = QtWidgets.QWidget(main_splitter)
|
||||
|
||||
new_folder_checkbox = NiceCheckbox(True, parent=inputs_widget)
|
||||
|
||||
folder_name_input = PlaceholderLineEdit(inputs_widget)
|
||||
folder_name_input.setPlaceholderText("< Name of new folder >")
|
||||
folder_name_input.setObjectName("ValidatedLineEdit")
|
||||
folder_name_input.setEnabled(new_folder_checkbox.isChecked())
|
||||
|
||||
variant_input = PlaceholderLineEdit(inputs_widget)
|
||||
variant_input.setPlaceholderText("< Variant >")
|
||||
|
|
@ -79,6 +139,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
|
||||
inputs_layout = QtWidgets.QFormLayout(inputs_widget)
|
||||
inputs_layout.setContentsMargins(0, 0, 0, 0)
|
||||
inputs_layout.addRow("Create new folder", new_folder_checkbox)
|
||||
inputs_layout.addRow("New folder name", folder_name_input)
|
||||
inputs_layout.addRow("Variant", variant_input)
|
||||
inputs_layout.addRow("Comment", comment_input)
|
||||
|
|
@ -113,6 +174,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
|
||||
overlay_label = QtWidgets.QLabel(overlay_widget)
|
||||
overlay_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
overlay_label.setWordWrap(True)
|
||||
overlay_label.setTextInteractionFlags(
|
||||
QtCore.Qt.TextBrowserInteraction
|
||||
)
|
||||
|
||||
overlay_btns_widget = QtWidgets.QWidget(overlay_widget)
|
||||
overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
|
@ -121,13 +186,28 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
overlay_try_btn = QtWidgets.QPushButton(
|
||||
"Try again", overlay_btns_widget
|
||||
)
|
||||
overlay_try_btn.setToolTip(
|
||||
"Hide overlay and modify submit information."
|
||||
)
|
||||
|
||||
show_detail_btn = QtWidgets.QPushButton(
|
||||
"Show error detail", overlay_btns_widget
|
||||
)
|
||||
show_detail_btn.setToolTip(
|
||||
"Show error detail dialog to copy full error."
|
||||
)
|
||||
|
||||
overlay_close_btn = QtWidgets.QPushButton(
|
||||
"Close", overlay_btns_widget
|
||||
)
|
||||
overlay_close_btn.setToolTip("Discard changes and close window.")
|
||||
|
||||
overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget)
|
||||
overlay_btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
overlay_btns_layout.setSpacing(10)
|
||||
overlay_btns_layout.addStretch(1)
|
||||
overlay_btns_layout.addWidget(overlay_try_btn, 0)
|
||||
overlay_btns_layout.addWidget(show_detail_btn, 0)
|
||||
overlay_btns_layout.addWidget(overlay_close_btn, 0)
|
||||
overlay_btns_layout.addStretch(1)
|
||||
|
||||
|
|
@ -156,12 +236,14 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
main_thread_timer.timeout.connect(self._on_main_thread_timer)
|
||||
show_timer.timeout.connect(self._on_show_timer)
|
||||
user_input_changed_timer.timeout.connect(self._on_user_input_timer)
|
||||
new_folder_checkbox.stateChanged.connect(self._on_new_folder_check)
|
||||
folder_name_input.textChanged.connect(self._on_new_folder_change)
|
||||
variant_input.textChanged.connect(self._on_variant_change)
|
||||
comment_input.textChanged.connect(self._on_comment_change)
|
||||
|
||||
publish_btn.clicked.connect(self._on_select_click)
|
||||
cancel_btn.clicked.connect(self._on_close_click)
|
||||
show_detail_btn.clicked.connect(self._on_show_detail_click)
|
||||
overlay_close_btn.clicked.connect(self._on_close_click)
|
||||
overlay_try_btn.clicked.connect(self._on_try_again_click)
|
||||
|
||||
|
|
@ -203,23 +285,28 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._tasks_widget = tasks_widget
|
||||
|
||||
self._variant_input = variant_input
|
||||
self._new_folder_checkbox = new_folder_checkbox
|
||||
self._folder_name_input = folder_name_input
|
||||
self._comment_input = comment_input
|
||||
|
||||
self._publish_btn = publish_btn
|
||||
|
||||
self._overlay_widget = overlay_widget
|
||||
self._show_detail_btn = show_detail_btn
|
||||
self._overlay_close_btn = overlay_close_btn
|
||||
self._overlay_try_btn = overlay_try_btn
|
||||
self._overlay_label = overlay_label
|
||||
|
||||
self._error_detail_dialog = ErrorDetailDialog(self)
|
||||
|
||||
self._user_input_changed_timer = user_input_changed_timer
|
||||
# Store current value on input text change
|
||||
# The value is unset when is passed to controller
|
||||
# The goal is to have controll over changes happened during user change
|
||||
# in UI and controller auto-changes
|
||||
self._variant_input_text = None
|
||||
self._new_folder_name_enabled = None
|
||||
self._new_folder_name_input_text = None
|
||||
self._variant_input_text = None
|
||||
self._comment_input_text = None
|
||||
|
||||
self._first_show = True
|
||||
|
|
@ -235,6 +322,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._folder_is_valid = None
|
||||
|
||||
publish_btn.setEnabled(False)
|
||||
show_detail_btn.setVisible(False)
|
||||
overlay_close_btn.setVisible(False)
|
||||
overlay_try_btn.setVisible(False)
|
||||
|
||||
|
|
@ -289,6 +377,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
|
||||
self.refresh()
|
||||
|
||||
def _on_new_folder_check(self):
|
||||
self._new_folder_name_enabled = self._new_folder_checkbox.isChecked()
|
||||
self._folder_name_input.setEnabled(self._new_folder_name_enabled)
|
||||
self._user_input_changed_timer.start()
|
||||
|
||||
def _on_new_folder_change(self, text):
|
||||
self._new_folder_name_input_text = text
|
||||
self._user_input_changed_timer.start()
|
||||
|
|
@ -302,9 +395,15 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._user_input_changed_timer.start()
|
||||
|
||||
def _on_user_input_timer(self):
|
||||
folder_name_enabled = self._new_folder_name_enabled
|
||||
folder_name = self._new_folder_name_input_text
|
||||
if folder_name is not None:
|
||||
if folder_name is not None or folder_name_enabled is not None:
|
||||
self._new_folder_name_input_text = None
|
||||
self._new_folder_name_enabled = None
|
||||
if not self._new_folder_checkbox.isChecked():
|
||||
folder_name = None
|
||||
elif folder_name is None:
|
||||
folder_name = self._folder_name_input.text()
|
||||
self._controller.set_user_value_folder_name(folder_name)
|
||||
|
||||
variant = self._variant_input_text
|
||||
|
|
@ -350,16 +449,13 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._header_label.setText(self._controller.get_source_label())
|
||||
|
||||
def _invalidate_new_folder_name(self, folder_name, is_valid):
|
||||
self._tasks_widget.setVisible(not folder_name)
|
||||
self._tasks_widget.setVisible(folder_name is None)
|
||||
if self._folder_is_valid is is_valid:
|
||||
return
|
||||
self._folder_is_valid = is_valid
|
||||
state = ""
|
||||
if folder_name:
|
||||
if is_valid is True:
|
||||
state = "valid"
|
||||
elif is_valid is False:
|
||||
state = "invalid"
|
||||
if folder_name is not None:
|
||||
state = "valid" if is_valid else "invalid"
|
||||
set_style_property(
|
||||
self._folder_name_input, "state", state
|
||||
)
|
||||
|
|
@ -374,6 +470,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
def _on_submission_change(self, event):
|
||||
self._publish_btn.setEnabled(event["enabled"])
|
||||
|
||||
def _on_show_detail_click(self):
|
||||
self._error_detail_dialog.show()
|
||||
|
||||
def _on_close_click(self):
|
||||
self.close()
|
||||
|
||||
|
|
@ -384,8 +483,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._process_item_id = None
|
||||
self._last_submit_message = None
|
||||
|
||||
self._error_detail_dialog.close()
|
||||
|
||||
self._overlay_close_btn.setVisible(False)
|
||||
self._overlay_try_btn.setVisible(False)
|
||||
self._show_detail_btn.setVisible(False)
|
||||
self._main_layout.setCurrentWidget(self._main_context_widget)
|
||||
|
||||
def _on_main_thread_timer(self):
|
||||
|
|
@ -401,13 +503,24 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
if self._main_thread_timer_can_stop:
|
||||
self._main_thread_timer.stop()
|
||||
self._overlay_close_btn.setVisible(True)
|
||||
if push_failed and not fail_traceback:
|
||||
if push_failed:
|
||||
self._overlay_try_btn.setVisible(True)
|
||||
if fail_traceback:
|
||||
self._show_detail_btn.setVisible(True)
|
||||
|
||||
if push_failed:
|
||||
message = "Push Failed:\n{}".format(process_status["fail_reason"])
|
||||
reason = process_status["fail_reason"]
|
||||
if fail_traceback:
|
||||
message += "\n{}".format(fail_traceback)
|
||||
message = (
|
||||
"Unhandled error happened."
|
||||
" Check error detail for more information."
|
||||
)
|
||||
self._error_detail_dialog.set_detail(
|
||||
reason, fail_traceback
|
||||
)
|
||||
else:
|
||||
message = f"Push Failed:\n{reason}"
|
||||
|
||||
self._overlay_label.setText(message)
|
||||
set_style_property(self._overlay_close_btn, "state", "error")
|
||||
|
||||
|
|
|
|||
|
|
@ -86,8 +86,9 @@ class SceneInventoryController:
|
|||
self._current_folder_set = True
|
||||
return self._current_folder_id
|
||||
|
||||
def get_project_status_items(self):
|
||||
project_name = self.get_current_project_name()
|
||||
def get_project_status_items(self, project_name=None):
|
||||
if project_name is None:
|
||||
project_name = self.get_current_project_name()
|
||||
return self._projects_model.get_project_status_items(
|
||||
project_name, None
|
||||
)
|
||||
|
|
@ -105,32 +106,39 @@ class SceneInventoryController:
|
|||
def get_container_items_by_id(self, item_ids):
|
||||
return self._containers_model.get_container_items_by_id(item_ids)
|
||||
|
||||
def get_representation_info_items(self, representation_ids):
|
||||
def get_representation_info_items(self, project_name, representation_ids):
|
||||
return self._containers_model.get_representation_info_items(
|
||||
representation_ids
|
||||
project_name, representation_ids
|
||||
)
|
||||
|
||||
def get_version_items(self, product_ids):
|
||||
return self._containers_model.get_version_items(product_ids)
|
||||
def get_version_items(self, project_name, product_ids):
|
||||
return self._containers_model.get_version_items(
|
||||
project_name, product_ids)
|
||||
|
||||
# Site Sync methods
|
||||
def is_sitesync_enabled(self):
|
||||
return self._sitesync_model.is_sitesync_enabled()
|
||||
|
||||
def get_sites_information(self):
|
||||
return self._sitesync_model.get_sites_information()
|
||||
def get_sites_information(self, project_name):
|
||||
return self._sitesync_model.get_sites_information(project_name)
|
||||
|
||||
def get_site_provider_icons(self):
|
||||
return self._sitesync_model.get_site_provider_icons()
|
||||
|
||||
def get_representations_site_progress(self, representation_ids):
|
||||
def get_representations_site_progress(
|
||||
self, project_name, representation_ids
|
||||
):
|
||||
return self._sitesync_model.get_representations_site_progress(
|
||||
representation_ids
|
||||
project_name, representation_ids
|
||||
)
|
||||
|
||||
def resync_representations(self, representation_ids, site_type):
|
||||
def resync_representations(
|
||||
self, project_name, representation_ids, site_type
|
||||
):
|
||||
return self._sitesync_model.resync_representations(
|
||||
representation_ids, site_type
|
||||
project_name,
|
||||
representation_ids,
|
||||
site_type
|
||||
)
|
||||
|
||||
# Switch dialog methods
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23
|
|||
# This value hold unique value of container that should be used to identify
|
||||
# containers inbetween refresh.
|
||||
ITEM_UNIQUE_NAME_ROLE = QtCore.Qt.UserRole + 24
|
||||
PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 25
|
||||
|
||||
|
||||
class InventoryModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -52,6 +53,7 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
"Object name",
|
||||
"Active site",
|
||||
"Remote site",
|
||||
"Project",
|
||||
]
|
||||
name_col = column_labels.index("Name")
|
||||
version_col = column_labels.index("Version")
|
||||
|
|
@ -63,6 +65,7 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
object_name_col = column_labels.index("Object name")
|
||||
active_site_col = column_labels.index("Active site")
|
||||
remote_site_col = column_labels.index("Remote site")
|
||||
project_col = column_labels.index("Project")
|
||||
display_role_by_column = {
|
||||
name_col: QtCore.Qt.DisplayRole,
|
||||
version_col: VERSION_LABEL_ROLE,
|
||||
|
|
@ -72,6 +75,7 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
product_group_col: PRODUCT_GROUP_NAME_ROLE,
|
||||
loader_col: LOADER_NAME_ROLE,
|
||||
object_name_col: OBJECT_NAME_ROLE,
|
||||
project_col: PROJECT_NAME_ROLE,
|
||||
active_site_col: ACTIVE_SITE_PROGRESS_ROLE,
|
||||
remote_site_col: REMOTE_SITE_PROGRESS_ROLE,
|
||||
}
|
||||
|
|
@ -85,7 +89,7 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
foreground_role_by_column = {
|
||||
name_col: NAME_COLOR_ROLE,
|
||||
version_col: VERSION_COLOR_ROLE,
|
||||
status_col: STATUS_COLOR_ROLE
|
||||
status_col: STATUS_COLOR_ROLE,
|
||||
}
|
||||
width_by_column = {
|
||||
name_col: 250,
|
||||
|
|
@ -95,6 +99,7 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
product_type_col: 150,
|
||||
product_group_col: 120,
|
||||
loader_col: 150,
|
||||
project_col: 150,
|
||||
}
|
||||
|
||||
OUTDATED_COLOR = QtGui.QColor(235, 30, 30)
|
||||
|
|
@ -116,8 +121,8 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
|
||||
self._default_icon_color = get_default_entity_icon_color()
|
||||
|
||||
self._last_project_statuses = {}
|
||||
self._last_status_icons_by_name = {}
|
||||
self._last_project_statuses = collections.defaultdict(dict)
|
||||
self._last_status_icons_by_name = collections.defaultdict(dict)
|
||||
|
||||
def outdated(self, item):
|
||||
return item.get("isOutdated", True)
|
||||
|
|
@ -129,45 +134,73 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
|
||||
self._clear_items()
|
||||
|
||||
items_by_repre_id = {}
|
||||
project_names = set()
|
||||
repre_ids_by_project = collections.defaultdict(set)
|
||||
version_items_by_project = collections.defaultdict(dict)
|
||||
repre_info_by_id_by_project = collections.defaultdict(dict)
|
||||
item_by_repre_id_by_project = collections.defaultdict(
|
||||
lambda: collections.defaultdict(list))
|
||||
for container_item in container_items:
|
||||
# if (
|
||||
# selected is not None
|
||||
# and container_item.item_id not in selected
|
||||
# ):
|
||||
# continue
|
||||
repre_id = container_item.representation_id
|
||||
items = items_by_repre_id.setdefault(repre_id, [])
|
||||
items.append(container_item)
|
||||
project_name = container_item.project_name
|
||||
representation_id = container_item.representation_id
|
||||
project_names.add(project_name)
|
||||
repre_ids_by_project[project_name].add(representation_id)
|
||||
(
|
||||
item_by_repre_id_by_project
|
||||
[project_name]
|
||||
[representation_id]
|
||||
).append(container_item)
|
||||
|
||||
for project_name, representation_ids in repre_ids_by_project.items():
|
||||
repre_info = self._controller.get_representation_info_items(
|
||||
project_name, representation_ids
|
||||
)
|
||||
repre_info_by_id_by_project[project_name] = repre_info
|
||||
|
||||
product_ids = {
|
||||
repre_info.product_id
|
||||
for repre_info in repre_info.values()
|
||||
if repre_info.is_valid
|
||||
}
|
||||
version_items = self._controller.get_version_items(
|
||||
project_name, product_ids
|
||||
)
|
||||
version_items_by_project[project_name] = version_items
|
||||
|
||||
repre_id = set(items_by_repre_id.keys())
|
||||
repre_info_by_id = self._controller.get_representation_info_items(
|
||||
repre_id
|
||||
)
|
||||
product_ids = {
|
||||
repre_info.product_id
|
||||
for repre_info in repre_info_by_id.values()
|
||||
if repre_info.is_valid
|
||||
}
|
||||
version_items_by_product_id = self._controller.get_version_items(
|
||||
product_ids
|
||||
)
|
||||
# SiteSync addon information
|
||||
progress_by_id = self._controller.get_representations_site_progress(
|
||||
repre_id
|
||||
)
|
||||
sites_info = self._controller.get_sites_information()
|
||||
progress_by_project = {
|
||||
project_name: self._controller.get_representations_site_progress(
|
||||
project_name, repre_ids
|
||||
)
|
||||
for project_name, repre_ids in repre_ids_by_project.items()
|
||||
}
|
||||
|
||||
sites_info_by_project_name = {
|
||||
project_name: self._controller.get_sites_information(project_name)
|
||||
for project_name in project_names
|
||||
}
|
||||
site_icons = {
|
||||
provider: get_qt_icon(icon_def)
|
||||
for provider, icon_def in (
|
||||
self._controller.get_site_provider_icons().items()
|
||||
)
|
||||
}
|
||||
self._last_project_statuses = {
|
||||
status_item.name: status_item
|
||||
for status_item in self._controller.get_project_status_items()
|
||||
}
|
||||
self._last_status_icons_by_name = {}
|
||||
last_project_statuses = collections.defaultdict(dict)
|
||||
for project_name in project_names:
|
||||
status_items_by_name = {
|
||||
status_item.name: status_item
|
||||
for status_item in self._controller.get_project_status_items(
|
||||
project_name
|
||||
)
|
||||
}
|
||||
last_project_statuses[project_name] = status_items_by_name
|
||||
self._last_project_statuses = last_project_statuses
|
||||
self._last_status_icons_by_name = collections.defaultdict(dict)
|
||||
|
||||
group_item_icon = qtawesome.icon(
|
||||
"fa.folder", color=self._default_icon_color
|
||||
|
|
@ -187,117 +220,130 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
group_item_font = QtGui.QFont()
|
||||
group_item_font.setBold(True)
|
||||
|
||||
active_site_icon = site_icons.get(sites_info["active_site_provider"])
|
||||
remote_site_icon = site_icons.get(sites_info["remote_site_provider"])
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
group_items = []
|
||||
for repre_id, container_items in items_by_repre_id.items():
|
||||
repre_info = repre_info_by_id[repre_id]
|
||||
version_label = "N/A"
|
||||
version_color = None
|
||||
is_latest = False
|
||||
is_hero = False
|
||||
status_name = None
|
||||
if not repre_info.is_valid:
|
||||
group_name = "< Entity N/A >"
|
||||
item_icon = invalid_item_icon
|
||||
for project_name, items_by_repre_id in (
|
||||
item_by_repre_id_by_project.items()
|
||||
):
|
||||
sites_info = sites_info_by_project_name[project_name]
|
||||
active_site_icon = site_icons.get(
|
||||
sites_info["active_site_provider"]
|
||||
)
|
||||
remote_site_icon = site_icons.get(
|
||||
sites_info["remote_site_provider"]
|
||||
)
|
||||
|
||||
else:
|
||||
group_name = "{}_{}: ({})".format(
|
||||
repre_info.folder_path.rsplit("/")[-1],
|
||||
repre_info.product_name,
|
||||
repre_info.representation_name
|
||||
progress_by_id = progress_by_project[project_name]
|
||||
repre_info_by_id = repre_info_by_id_by_project[project_name]
|
||||
version_items_by_product_id = (
|
||||
version_items_by_project[project_name]
|
||||
)
|
||||
for repre_id, container_items in items_by_repre_id.items():
|
||||
repre_info = repre_info_by_id[repre_id]
|
||||
version_color = None
|
||||
if not repre_info.is_valid:
|
||||
version_label = "N/A"
|
||||
group_name = "< Entity N/A >"
|
||||
item_icon = invalid_item_icon
|
||||
is_latest = False
|
||||
is_hero = False
|
||||
status_name = None
|
||||
|
||||
else:
|
||||
group_name = "{}_{}: ({})".format(
|
||||
repre_info.folder_path.rsplit("/")[-1],
|
||||
repre_info.product_name,
|
||||
repre_info.representation_name
|
||||
)
|
||||
item_icon = valid_item_icon
|
||||
|
||||
version_items = (
|
||||
version_items_by_product_id[repre_info.product_id]
|
||||
)
|
||||
version_item = version_items[repre_info.version_id]
|
||||
version_label = format_version(version_item.version)
|
||||
is_hero = version_item.version < 0
|
||||
is_latest = version_item.is_latest
|
||||
if not version_item.is_latest:
|
||||
version_color = self.OUTDATED_COLOR
|
||||
status_name = version_item.status
|
||||
|
||||
(
|
||||
status_color, status_short, status_icon
|
||||
) = self._get_status_data(project_name, status_name)
|
||||
|
||||
repre_name = (
|
||||
repre_info.representation_name or
|
||||
"<unknown representation>"
|
||||
)
|
||||
item_icon = valid_item_icon
|
||||
container_model_items = []
|
||||
for container_item in container_items:
|
||||
object_name = container_item.object_name or "<none>"
|
||||
unique_name = repre_name + object_name
|
||||
item = QtGui.QStandardItem()
|
||||
item.setColumnCount(root_item.columnCount())
|
||||
item.setData(container_item.namespace,
|
||||
QtCore.Qt.DisplayRole)
|
||||
item.setData(self.GRAYOUT_COLOR, NAME_COLOR_ROLE)
|
||||
item.setData(self.GRAYOUT_COLOR, VERSION_COLOR_ROLE)
|
||||
item.setData(item_icon, QtCore.Qt.DecorationRole)
|
||||
item.setData(repre_info.product_id, PRODUCT_ID_ROLE)
|
||||
item.setData(container_item.item_id, ITEM_ID_ROLE)
|
||||
item.setData(version_label, VERSION_LABEL_ROLE)
|
||||
item.setData(container_item.loader_name, LOADER_NAME_ROLE)
|
||||
item.setData(container_item.object_name, OBJECT_NAME_ROLE)
|
||||
item.setData(True, IS_CONTAINER_ITEM_ROLE)
|
||||
item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE)
|
||||
container_model_items.append(item)
|
||||
|
||||
version_items = (
|
||||
version_items_by_product_id[repre_info.product_id]
|
||||
progress = progress_by_id[repre_id]
|
||||
active_site_progress = "{}%".format(
|
||||
max(progress["active_site"], 0) * 100
|
||||
)
|
||||
remote_site_progress = "{}%".format(
|
||||
max(progress["remote_site"], 0) * 100
|
||||
)
|
||||
version_item = version_items[repre_info.version_id]
|
||||
version_label = format_version(version_item.version)
|
||||
is_hero = version_item.version < 0
|
||||
if not version_item.is_latest:
|
||||
version_color = self.OUTDATED_COLOR
|
||||
status_name = version_item.status
|
||||
|
||||
status_color, status_short, status_icon = self._get_status_data(
|
||||
status_name
|
||||
)
|
||||
group_item = QtGui.QStandardItem()
|
||||
group_item.setColumnCount(root_item.columnCount())
|
||||
group_item.setData(group_name, QtCore.Qt.DisplayRole)
|
||||
group_item.setData(group_name, ITEM_UNIQUE_NAME_ROLE)
|
||||
group_item.setData(group_item_icon, QtCore.Qt.DecorationRole)
|
||||
group_item.setData(group_item_font, QtCore.Qt.FontRole)
|
||||
group_item.setData(repre_info.product_id, PRODUCT_ID_ROLE)
|
||||
group_item.setData(repre_info.product_type, PRODUCT_TYPE_ROLE)
|
||||
group_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)
|
||||
group_item.setData(is_latest, VERSION_IS_LATEST_ROLE)
|
||||
group_item.setData(is_hero, VERSION_IS_HERO_ROLE)
|
||||
group_item.setData(version_label, VERSION_LABEL_ROLE)
|
||||
group_item.setData(len(container_items), COUNT_ROLE)
|
||||
group_item.setData(status_name, STATUS_NAME_ROLE)
|
||||
group_item.setData(status_short, STATUS_SHORT_ROLE)
|
||||
group_item.setData(status_color, STATUS_COLOR_ROLE)
|
||||
group_item.setData(status_icon, STATUS_ICON_ROLE)
|
||||
group_item.setData(project_name, PROJECT_NAME_ROLE)
|
||||
|
||||
repre_name = (
|
||||
repre_info.representation_name or "<unknown representation>"
|
||||
)
|
||||
container_model_items = []
|
||||
for container_item in container_items:
|
||||
object_name = container_item.object_name or "<none>"
|
||||
unique_name = repre_name + object_name
|
||||
|
||||
item = QtGui.QStandardItem()
|
||||
item.setColumnCount(root_item.columnCount())
|
||||
item.setData(container_item.namespace, QtCore.Qt.DisplayRole)
|
||||
item.setData(self.GRAYOUT_COLOR, NAME_COLOR_ROLE)
|
||||
item.setData(self.GRAYOUT_COLOR, VERSION_COLOR_ROLE)
|
||||
item.setData(item_icon, QtCore.Qt.DecorationRole)
|
||||
item.setData(repre_info.product_id, PRODUCT_ID_ROLE)
|
||||
item.setData(container_item.item_id, ITEM_ID_ROLE)
|
||||
item.setData(version_label, VERSION_LABEL_ROLE)
|
||||
item.setData(container_item.loader_name, LOADER_NAME_ROLE)
|
||||
item.setData(container_item.object_name, OBJECT_NAME_ROLE)
|
||||
item.setData(True, IS_CONTAINER_ITEM_ROLE)
|
||||
item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE)
|
||||
container_model_items.append(item)
|
||||
|
||||
if not container_model_items:
|
||||
continue
|
||||
|
||||
progress = progress_by_id[repre_id]
|
||||
active_site_progress = "{}%".format(
|
||||
max(progress["active_site"], 0) * 100
|
||||
)
|
||||
remote_site_progress = "{}%".format(
|
||||
max(progress["remote_site"], 0) * 100
|
||||
)
|
||||
|
||||
group_item = QtGui.QStandardItem()
|
||||
group_item.setColumnCount(root_item.columnCount())
|
||||
group_item.setData(group_name, QtCore.Qt.DisplayRole)
|
||||
group_item.setData(group_name, ITEM_UNIQUE_NAME_ROLE)
|
||||
group_item.setData(group_item_icon, QtCore.Qt.DecorationRole)
|
||||
group_item.setData(group_item_font, QtCore.Qt.FontRole)
|
||||
group_item.setData(repre_info.product_id, PRODUCT_ID_ROLE)
|
||||
group_item.setData(repre_info.product_type, PRODUCT_TYPE_ROLE)
|
||||
group_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)
|
||||
group_item.setData(is_latest, VERSION_IS_LATEST_ROLE)
|
||||
group_item.setData(is_hero, VERSION_IS_HERO_ROLE)
|
||||
group_item.setData(version_label, VERSION_LABEL_ROLE)
|
||||
group_item.setData(len(container_items), COUNT_ROLE)
|
||||
group_item.setData(status_name, STATUS_NAME_ROLE)
|
||||
group_item.setData(status_short, STATUS_SHORT_ROLE)
|
||||
group_item.setData(status_color, STATUS_COLOR_ROLE)
|
||||
group_item.setData(status_icon, STATUS_ICON_ROLE)
|
||||
|
||||
group_item.setData(
|
||||
active_site_progress, ACTIVE_SITE_PROGRESS_ROLE
|
||||
)
|
||||
group_item.setData(
|
||||
remote_site_progress, REMOTE_SITE_PROGRESS_ROLE
|
||||
)
|
||||
group_item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE)
|
||||
group_item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE)
|
||||
group_item.setData(False, IS_CONTAINER_ITEM_ROLE)
|
||||
|
||||
if version_color is not None:
|
||||
group_item.setData(version_color, VERSION_COLOR_ROLE)
|
||||
|
||||
if repre_info.product_group:
|
||||
group_item.setData(
|
||||
repre_info.product_group, PRODUCT_GROUP_NAME_ROLE
|
||||
active_site_progress, ACTIVE_SITE_PROGRESS_ROLE
|
||||
)
|
||||
group_item.setData(group_icon, PRODUCT_GROUP_ICON_ROLE)
|
||||
group_item.setData(
|
||||
remote_site_progress, REMOTE_SITE_PROGRESS_ROLE
|
||||
)
|
||||
group_item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE)
|
||||
group_item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE)
|
||||
group_item.setData(False, IS_CONTAINER_ITEM_ROLE)
|
||||
|
||||
group_item.appendRows(container_model_items)
|
||||
group_items.append(group_item)
|
||||
if version_color is not None:
|
||||
group_item.setData(version_color, VERSION_COLOR_ROLE)
|
||||
|
||||
if repre_info.product_group:
|
||||
group_item.setData(
|
||||
repre_info.product_group, PRODUCT_GROUP_NAME_ROLE
|
||||
)
|
||||
group_item.setData(group_icon, PRODUCT_GROUP_ICON_ROLE)
|
||||
|
||||
group_item.appendRows(container_model_items)
|
||||
group_items.append(group_item)
|
||||
|
||||
if group_items:
|
||||
root_item.appendRows(group_items)
|
||||
|
|
@ -358,17 +404,21 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
def _get_status_data(self, status_name):
|
||||
status_item = self._last_project_statuses.get(status_name)
|
||||
status_icon = self._get_status_icon(status_name, status_item)
|
||||
def _get_status_data(self, project_name, status_name):
|
||||
status_item = self._last_project_statuses[project_name].get(
|
||||
status_name
|
||||
)
|
||||
status_icon = self._get_status_icon(
|
||||
project_name, status_name, status_item
|
||||
)
|
||||
status_color = status_short = None
|
||||
if status_item is not None:
|
||||
status_color = status_item.color
|
||||
status_short = status_item.short
|
||||
return status_color, status_short, status_icon
|
||||
|
||||
def _get_status_icon(self, status_name, status_item):
|
||||
icon = self._last_status_icons_by_name.get(status_name)
|
||||
def _get_status_icon(self, project_name, status_name, status_item):
|
||||
icon = self._last_status_icons_by_name[project_name].get(status_name)
|
||||
if icon is not None:
|
||||
return icon
|
||||
|
||||
|
|
@ -381,7 +431,7 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
})
|
||||
if icon is None:
|
||||
icon = QtGui.QIcon()
|
||||
self._last_status_icons_by_name[status_name] = icon
|
||||
self._last_status_icons_by_name[project_name][status_name] = icon
|
||||
return icon
|
||||
|
||||
|
||||
|
|
@ -425,7 +475,7 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
|||
state = bool(state)
|
||||
|
||||
if state != self._filter_outdated:
|
||||
self._filter_outdated = bool(state)
|
||||
self._filter_outdated = state
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_hierarchy_view(self, state):
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import collections
|
|||
import ayon_api
|
||||
from ayon_api.graphql import GraphQlQuery
|
||||
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.host import ILoadHost
|
||||
from ayon_core.tools.common_models.projects import StatusStates
|
||||
|
||||
|
|
@ -93,22 +94,27 @@ class ContainerItem:
|
|||
loader_name,
|
||||
namespace,
|
||||
object_name,
|
||||
item_id
|
||||
item_id,
|
||||
project_name
|
||||
):
|
||||
self.representation_id = representation_id
|
||||
self.loader_name = loader_name
|
||||
self.object_name = object_name
|
||||
self.namespace = namespace
|
||||
self.item_id = item_id
|
||||
self.project_name = project_name
|
||||
|
||||
@classmethod
|
||||
def from_container_data(cls, container):
|
||||
def from_container_data(cls, current_project_name, container):
|
||||
return cls(
|
||||
representation_id=container["representation"],
|
||||
loader_name=container["loader"],
|
||||
namespace=container["namespace"],
|
||||
object_name=container["objectName"],
|
||||
item_id=uuid.uuid4().hex,
|
||||
project_name=container.get(
|
||||
"project_name", current_project_name
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -191,6 +197,7 @@ class ContainersModel:
|
|||
self._container_items_by_id = {}
|
||||
self._version_items_by_product_id = {}
|
||||
self._repre_info_by_id = {}
|
||||
self._log = Logger.get_logger("ContainersModel")
|
||||
|
||||
def reset(self):
|
||||
self._items_cache = None
|
||||
|
|
@ -219,26 +226,23 @@ class ContainersModel:
|
|||
for item_id in item_ids
|
||||
}
|
||||
|
||||
def get_representation_info_items(self, representation_ids):
|
||||
def get_representation_info_items(self, project_name, representation_ids):
|
||||
output = {}
|
||||
missing_repre_ids = set()
|
||||
for repre_id in representation_ids:
|
||||
try:
|
||||
uuid.UUID(repre_id)
|
||||
except ValueError:
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
output[repre_id] = RepresentationInfo.new_invalid()
|
||||
continue
|
||||
|
||||
repre_info = self._repre_info_by_id.get(repre_id)
|
||||
if repre_info is None:
|
||||
missing_repre_ids.add(repre_id)
|
||||
else:
|
||||
output[repre_id] = repre_info
|
||||
|
||||
if not missing_repre_ids:
|
||||
return output
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
repre_hierarchy_by_id = get_representations_hierarchy(
|
||||
project_name, missing_repre_ids
|
||||
)
|
||||
|
|
@ -276,10 +280,9 @@ class ContainersModel:
|
|||
output[repre_id] = repre_info
|
||||
return output
|
||||
|
||||
def get_version_items(self, product_ids):
|
||||
def get_version_items(self, project_name, product_ids):
|
||||
if not product_ids:
|
||||
return {}
|
||||
|
||||
missing_ids = {
|
||||
product_id
|
||||
for product_id in product_ids
|
||||
|
|
@ -294,7 +297,6 @@ class ContainersModel:
|
|||
def version_sorted(entity):
|
||||
return entity["version"]
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
version_entities_by_product_id = {
|
||||
product_id: []
|
||||
for product_id in missing_ids
|
||||
|
|
@ -348,34 +350,45 @@ class ContainersModel:
|
|||
return
|
||||
|
||||
host = self._controller.get_host()
|
||||
if isinstance(host, ILoadHost):
|
||||
containers = list(host.get_containers())
|
||||
elif hasattr(host, "ls"):
|
||||
containers = list(host.ls())
|
||||
else:
|
||||
containers = []
|
||||
containers = []
|
||||
try:
|
||||
if isinstance(host, ILoadHost):
|
||||
containers = list(host.get_containers())
|
||||
elif hasattr(host, "ls"):
|
||||
containers = list(host.ls())
|
||||
except Exception:
|
||||
self._log.error("Failed to get containers", exc_info=True)
|
||||
|
||||
container_items = []
|
||||
containers_by_id = {}
|
||||
container_items_by_id = {}
|
||||
invalid_ids_mapping = {}
|
||||
current_project_name = self._controller.get_current_project_name()
|
||||
for container in containers:
|
||||
if not container:
|
||||
continue
|
||||
|
||||
try:
|
||||
item = ContainerItem.from_container_data(container)
|
||||
item = ContainerItem.from_container_data(
|
||||
current_project_name, container)
|
||||
repre_id = item.representation_id
|
||||
try:
|
||||
uuid.UUID(repre_id)
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
# Fake not existing representation id so container is shown in UI
|
||||
# but as invalid
|
||||
self._log.warning(
|
||||
"Container contains invalid representation id."
|
||||
f"\n{container}"
|
||||
)
|
||||
# Fake not existing representation id so container
|
||||
# is shown in UI but as invalid
|
||||
item.representation_id = invalid_ids_mapping.setdefault(
|
||||
repre_id, uuid.uuid4().hex
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# skip item if required data are missing
|
||||
self._controller.log_error(
|
||||
f"Failed to create item: {e}"
|
||||
self._log.warning(
|
||||
"Failed to create container item", exc_info=True
|
||||
)
|
||||
continue
|
||||
|
||||
|
|
@ -383,7 +396,6 @@ class ContainersModel:
|
|||
container_items_by_id[item.item_id] = item
|
||||
container_items.append(item)
|
||||
|
||||
|
||||
self._containers_by_id = containers_by_id
|
||||
self._container_items_by_id = container_items_by_id
|
||||
self._items_cache = container_items
|
||||
|
|
|
|||
|
|
@ -11,18 +11,18 @@ class SiteSyncModel:
|
|||
|
||||
self._sitesync_addon = NOT_SET
|
||||
self._sitesync_enabled = None
|
||||
self._active_site = NOT_SET
|
||||
self._remote_site = NOT_SET
|
||||
self._active_site_provider = NOT_SET
|
||||
self._remote_site_provider = NOT_SET
|
||||
self._active_site = {}
|
||||
self._remote_site = {}
|
||||
self._active_site_provider = {}
|
||||
self._remote_site_provider = {}
|
||||
|
||||
def reset(self):
|
||||
self._sitesync_addon = NOT_SET
|
||||
self._sitesync_enabled = None
|
||||
self._active_site = NOT_SET
|
||||
self._remote_site = NOT_SET
|
||||
self._active_site_provider = NOT_SET
|
||||
self._remote_site_provider = NOT_SET
|
||||
self._active_site = {}
|
||||
self._remote_site = {}
|
||||
self._active_site_provider = {}
|
||||
self._remote_site_provider = {}
|
||||
|
||||
def is_sitesync_enabled(self):
|
||||
"""Site sync is enabled.
|
||||
|
|
@ -46,15 +46,21 @@ class SiteSyncModel:
|
|||
sitesync_addon = self._get_sitesync_addon()
|
||||
return sitesync_addon.get_site_icons()
|
||||
|
||||
def get_sites_information(self):
|
||||
def get_sites_information(self, project_name):
|
||||
return {
|
||||
"active_site": self._get_active_site(),
|
||||
"active_site_provider": self._get_active_site_provider(),
|
||||
"remote_site": self._get_remote_site(),
|
||||
"remote_site_provider": self._get_remote_site_provider()
|
||||
"active_site": self._get_active_site(project_name),
|
||||
"remote_site": self._get_remote_site(project_name),
|
||||
"active_site_provider": self._get_active_site_provider(
|
||||
project_name
|
||||
),
|
||||
"remote_site_provider": self._get_remote_site_provider(
|
||||
project_name
|
||||
)
|
||||
}
|
||||
|
||||
def get_representations_site_progress(self, representation_ids):
|
||||
def get_representations_site_progress(
|
||||
self, project_name, representation_ids
|
||||
):
|
||||
"""Get progress of representations sync."""
|
||||
|
||||
representation_ids = set(representation_ids)
|
||||
|
|
@ -68,13 +74,12 @@ class SiteSyncModel:
|
|||
if not self.is_sitesync_enabled():
|
||||
return output
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
sitesync_addon = self._get_sitesync_addon()
|
||||
repre_entities = ayon_api.get_representations(
|
||||
project_name, representation_ids
|
||||
)
|
||||
active_site = self._get_active_site()
|
||||
remote_site = self._get_remote_site()
|
||||
active_site = self._get_active_site(project_name)
|
||||
remote_site = self._get_remote_site(project_name)
|
||||
|
||||
for repre_entity in repre_entities:
|
||||
repre_output = output[repre_entity["id"]]
|
||||
|
|
@ -86,20 +91,21 @@ class SiteSyncModel:
|
|||
|
||||
return output
|
||||
|
||||
def resync_representations(self, representation_ids, site_type):
|
||||
def resync_representations(
|
||||
self, project_name, representation_ids, site_type
|
||||
):
|
||||
"""
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
representation_ids (Iterable[str]): Representation ids.
|
||||
site_type (Literal[active_site, remote_site]): Site type.
|
||||
"""
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
sitesync_addon = self._get_sitesync_addon()
|
||||
active_site = self._get_active_site()
|
||||
remote_site = self._get_remote_site()
|
||||
active_site = self._get_active_site(project_name)
|
||||
remote_site = self._get_remote_site(project_name)
|
||||
progress = self.get_representations_site_progress(
|
||||
representation_ids
|
||||
project_name, representation_ids
|
||||
)
|
||||
for repre_id in representation_ids:
|
||||
repre_progress = progress.get(repre_id)
|
||||
|
|
@ -132,48 +138,49 @@ class SiteSyncModel:
|
|||
self._sitesync_addon = sitesync_addon
|
||||
self._sitesync_enabled = sync_enabled
|
||||
|
||||
def _get_active_site(self):
|
||||
if self._active_site is NOT_SET:
|
||||
self._cache_sites()
|
||||
return self._active_site
|
||||
def _get_active_site(self, project_name):
|
||||
if project_name not in self._active_site:
|
||||
self._cache_sites(project_name)
|
||||
return self._active_site[project_name]
|
||||
|
||||
def _get_remote_site(self):
|
||||
if self._remote_site is NOT_SET:
|
||||
self._cache_sites()
|
||||
return self._remote_site
|
||||
def _get_remote_site(self, project_name):
|
||||
if project_name not in self._remote_site:
|
||||
self._cache_sites(project_name)
|
||||
return self._remote_site[project_name]
|
||||
|
||||
def _get_active_site_provider(self):
|
||||
if self._active_site_provider is NOT_SET:
|
||||
self._cache_sites()
|
||||
return self._active_site_provider
|
||||
def _get_active_site_provider(self, project_name):
|
||||
if project_name not in self._active_site_provider:
|
||||
self._cache_sites(project_name)
|
||||
return self._active_site_provider[project_name]
|
||||
|
||||
def _get_remote_site_provider(self):
|
||||
if self._remote_site_provider is NOT_SET:
|
||||
self._cache_sites()
|
||||
return self._remote_site_provider
|
||||
def _get_remote_site_provider(self, project_name):
|
||||
if project_name not in self._remote_site_provider:
|
||||
self._cache_sites(project_name)
|
||||
return self._remote_site_provider[project_name]
|
||||
|
||||
def _cache_sites(self):
|
||||
active_site = None
|
||||
remote_site = None
|
||||
active_site_provider = None
|
||||
remote_site_provider = None
|
||||
if self.is_sitesync_enabled():
|
||||
sitesync_addon = self._get_sitesync_addon()
|
||||
project_name = self._controller.get_current_project_name()
|
||||
active_site = sitesync_addon.get_active_site(project_name)
|
||||
remote_site = sitesync_addon.get_remote_site(project_name)
|
||||
active_site_provider = "studio"
|
||||
remote_site_provider = "studio"
|
||||
if active_site != "studio":
|
||||
active_site_provider = sitesync_addon.get_provider_for_site(
|
||||
project_name, active_site
|
||||
)
|
||||
if remote_site != "studio":
|
||||
remote_site_provider = sitesync_addon.get_provider_for_site(
|
||||
project_name, remote_site
|
||||
)
|
||||
def _cache_sites(self, project_name):
|
||||
self._active_site[project_name] = None
|
||||
self._remote_site[project_name] = None
|
||||
self._active_site_provider[project_name] = None
|
||||
self._remote_site_provider[project_name] = None
|
||||
if not self.is_sitesync_enabled():
|
||||
return
|
||||
|
||||
self._active_site = active_site
|
||||
self._remote_site = remote_site
|
||||
self._active_site_provider = active_site_provider
|
||||
self._remote_site_provider = remote_site_provider
|
||||
sitesync_addon = self._get_sitesync_addon()
|
||||
active_site = sitesync_addon.get_active_site(project_name)
|
||||
remote_site = sitesync_addon.get_remote_site(project_name)
|
||||
active_site_provider = "studio"
|
||||
remote_site_provider = "studio"
|
||||
if active_site != "studio":
|
||||
active_site_provider = sitesync_addon.get_provider_for_site(
|
||||
project_name, active_site
|
||||
)
|
||||
if remote_site != "studio":
|
||||
remote_site_provider = sitesync_addon.get_provider_for_site(
|
||||
project_name, remote_site
|
||||
)
|
||||
|
||||
self._active_site[project_name] = active_site
|
||||
self._remote_site[project_name] = remote_site
|
||||
self._active_site_provider[project_name] = active_site_provider
|
||||
self._remote_site_provider[project_name] = remote_site_provider
|
||||
|
|
|
|||
|
|
@ -46,8 +46,13 @@ class SwitchAssetDialog(QtWidgets.QDialog):
|
|||
|
||||
switched = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent=None, items=None):
|
||||
super(SwitchAssetDialog, self).__init__(parent)
|
||||
def __init__(self, controller, project_name, items, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
current_project_name = controller.get_current_project_name()
|
||||
folder_id = None
|
||||
if current_project_name == project_name:
|
||||
folder_id = controller.get_current_folder_id()
|
||||
|
||||
self.setWindowTitle("Switch selected items ...")
|
||||
|
||||
|
|
@ -147,11 +152,10 @@ class SwitchAssetDialog(QtWidgets.QDialog):
|
|||
self._init_repre_name = None
|
||||
|
||||
self._fill_check = False
|
||||
self._project_name = project_name
|
||||
self._folder_id = folder_id
|
||||
|
||||
self._project_name = controller.get_current_project_name()
|
||||
self._folder_id = controller.get_current_folder_id()
|
||||
|
||||
self._current_folder_btn.setEnabled(self._folder_id is not None)
|
||||
self._current_folder_btn.setEnabled(folder_id is not None)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
|
|
@ -159,7 +163,7 @@ class SwitchAssetDialog(QtWidgets.QDialog):
|
|||
self._prepare_content_data()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(SwitchAssetDialog, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
self._show_timer.start()
|
||||
|
||||
def refresh(self, init_refresh=False):
|
||||
|
|
|
|||
|
|
@ -192,29 +192,46 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
container_item = container_items_by_id[item_id]
|
||||
active_repre_id = container_item.representation_id
|
||||
break
|
||||
repre_ids_by_project = collections.defaultdict(set)
|
||||
for container_item in container_items_by_id.values():
|
||||
repre_id = container_item.representation_id
|
||||
project_name = container_item.project_name
|
||||
repre_ids_by_project[project_name].add(repre_id)
|
||||
|
||||
repre_info_by_id = self._controller.get_representation_info_items({
|
||||
container_item.representation_id
|
||||
for container_item in container_items_by_id.values()
|
||||
})
|
||||
valid_repre_ids = {
|
||||
repre_id
|
||||
for repre_id, repre_info in repre_info_by_id.items()
|
||||
if repre_info.is_valid
|
||||
}
|
||||
repre_info_by_project = {}
|
||||
repre_ids_by_project_name = {}
|
||||
version_ids_by_project = {}
|
||||
product_ids_by_project = {}
|
||||
for project_name, repre_ids in repre_ids_by_project.items():
|
||||
repres_info = self._controller.get_representation_info_items(
|
||||
project_name, repre_ids
|
||||
)
|
||||
|
||||
repre_info_by_project[project_name] = repres_info
|
||||
repre_ids = set()
|
||||
version_ids = set()
|
||||
product_ids = set()
|
||||
for repre_id, repre_info in repres_info.items():
|
||||
if not repre_info.is_valid:
|
||||
continue
|
||||
repre_ids.add(repre_id)
|
||||
version_ids.add(repre_info.version_id)
|
||||
product_ids.add(repre_info.product_id)
|
||||
|
||||
repre_ids_by_project_name[project_name] = repre_ids
|
||||
version_ids_by_project[project_name] = version_ids
|
||||
product_ids_by_project[project_name] = product_ids
|
||||
|
||||
# Exclude items that are "NOT FOUND" since setting versions, updating
|
||||
# and removal won't work for those items.
|
||||
filtered_items = []
|
||||
product_ids = set()
|
||||
version_ids = set()
|
||||
for container_item in container_items_by_id.values():
|
||||
project_name = container_item.project_name
|
||||
repre_id = container_item.representation_id
|
||||
repre_info_by_id = repre_info_by_project.get(project_name, {})
|
||||
repre_info = repre_info_by_id.get(repre_id)
|
||||
if repre_info and repre_info.is_valid:
|
||||
filtered_items.append(container_item)
|
||||
version_ids.add(repre_info.version_id)
|
||||
product_ids.add(repre_info.product_id)
|
||||
|
||||
# remove
|
||||
remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR)
|
||||
|
|
@ -227,43 +244,51 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
menu.addAction(remove_action)
|
||||
return
|
||||
|
||||
version_items_by_product_id = self._controller.get_version_items(
|
||||
product_ids
|
||||
)
|
||||
version_items_by_project = {
|
||||
project_name: self._controller.get_version_items(
|
||||
project_name, product_ids
|
||||
)
|
||||
for project_name, product_ids in product_ids_by_project.items()
|
||||
}
|
||||
|
||||
has_outdated = False
|
||||
has_loaded_hero_versions = False
|
||||
has_available_hero_version = False
|
||||
has_outdated_approved = False
|
||||
last_version_by_product_id = {}
|
||||
for product_id, version_items_by_id in (
|
||||
version_items_by_product_id.items()
|
||||
for project_name, version_items_by_product_id in (
|
||||
version_items_by_project.items()
|
||||
):
|
||||
_has_outdated_approved = False
|
||||
_last_approved_version_item = None
|
||||
for version_item in version_items_by_id.values():
|
||||
if version_item.is_hero:
|
||||
has_available_hero_version = True
|
||||
|
||||
elif version_item.is_last_approved:
|
||||
_last_approved_version_item = version_item
|
||||
_has_outdated_approved = True
|
||||
|
||||
if version_item.version_id not in version_ids:
|
||||
continue
|
||||
|
||||
if version_item.is_hero:
|
||||
has_loaded_hero_versions = True
|
||||
elif not version_item.is_latest:
|
||||
has_outdated = True
|
||||
|
||||
if (
|
||||
_has_outdated_approved
|
||||
and _last_approved_version_item is not None
|
||||
version_ids = version_ids_by_project[project_name]
|
||||
for product_id, version_items_by_id in (
|
||||
version_items_by_product_id.items()
|
||||
):
|
||||
last_version_by_product_id[product_id] = (
|
||||
_last_approved_version_item
|
||||
)
|
||||
has_outdated_approved = True
|
||||
_has_outdated_approved = False
|
||||
_last_approved_version_item = None
|
||||
for version_item in version_items_by_id.values():
|
||||
if version_item.is_hero:
|
||||
has_available_hero_version = True
|
||||
|
||||
elif version_item.is_last_approved:
|
||||
_last_approved_version_item = version_item
|
||||
_has_outdated_approved = True
|
||||
|
||||
if version_item.version_id not in version_ids:
|
||||
continue
|
||||
|
||||
if version_item.is_hero:
|
||||
has_loaded_hero_versions = True
|
||||
elif not version_item.is_latest:
|
||||
has_outdated = True
|
||||
|
||||
if (
|
||||
_has_outdated_approved
|
||||
and _last_approved_version_item is not None
|
||||
):
|
||||
last_version_by_product_id[product_id] = (
|
||||
_last_approved_version_item
|
||||
)
|
||||
has_outdated_approved = True
|
||||
|
||||
switch_to_versioned = None
|
||||
if has_loaded_hero_versions:
|
||||
|
|
@ -284,8 +309,9 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
approved_version_by_item_id = {}
|
||||
if has_outdated_approved:
|
||||
for container_item in container_items_by_id.values():
|
||||
project_name = container_item.project_name
|
||||
repre_id = container_item.representation_id
|
||||
repre_info = repre_info_by_id.get(repre_id)
|
||||
repre_info = repre_info_by_project[project_name][repre_id]
|
||||
if not repre_info or not repre_info.is_valid:
|
||||
continue
|
||||
version_item = last_version_by_product_id.get(
|
||||
|
|
@ -397,14 +423,15 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
|
||||
menu.addAction(remove_action)
|
||||
|
||||
self._handle_sitesync(menu, valid_repre_ids)
|
||||
self._handle_sitesync(menu, repre_ids_by_project_name)
|
||||
|
||||
def _handle_sitesync(self, menu, repre_ids):
|
||||
def _handle_sitesync(self, menu, repre_ids_by_project_name):
|
||||
"""Adds actions for download/upload when SyncServer is enabled
|
||||
|
||||
Args:
|
||||
menu (OptionMenu)
|
||||
repre_ids (list) of object_ids
|
||||
repre_ids_by_project_name (Dict[str, Set[str]]): Representation
|
||||
ids by project name.
|
||||
|
||||
Returns:
|
||||
(OptionMenu)
|
||||
|
|
@ -413,7 +440,7 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
if not self._controller.is_sitesync_enabled():
|
||||
return
|
||||
|
||||
if not repre_ids:
|
||||
if not repre_ids_by_project_name:
|
||||
return
|
||||
|
||||
menu.addSeparator()
|
||||
|
|
@ -425,7 +452,10 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
menu
|
||||
)
|
||||
download_active_action.triggered.connect(
|
||||
lambda: self._add_sites(repre_ids, "active_site"))
|
||||
lambda: self._add_sites(
|
||||
repre_ids_by_project_name, "active_site"
|
||||
)
|
||||
)
|
||||
|
||||
upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR)
|
||||
upload_remote_action = QtWidgets.QAction(
|
||||
|
|
@ -434,23 +464,30 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
menu
|
||||
)
|
||||
upload_remote_action.triggered.connect(
|
||||
lambda: self._add_sites(repre_ids, "remote_site"))
|
||||
lambda: self._add_sites(
|
||||
repre_ids_by_project_name, "remote_site"
|
||||
)
|
||||
)
|
||||
|
||||
menu.addAction(download_active_action)
|
||||
menu.addAction(upload_remote_action)
|
||||
|
||||
def _add_sites(self, repre_ids, site_type):
|
||||
def _add_sites(self, repre_ids_by_project_name, site_type):
|
||||
"""(Re)sync all 'repre_ids' to specific site.
|
||||
|
||||
It checks if opposite site has fully available content to limit
|
||||
accidents. (ReSync active when no remote >> losing active content)
|
||||
|
||||
Args:
|
||||
repre_ids (list)
|
||||
repre_ids_by_project_name (Dict[str, Set[str]]): Representation
|
||||
ids by project name.
|
||||
site_type (Literal[active_site, remote_site]): Site type.
|
||||
"""
|
||||
|
||||
self._controller.resync_representations(repre_ids, site_type)
|
||||
"""
|
||||
for project_name, repre_ids in repre_ids_by_project_name.items():
|
||||
self._controller.resync_representations(
|
||||
project_name, repre_ids, site_type
|
||||
)
|
||||
|
||||
self.data_changed.emit()
|
||||
|
||||
|
|
@ -735,42 +772,68 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
container_items_by_id = self._controller.get_container_items_by_id(
|
||||
item_ids
|
||||
)
|
||||
repre_ids = {
|
||||
container_item.representation_id
|
||||
for container_item in container_items_by_id.values()
|
||||
}
|
||||
repre_info_by_id = self._controller.get_representation_info_items(
|
||||
repre_ids
|
||||
)
|
||||
project_names = set()
|
||||
repre_ids_by_project = collections.defaultdict(set)
|
||||
for container_item in container_items_by_id.values():
|
||||
repre_id = container_item.representation_id
|
||||
project_name = container_item.project_name
|
||||
project_names.add(project_name)
|
||||
repre_ids_by_project[project_name].add(repre_id)
|
||||
|
||||
# active_project_name = None
|
||||
active_repre_info = None
|
||||
repre_info_by_project = {}
|
||||
version_items_by_project = {}
|
||||
for project_name, repre_ids in repre_ids_by_project.items():
|
||||
repres_info = self._controller.get_representation_info_items(
|
||||
project_name, repre_ids
|
||||
)
|
||||
if active_repre_info is None:
|
||||
# active_project_name = project_name
|
||||
active_repre_info = repres_info.get(active_repre_id)
|
||||
|
||||
product_ids = {
|
||||
repre_info.product_id
|
||||
for repre_info in repres_info.values()
|
||||
if repre_info.is_valid
|
||||
}
|
||||
version_items_by_product_id = self._controller.get_version_items(
|
||||
project_name, product_ids
|
||||
)
|
||||
|
||||
repre_info_by_project[project_name] = repres_info
|
||||
version_items_by_project[project_name] = (
|
||||
version_items_by_product_id
|
||||
)
|
||||
|
||||
product_ids = {
|
||||
repre_info.product_id
|
||||
for repre_info in repre_info_by_id.values()
|
||||
}
|
||||
active_repre_info = repre_info_by_id[active_repre_id]
|
||||
active_version_id = active_repre_info.version_id
|
||||
active_product_id = active_repre_info.product_id
|
||||
version_items_by_product_id = self._controller.get_version_items(
|
||||
product_ids
|
||||
)
|
||||
version_items = list(
|
||||
version_items_by_product_id[active_product_id].values()
|
||||
)
|
||||
versions = {version_item.version for version_item in version_items}
|
||||
product_ids_by_version = collections.defaultdict(set)
|
||||
for version_items_by_id in version_items_by_product_id.values():
|
||||
for version_item in version_items_by_id.values():
|
||||
version = version_item.version
|
||||
_prod_version = version
|
||||
if _prod_version < 0:
|
||||
_prod_version = -1
|
||||
product_ids_by_version[_prod_version].add(
|
||||
version_item.product_id
|
||||
)
|
||||
if version in versions:
|
||||
continue
|
||||
versions.add(version)
|
||||
version_items.append(version_item)
|
||||
# active_product_id = active_repre_info.product_id
|
||||
|
||||
versions = set()
|
||||
product_ids = set()
|
||||
version_items = []
|
||||
product_ids_by_version_by_project = {}
|
||||
for project_name, version_items_by_product_id in (
|
||||
version_items_by_project.items()
|
||||
):
|
||||
product_ids_by_version = collections.defaultdict(set)
|
||||
product_ids_by_version_by_project[project_name] = (
|
||||
product_ids_by_version
|
||||
)
|
||||
for version_items_by_id in version_items_by_product_id.values():
|
||||
for version_item in version_items_by_id.values():
|
||||
version = version_item.version
|
||||
_prod_version = version
|
||||
if _prod_version < 0:
|
||||
_prod_version = -1
|
||||
product_ids_by_version[_prod_version].add(
|
||||
version_item.product_id
|
||||
)
|
||||
product_ids.add(version_item.product_id)
|
||||
if version in versions:
|
||||
continue
|
||||
versions.add(version)
|
||||
version_items.append(version_item)
|
||||
|
||||
def version_sorter(item):
|
||||
hero_value = 0
|
||||
|
|
@ -831,12 +894,15 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
product_version = -1
|
||||
version = HeroVersionType(version)
|
||||
|
||||
product_ids = product_ids_by_version[product_version]
|
||||
|
||||
filtered_item_ids = set()
|
||||
for container_item in container_items_by_id.values():
|
||||
project_name = container_item.project_name
|
||||
product_ids_by_version = (
|
||||
product_ids_by_version_by_project[project_name]
|
||||
)
|
||||
product_ids = product_ids_by_version[product_version]
|
||||
repre_id = container_item.representation_id
|
||||
repre_info = repre_info_by_id[repre_id]
|
||||
repre_info = repre_info_by_project[project_name][repre_id]
|
||||
if repre_info.product_id in product_ids:
|
||||
filtered_item_ids.add(container_item.item_id)
|
||||
|
||||
|
|
@ -846,14 +912,28 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
|
||||
def _show_switch_dialog(self, item_ids):
|
||||
"""Display Switch dialog"""
|
||||
containers_by_id = self._controller.get_containers_by_item_ids(
|
||||
container_items_by_id = self._controller.get_container_items_by_id(
|
||||
item_ids
|
||||
)
|
||||
dialog = SwitchAssetDialog(
|
||||
self._controller, self, list(containers_by_id.values())
|
||||
)
|
||||
dialog.switched.connect(self.data_changed.emit)
|
||||
dialog.show()
|
||||
container_ids_by_project_name = collections.defaultdict(set)
|
||||
for container_id, container_item in container_items_by_id.items():
|
||||
project_name = container_item.project_name
|
||||
container_ids_by_project_name[project_name].add(container_id)
|
||||
|
||||
for project_name, container_ids in (
|
||||
container_ids_by_project_name.items()
|
||||
):
|
||||
containers_by_id = self._controller.get_containers_by_item_ids(
|
||||
container_ids
|
||||
)
|
||||
dialog = SwitchAssetDialog(
|
||||
self._controller,
|
||||
project_name,
|
||||
list(containers_by_id.values()),
|
||||
self
|
||||
)
|
||||
dialog.switched.connect(self.data_changed.emit)
|
||||
dialog.show()
|
||||
|
||||
def _show_remove_warning_dialog(self, item_ids):
|
||||
"""Prompt a dialog to inform the user the action will remove items"""
|
||||
|
|
@ -927,38 +1007,58 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
self._update_containers_to_version(item_ids, version=-1)
|
||||
|
||||
def _on_switch_to_versioned(self, item_ids):
|
||||
# Get container items by ID
|
||||
containers_items_by_id = self._controller.get_container_items_by_id(
|
||||
item_ids
|
||||
)
|
||||
repre_ids = {
|
||||
container_item.representation_id
|
||||
for container_item in containers_items_by_id.values()
|
||||
}
|
||||
repre_info_by_id = self._controller.get_representation_info_items(
|
||||
repre_ids
|
||||
)
|
||||
product_ids = {
|
||||
repre_info.product_id
|
||||
for repre_info in repre_info_by_id.values()
|
||||
if repre_info.is_valid
|
||||
}
|
||||
version_items_by_product_id = self._controller.get_version_items(
|
||||
product_ids
|
||||
)
|
||||
item_ids)
|
||||
# Extract project names and their corresponding representation IDs
|
||||
repre_ids_by_project = collections.defaultdict(set)
|
||||
for container_item in containers_items_by_id.values():
|
||||
project_name = container_item.project_name
|
||||
repre_id = container_item.representation_id
|
||||
repre_ids_by_project[project_name].add(repre_id)
|
||||
|
||||
# Get representation info items by ID
|
||||
repres_info_by_project = {}
|
||||
version_items_by_project = {}
|
||||
for project_name, repre_ids in repre_ids_by_project.items():
|
||||
repre_info_by_id = self._controller.get_representation_info_items(
|
||||
project_name, repre_ids
|
||||
)
|
||||
repres_info_by_project[project_name] = repre_info_by_id
|
||||
|
||||
product_ids = {
|
||||
repre_info.product_id
|
||||
for repre_info in repre_info_by_id.values()
|
||||
if repre_info.is_valid
|
||||
}
|
||||
version_items_by_product_id = self._controller.get_version_items(
|
||||
project_name, product_ids
|
||||
)
|
||||
version_items_by_project[project_name] = (
|
||||
version_items_by_product_id
|
||||
)
|
||||
|
||||
update_containers = []
|
||||
update_versions = []
|
||||
for item_id, container_item in containers_items_by_id.items():
|
||||
for container_item in containers_items_by_id.values():
|
||||
project_name = container_item.project_name
|
||||
repre_id = container_item.representation_id
|
||||
|
||||
repre_info_by_id = repres_info_by_project[project_name]
|
||||
repre_info = repre_info_by_id[repre_id]
|
||||
|
||||
version_items_by_product_id = (
|
||||
version_items_by_project[project_name]
|
||||
)
|
||||
product_id = repre_info.product_id
|
||||
version_items_id = version_items_by_product_id[product_id]
|
||||
version_item = version_items_id.get(repre_info.version_id, {})
|
||||
version_items_by_id = version_items_by_product_id[product_id]
|
||||
version_item = version_items_by_id.get(repre_info.version_id, {})
|
||||
if not version_item or not version_item.is_hero:
|
||||
continue
|
||||
|
||||
version = abs(version_item.version)
|
||||
version_found = False
|
||||
for version_item in version_items_id.values():
|
||||
for version_item in version_items_by_id.values():
|
||||
if version_item.is_hero:
|
||||
continue
|
||||
if version_item.version == version:
|
||||
|
|
@ -971,8 +1071,8 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
update_containers.append(container_item.item_id)
|
||||
update_versions.append(version)
|
||||
|
||||
# Specify version per item to update to
|
||||
self._update_containers(update_containers, update_versions)
|
||||
# Specify version per item to update to
|
||||
self._update_containers(update_containers, update_versions)
|
||||
|
||||
def _update_containers(self, item_ids, versions):
|
||||
"""Helper to update items to given version (or version per item)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,9 @@ import sys
|
|||
import json
|
||||
import hashlib
|
||||
import platform
|
||||
import subprocess
|
||||
import csv
|
||||
import time
|
||||
import signal
|
||||
import locale
|
||||
from typing import Optional, Dict, Tuple, Any
|
||||
from typing import Optional, List, Dict, Tuple, Any
|
||||
|
||||
import requests
|
||||
from ayon_api.utils import get_default_settings_variant
|
||||
|
|
@ -53,15 +50,101 @@ def _get_server_and_variant(
|
|||
return server_url, variant
|
||||
|
||||
|
||||
def _windows_get_pid_args(pid: int) -> Optional[List[str]]:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
# Define constants
|
||||
PROCESS_COMMANDLINE_INFO = 60
|
||||
STATUS_NOT_FOUND = 0xC0000225
|
||||
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
||||
|
||||
# Define the UNICODE_STRING structure
|
||||
class UNICODE_STRING(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("Length", wintypes.USHORT),
|
||||
("MaximumLength", wintypes.USHORT),
|
||||
("Buffer", wintypes.LPWSTR)
|
||||
]
|
||||
|
||||
shell32 = ctypes.WinDLL("shell32", use_last_error=True)
|
||||
|
||||
CommandLineToArgvW = shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [
|
||||
wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)
|
||||
]
|
||||
CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR)
|
||||
|
||||
output = None
|
||||
# Open the process
|
||||
handle = ctypes.windll.kernel32.OpenProcess(
|
||||
PROCESS_QUERY_LIMITED_INFORMATION, False, pid
|
||||
)
|
||||
if not handle:
|
||||
return output
|
||||
|
||||
try:
|
||||
buffer_len = wintypes.ULONG()
|
||||
# Get the right buffer size first
|
||||
status = ctypes.windll.ntdll.NtQueryInformationProcess(
|
||||
handle,
|
||||
PROCESS_COMMANDLINE_INFO,
|
||||
ctypes.c_void_p(None),
|
||||
0,
|
||||
ctypes.byref(buffer_len)
|
||||
)
|
||||
|
||||
if status == STATUS_NOT_FOUND:
|
||||
return output
|
||||
|
||||
# Create buffer with collected size
|
||||
buffer = ctypes.create_string_buffer(buffer_len.value)
|
||||
|
||||
# Get the command line
|
||||
status = ctypes.windll.ntdll.NtQueryInformationProcess(
|
||||
handle,
|
||||
PROCESS_COMMANDLINE_INFO,
|
||||
buffer,
|
||||
buffer_len,
|
||||
ctypes.byref(buffer_len)
|
||||
)
|
||||
if status:
|
||||
return output
|
||||
# Build the string
|
||||
tmp = ctypes.cast(buffer, ctypes.POINTER(UNICODE_STRING)).contents
|
||||
size = tmp.Length // 2 + 1
|
||||
cmdline_buffer = ctypes.create_unicode_buffer(size)
|
||||
ctypes.cdll.msvcrt.wcscpy(cmdline_buffer, tmp.Buffer)
|
||||
|
||||
args_len = ctypes.c_int()
|
||||
args = CommandLineToArgvW(
|
||||
cmdline_buffer, ctypes.byref(args_len)
|
||||
)
|
||||
output = [args[idx] for idx in range(args_len.value)]
|
||||
ctypes.windll.kernel32.LocalFree(args)
|
||||
|
||||
finally:
|
||||
ctypes.windll.kernel32.CloseHandle(handle)
|
||||
return output
|
||||
|
||||
|
||||
def _windows_pid_is_running(pid: int) -> bool:
|
||||
args = ["tasklist.exe", "/fo", "csv", "/fi", f"PID eq {pid}"]
|
||||
output = subprocess.check_output(args)
|
||||
encoding = locale.getpreferredencoding()
|
||||
csv_content = csv.DictReader(output.decode(encoding).splitlines())
|
||||
# if "PID" not in csv_content.fieldnames:
|
||||
# return False
|
||||
for _ in csv_content:
|
||||
args = _windows_get_pid_args(pid)
|
||||
if not args:
|
||||
return False
|
||||
executable_path = args[0]
|
||||
|
||||
filename = os.path.basename(executable_path).lower()
|
||||
if "ayon" in filename:
|
||||
return True
|
||||
|
||||
# Try to handle tray running from code
|
||||
# - this might be potential danger that kills other python process running
|
||||
# 'start.py' script (low chance, but still)
|
||||
if "python" in filename and len(args) > 1:
|
||||
script_filename = os.path.basename(args[1].lower())
|
||||
if script_filename == "start.py":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@ from ayon_core.lib import (
|
|||
)
|
||||
from ayon_core.settings import get_studio_settings
|
||||
from ayon_core.addon import (
|
||||
ITrayAction,
|
||||
ITrayAddon,
|
||||
ITrayService,
|
||||
)
|
||||
from ayon_core.pipeline import install_ayon_plugins
|
||||
from ayon_core.tools.utils import (
|
||||
WrappedCallbackItem,
|
||||
get_ayon_qt_app,
|
||||
|
|
@ -32,6 +33,12 @@ from ayon_core.tools.tray.lib import (
|
|||
remove_tray_server_url,
|
||||
TrayIsRunningError,
|
||||
)
|
||||
from ayon_core.tools.launcher.ui import LauncherWindow
|
||||
from ayon_core.tools.loader.ui import LoaderWindow
|
||||
from ayon_core.tools.console_interpreter.ui import ConsoleInterpreterWindow
|
||||
from ayon_core.tools.publisher.publish_report_viewer import (
|
||||
PublishReportViewerWindow,
|
||||
)
|
||||
|
||||
from .addons_manager import TrayAddonsManager
|
||||
from .host_console_listener import HostListener
|
||||
|
|
@ -82,6 +89,11 @@ class TrayManager:
|
|||
|
||||
self._outdated_dialog = None
|
||||
|
||||
self._launcher_window = None
|
||||
self._browser_window = None
|
||||
self._console_window = ConsoleInterpreterWindow()
|
||||
self._publish_report_viewer_window = PublishReportViewerWindow()
|
||||
|
||||
self._update_check_timer = update_check_timer
|
||||
self._update_check_interval = update_check_interval
|
||||
self._main_thread_timer = main_thread_timer
|
||||
|
|
@ -109,12 +121,15 @@ class TrayManager:
|
|||
@property
|
||||
def doubleclick_callback(self):
|
||||
"""Double-click callback for Tray icon."""
|
||||
return self._addons_manager.get_doubleclick_callback()
|
||||
callback = self._addons_manager.get_doubleclick_callback()
|
||||
if callback is None:
|
||||
callback = self._show_launcher_window
|
||||
return callback
|
||||
|
||||
def execute_doubleclick(self):
|
||||
"""Execute double click callback in main thread."""
|
||||
callback = self.doubleclick_callback
|
||||
if callback:
|
||||
if callback is not None:
|
||||
self.execute_in_main_thread(callback)
|
||||
|
||||
def show_tray_message(self, title, message, icon=None, msecs=None):
|
||||
|
|
@ -144,8 +159,34 @@ class TrayManager:
|
|||
return
|
||||
|
||||
tray_menu = self.tray_widget.menu
|
||||
# Add launcher at first place
|
||||
launcher_action = QtWidgets.QAction(
|
||||
"Launcher", tray_menu
|
||||
)
|
||||
launcher_action.triggered.connect(self._show_launcher_window)
|
||||
tray_menu.addAction(launcher_action)
|
||||
|
||||
console_action = ITrayAddon.add_action_to_admin_submenu(
|
||||
"Console", tray_menu
|
||||
)
|
||||
console_action.triggered.connect(self._show_console_window)
|
||||
|
||||
publish_report_viewer_action = ITrayAddon.add_action_to_admin_submenu(
|
||||
"Publish report viewer", tray_menu
|
||||
)
|
||||
publish_report_viewer_action.triggered.connect(
|
||||
self._show_publish_report_viewer
|
||||
)
|
||||
|
||||
self._addons_manager.initialize(tray_menu)
|
||||
|
||||
# Add browser action after addon actions
|
||||
browser_action = QtWidgets.QAction(
|
||||
"Browser", tray_menu
|
||||
)
|
||||
browser_action.triggered.connect(self._show_browser_window)
|
||||
tray_menu.addAction(browser_action)
|
||||
|
||||
self._addons_manager.add_route(
|
||||
"GET", "/tray", self._web_get_tray_info
|
||||
)
|
||||
|
|
@ -153,7 +194,7 @@ class TrayManager:
|
|||
"POST", "/tray/message", self._web_show_tray_message
|
||||
)
|
||||
|
||||
admin_submenu = ITrayAction.admin_submenu(tray_menu)
|
||||
admin_submenu = ITrayAddon.admin_submenu(tray_menu)
|
||||
tray_menu.addMenu(admin_submenu)
|
||||
|
||||
# Add services if they are
|
||||
|
|
@ -522,6 +563,35 @@ class TrayManager:
|
|||
self._info_widget.raise_()
|
||||
self._info_widget.activateWindow()
|
||||
|
||||
def _show_launcher_window(self):
|
||||
if self._launcher_window is None:
|
||||
self._launcher_window = LauncherWindow()
|
||||
|
||||
self._launcher_window.show()
|
||||
self._launcher_window.raise_()
|
||||
self._launcher_window.activateWindow()
|
||||
|
||||
def _show_browser_window(self):
|
||||
if self._browser_window is None:
|
||||
self._browser_window = LoaderWindow()
|
||||
self._browser_window.setWindowTitle("AYON Browser")
|
||||
install_ayon_plugins()
|
||||
|
||||
self._browser_window.show()
|
||||
self._browser_window.raise_()
|
||||
self._browser_window.activateWindow()
|
||||
|
||||
def _show_console_window(self):
|
||||
self._console_window.show()
|
||||
self._console_window.raise_()
|
||||
self._console_window.activateWindow()
|
||||
|
||||
def _show_publish_report_viewer(self):
|
||||
self._publish_report_viewer_window.refresh()
|
||||
self._publish_report_viewer_window.show()
|
||||
self._publish_report_viewer_window.raise_()
|
||||
self._publish_report_viewer_window.activateWindow()
|
||||
|
||||
|
||||
class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
|
||||
"""Tray widget.
|
||||
|
|
|
|||
|
|
@ -556,9 +556,10 @@ class _IconsCache:
|
|||
log.info("Didn't find icon \"{}\"".format(icon_name))
|
||||
|
||||
elif used_variant != icon_name:
|
||||
log.debug("Icon \"{}\" was not found \"{}\" is used instead".format(
|
||||
icon_name, used_variant
|
||||
))
|
||||
log.debug(
|
||||
f"Icon \"{icon_name}\" was not found"
|
||||
f" \"{used_variant}\" is used instead"
|
||||
)
|
||||
|
||||
cls._qtawesome_cache[full_icon_name] = icon
|
||||
return icon
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
|
||||
from qtpy import QtWidgets
|
||||
from qtpy import QtWidgets, QT6
|
||||
|
||||
|
||||
class Action(QtWidgets.QAction):
|
||||
|
|
@ -112,20 +112,21 @@ module.{module_name}()"""
|
|||
Run the command of the instance or copy the command to the active shelf
|
||||
based on the current modifiers.
|
||||
|
||||
If callbacks have been registered with fouind modifier integer the
|
||||
If callbacks have been registered with found modifier integer the
|
||||
function will trigger all callbacks. When a callback function returns a
|
||||
non zero integer it will not execute the action's command
|
||||
|
||||
"""
|
||||
|
||||
# get the current application and its linked keyboard modifiers
|
||||
app = QtWidgets.QApplication.instance()
|
||||
modifiers = app.keyboardModifiers()
|
||||
if not QT6:
|
||||
modifiers = int(modifiers)
|
||||
|
||||
# If the menu has a callback registered for the current modifier
|
||||
# we run the callback instead of the action itself.
|
||||
registered = self._root.registered_callbacks
|
||||
callbacks = registered.get(int(modifiers), [])
|
||||
callbacks = registered.get(modifiers, [])
|
||||
for callback in callbacks:
|
||||
signal = callback(self)
|
||||
if signal != 0:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import maya.cmds as cmds
|
|||
import maya.mel as mel
|
||||
|
||||
import scriptsmenu
|
||||
from qtpy import QtCore, QtWidgets
|
||||
from qtpy import QtCore, QtWidgets, QT6
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ def main(title="Scripts", parent=None, objectName=None):
|
|||
|
||||
# Register control + shift callback to add to shelf (maya behavior)
|
||||
modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
|
||||
if int(cmds.about(version=True)) < 2025:
|
||||
if not QT6:
|
||||
modifiers = int(modifiers)
|
||||
|
||||
menu.register_callback(modifiers, to_shelf)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.0.6+dev"
|
||||
__version__ = "1.0.12+dev"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ pydantic = "^2.9.2"
|
|||
aiohttp-middlewares = "^2.0.0"
|
||||
Click = "^8"
|
||||
OpenTimelineIO = "0.16.0"
|
||||
opencolorio = "^2.3.2"
|
||||
opencolorio = "^2.3.2,<2.4.0"
|
||||
Pillow = "9.5.0"
|
||||
websocket-client = ">=0.40.0,<2"
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue