Merge branch 'develop' of https://github.com/ynput/ayon-core into feature/AY-6789_Render-instance-support-of-Frame-List

This commit is contained in:
Petr Kalis 2024-11-06 17:40:02 +01:00
commit 4a5583cff0
17 changed files with 609 additions and 249 deletions

View file

@ -0,0 +1,16 @@
name: 📤 Upload to Ynput Cloud
on:
workflow_dispatch:
release:
types: [published]
jobs:
call-upload-to-ynput-cloud:
uses: ynput/ops-repo-automation/.github/workflows/upload_to_ynput_cloud.yml@main
secrets:
CI_EMAIL: ${{ secrets.CI_EMAIL }}
CI_USER: ${{ secrets.CI_USER }}
YNPUT_BOT_TOKEN: ${{ secrets.YNPUT_BOT_TOKEN }}
YNPUT_CLOUD_URL: ${{ secrets.YNPUT_CLOUD_URL }}
YNPUT_CLOUD_TOKEN: ${{ secrets.YNPUT_CLOUD_TOKEN }}

View file

@ -1,6 +1,5 @@
import os
import sys
import uuid
import getpass
import logging
import platform
@ -11,12 +10,12 @@ import copy
from . import Terminal
# Check for `unicode` in builtins
USE_UNICODE = hasattr(__builtins__, "unicode")
class LogStreamHandler(logging.StreamHandler):
""" StreamHandler class designed to handle utf errors in python 2.x hosts.
"""StreamHandler class.
This was originally designed to handle UTF errors in python 2.x hosts,
however currently solely remains for backwards compatibility.
"""
@ -25,49 +24,27 @@ class LogStreamHandler(logging.StreamHandler):
self.enabled = True
def enable(self):
""" Enable StreamHandler
"""Enable StreamHandler
Used to silence output
Make StreamHandler output again
"""
self.enabled = True
def disable(self):
""" Disable StreamHandler
"""Disable StreamHandler
Make StreamHandler output again
Used to silence output
"""
self.enabled = False
def emit(self, record):
if not self.enable:
if not self.enabled or self.stream is None:
return
try:
msg = self.format(record)
msg = Terminal.log(msg)
stream = self.stream
if stream is None:
return
fs = "%s\n"
# if no unicode support...
if not USE_UNICODE:
stream.write(fs % msg)
else:
try:
if (isinstance(msg, unicode) and # noqa: F821
getattr(stream, 'encoding', None)):
ufs = u'%s\n'
try:
stream.write(ufs % msg)
except UnicodeEncodeError:
stream.write((ufs % msg).encode(stream.encoding))
else:
if (getattr(stream, 'encoding', 'utf-8')):
ufs = u'%s\n'
stream.write(ufs % unicode(msg)) # noqa: F821
else:
stream.write(fs % msg)
except UnicodeError:
stream.write(fs % msg.encode("UTF-8"))
stream.write(f"{msg}\n")
self.flush()
except (KeyboardInterrupt, SystemExit):
raise
@ -141,8 +118,6 @@ class Logger:
process_data = None
# Cached process name or ability to set different process name
_process_name = None
# TODO Remove 'mongo_process_id' in 1.x.x
mongo_process_id = uuid.uuid4().hex
@classmethod
def get_logger(cls, name=None):

View file

@ -1,7 +1,6 @@
import os
import re
import logging
import platform
import clique
@ -38,31 +37,7 @@ def create_hard_link(src_path, dst_path):
dst_path(str): Full path to a file where a link of source will be
added.
"""
# Use `os.link` if is available
# - should be for all platforms with newer python versions
if hasattr(os, "link"):
os.link(src_path, dst_path)
return
# Windows implementation of hardlinks
# - used in Python 2
if platform.system().lower() == "windows":
import ctypes
from ctypes.wintypes import BOOL
CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW
CreateHardLink.argtypes = [
ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_void_p
]
CreateHardLink.restype = BOOL
res = CreateHardLink(dst_path, src_path, None)
if res == 0:
raise ctypes.WinError()
return
# Raises not implemented error if gets here
raise NotImplementedError(
"Implementation of hardlink for current environment is missing."
)
os.link(src_path, dst_path)
def collect_frames(files):
@ -210,7 +185,7 @@ def get_last_version_from_path(path_dir, filter):
assert isinstance(filter, list) and (
len(filter) != 0), "`filter` argument needs to be list and not empty"
filtred_files = list()
filtered_files = list()
# form regex for filtering
pattern = r".*".join(filter)
@ -218,10 +193,10 @@ def get_last_version_from_path(path_dir, filter):
for file in os.listdir(path_dir):
if not re.findall(pattern, file):
continue
filtred_files.append(file)
filtered_files.append(file)
if filtred_files:
sorted(filtred_files)
return filtred_files[-1]
if filtered_files:
filtered_files.sort()
return filtered_files[-1]
return None

