Merge branch 'develop' into bugfix/AY-7105_colorspacedata-not-correctly-distributed-from-clips

This commit is contained in:
Jakub Ježek 2024-11-12 15:58:19 +01:00 committed by GitHub
commit a42fff3c4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 375 additions and 174 deletions

View file

@ -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,7 @@ 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 +632,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 +688,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 +700,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 +708,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 +741,7 @@ class FileDefItem:
)
@property
def label(self):
def label(self) -> Optional[str]:
if self.is_empty:
return None
@ -774,7 +784,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 +795,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 +804,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 +820,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 +845,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 +891,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 +900,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 +933,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 +971,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 +996,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 +1017,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 +1058,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 +1078,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 +1098,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

View file

@ -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}
)

View file

@ -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:

View file

@ -202,7 +202,6 @@ 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:

View file

@ -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)

View file

@ -296,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,

View file

@ -194,14 +194,14 @@ class InventoryModel(QtGui.QStandardItemModel):
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:
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(
@ -217,6 +217,7 @@ class InventoryModel(QtGui.QStandardItemModel):
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
@ -425,7 +426,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):

View file

@ -383,7 +383,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

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'core' version."""
__version__ = "1.0.6+dev"
__version__ = "1.0.7+dev"

View file

@ -1,6 +1,6 @@
name = "core"
title = "Core"
version = "1.0.6+dev"
version = "1.0.7+dev"
client_dir = "ayon_core"

View file

@ -5,7 +5,7 @@
[tool.poetry]
name = "ayon-core"
version = "1.0.6+dev"
version = "1.0.7+dev"
description = ""
authors = ["Ynput Team <team@ynput.io>"]
readme = "README.md"