View file

@ -254,9 +254,8 @@ class CreateContext:
self._collection_shared_data = None
# Entities cache
self._folder_entities_by_id = {}
self._folder_entities_by_path = {}
self._task_entities_by_id = {}
self._folder_id_by_folder_path = {}
self._task_ids_by_folder_path = {}
self._task_names_by_folder_path = {}
@ -560,10 +559,9 @@ class CreateContext:
# Give ability to store shared data for collection phase
self._collection_shared_data = {}
self._folder_entities_by_id = {}
self._folder_entities_by_path = {}
self._task_entities_by_id = {}
self._folder_id_by_folder_path = {}
self._task_ids_by_folder_path = {}
self._task_names_by_folder_path = {}
@ -1480,43 +1478,35 @@ class CreateContext:
output = {
folder_path: None
for folder_path in folder_paths
if folder_path is not None
}
remainder_paths = set()
for folder_path in output:
# Skip empty/invalid folder paths
if folder_path is None or "/" not in folder_path:
# Skip invalid folder paths (folder name or empty path)
if not folder_path or "/" not in folder_path:
continue
if folder_path not in self._folder_id_by_folder_path:
if folder_path not in self._folder_entities_by_path:
remainder_paths.add(folder_path)
continue
folder_id = self._folder_id_by_folder_path.get(folder_path)
if not folder_id:
output[folder_path] = None
continue
folder_entity = self._folder_entities_by_id.get(folder_id)
if folder_entity:
output[folder_path] = folder_entity
else:
remainder_paths.add(folder_path)
output[folder_path] = self._folder_entities_by_path[folder_path]
if not remainder_paths:
return output
folder_paths_by_id = {}
found_paths = set()
for folder_entity in ayon_api.get_folders(
self.project_name,
folder_paths=remainder_paths,
):
folder_id = folder_entity["id"]
folder_path = folder_entity["path"]
folder_paths_by_id[folder_id] = folder_path
found_paths.add(folder_path)
output[folder_path] = folder_entity
self._folder_entities_by_id[folder_id] = folder_entity
self._folder_id_by_folder_path[folder_path] = folder_id
self._folder_entities_by_path[folder_path] = folder_entity
# Cache empty folder entities
for path in remainder_paths - found_paths:
self._folder_entities_by_path[path] = None
return output
@ -1676,6 +1666,7 @@ class CreateContext:
instance.get("folderPath")
for instance in instances
}
folder_paths.discard(None)
folder_entities_by_path = self.get_folder_entities(folder_paths)
for instance in instances:
folder_path = instance.get("folderPath")
@ -1775,9 +1766,9 @@ class CreateContext:
if not folder_path:
continue
if folder_path in self._folder_id_by_folder_path:
folder_id = self._folder_id_by_folder_path[folder_path]
if folder_id is None:
if folder_path in self._folder_entities_by_path:
folder_entity = self._folder_entities_by_path[folder_path]
if folder_entity is None:
continue
context_info.folder_is_valid = True

View file

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

View file

@ -1585,6 +1585,10 @@ CreateNextPageOverlay {
}
/* Attribute Definition widgets */
AttributeDefinitionsLabel[overridden="1"] {
color: {color:font-overridden};
}
AttributeDefinitionsWidget QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit {
padding: 1px;
}

View file

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

View file

@ -0,0 +1 @@
REVERT_TO_DEFAULT_LABEL = "Revert to default"

View file

@ -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
@ -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
@ -888,22 +900,28 @@ 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 dragEnterEvent(self, event):
if self._multivalue:
@ -1011,5 +1029,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()

View file

@ -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()
@ -134,6 +203,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 +215,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 +236,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 +247,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 +260,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 +304,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 +363,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 +372,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 +380,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 +392,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 +432,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 +507,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 +531,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 +592,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 +634,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 +669,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 +775,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 +811,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()

View file

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

View file

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

View file

@ -40,6 +40,7 @@ from ayon_core.tools.publisher.abstract import (
)
CREATE_EVENT_SOURCE = "publisher.create.model"
_DEFAULT_VALUE = object()
class CreatorType:
@ -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

View file

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

View file

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

View file

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

View file

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