From 3782105fc0c7b45edc11b32f3a09f26c2d542347 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:36:17 +0100 Subject: [PATCH 01/26] EnumDef allows placeholder to be set --- client/ayon_core/lib/attribute_definitions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e8327a45b6..e750c00a27 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -554,12 +554,18 @@ class EnumDef(AbstractAttrDef): """ type = "enum" + type_attributes = [ + "multiselection", + "placeholder", + ] + def __init__( self, key: str, items: "EnumItemsInputType", default: "Union[str, List[Any]]" = None, multiselection: Optional[bool] = False, + placeholder: Optional[str] = None, **kwargs ): if not items: @@ -587,6 +593,7 @@ class EnumDef(AbstractAttrDef): self.items: List["EnumItemDict"] = items self._item_values: Set[Any] = item_values_set self.multiselection: bool = multiselection + self.placeholder: Optional[str] = placeholder def convert_value(self, value): if not self.multiselection: @@ -612,7 +619,6 @@ class EnumDef(AbstractAttrDef): def serialize(self): data = super().serialize() data["items"] = copy.deepcopy(self.items) - data["multiselection"] = self.multiselection return data @staticmethod From 29dbc8b8cf50cd72825d61964d17e601733c47d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:37:14 +0100 Subject: [PATCH 02/26] set the placeholder on widgets --- client/ayon_core/tools/attribute_defs/widgets.py | 6 +++++- .../tools/utils/multiselection_combobox.py | 4 ++-- client/ayon_core/tools/utils/widgets.py | 13 ++++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 93f63730f5..635855863c 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -640,11 +640,15 @@ class EnumAttrWidget(_BaseAttrDefWidget): return self.attr_def.multiselection def _ui_init(self): + placeholder = self.attr_def.placeholder if self.multiselection: - input_widget = MultiSelectionComboBox(self) + input_widget = MultiSelectionComboBox( + self, placeholder=placeholder + ) else: input_widget = CustomTextComboBox(self) + input_widget.set_placeholder(placeholder) combo_delegate = QtWidgets.QStyledItemDelegate(input_widget) input_widget.setItemDelegate(combo_delegate) self._combo_delegate = combo_delegate diff --git a/client/ayon_core/tools/utils/multiselection_combobox.py b/client/ayon_core/tools/utils/multiselection_combobox.py index 34361fca17..b90838267b 100644 --- a/client/ayon_core/tools/utils/multiselection_combobox.py +++ b/client/ayon_core/tools/utils/multiselection_combobox.py @@ -61,7 +61,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True) self._initial_mouse_pos = None self._separator = separator - self._placeholder_text = placeholder + self._placeholder_text = placeholder or "" delegate = ComboItemDelegate(self) self.setItemDelegate(delegate) @@ -74,7 +74,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): return self._placeholder_text def set_placeholder_text(self, text): - self._placeholder_text = text + self._placeholder_text = text or "" self._update_size_hint() def set_custom_text(self, text): diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index 4c2b418c41..8d5a11b811 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -54,7 +54,7 @@ class ComboBox(QtWidgets.QComboBox): """ def __init__(self, *args, **kwargs): - super(ComboBox, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(delegate) self.setFocusPolicy(QtCore.Qt.StrongFocus) @@ -63,7 +63,7 @@ class ComboBox(QtWidgets.QComboBox): def wheelEvent(self, event): if self.hasFocus(): - return super(ComboBox, self).wheelEvent(event) + return super().wheelEvent(event) class CustomTextComboBox(ComboBox): @@ -71,7 +71,14 @@ class CustomTextComboBox(ComboBox): def __init__(self, *args, **kwargs): self._custom_text = None - super(CustomTextComboBox, self).__init__(*args, **kwargs) + self._placeholder = placeholder + super().__init__(*args, **kwargs) + + def set_placeholder(self, placeholder: Optional[str]): + if placeholder == self._placeholder: + return + self.lineEdit().setPlaceholderText(placeholder) + self.repaint() def set_custom_text(self, text=None): if self._custom_text != text: From 4c7176c7521827c4f48f2694ba3337d2fe0bbcf7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:40:03 +0100 Subject: [PATCH 03/26] remove placeholder from single selection combobox --- client/ayon_core/tools/attribute_defs/widgets.py | 4 +--- client/ayon_core/tools/utils/widgets.py | 7 ------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 635855863c..201fd5be48 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -640,15 +640,13 @@ class EnumAttrWidget(_BaseAttrDefWidget): return self.attr_def.multiselection def _ui_init(self): - placeholder = self.attr_def.placeholder if self.multiselection: input_widget = MultiSelectionComboBox( - self, placeholder=placeholder + self, placeholder=self.attr_def.placeholder ) else: input_widget = CustomTextComboBox(self) - input_widget.set_placeholder(placeholder) combo_delegate = QtWidgets.QStyledItemDelegate(input_widget) input_widget.setItemDelegate(combo_delegate) self._combo_delegate = combo_delegate diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index 8d5a11b811..059a06648b 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -71,15 +71,8 @@ class CustomTextComboBox(ComboBox): def __init__(self, *args, **kwargs): self._custom_text = None - self._placeholder = placeholder super().__init__(*args, **kwargs) - def set_placeholder(self, placeholder: Optional[str]): - if placeholder == self._placeholder: - return - self.lineEdit().setPlaceholderText(placeholder) - self.repaint() - def set_custom_text(self, text=None): if self._custom_text != text: self._custom_text = text From a312c463842bbeae1053054503f80168faa7abd3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:40:14 +0100 Subject: [PATCH 04/26] added placeholder to docstring --- client/ayon_core/lib/attribute_definitions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e750c00a27..c901e0f03e 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -550,6 +550,8 @@ class EnumDef(AbstractAttrDef): passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. Output is list of selected items. + placeholder (Optional[str]): Placeholder for UI purposes, only for + multiselection enumeration. """ type = "enum" From e13de28a24c2a223fe4903421f802a4ba7fcef08 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Feb 2025 11:24:07 +0100 Subject: [PATCH 05/26] draw placeholder with disabled color --- client/ayon_core/tools/utils/multiselection_combobox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/utils/multiselection_combobox.py b/client/ayon_core/tools/utils/multiselection_combobox.py index b90838267b..e8bc688234 100644 --- a/client/ayon_core/tools/utils/multiselection_combobox.py +++ b/client/ayon_core/tools/utils/multiselection_combobox.py @@ -208,7 +208,8 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): draw_text = False if draw_text: option.currentText = combotext - option.palette.setCurrentColorGroup(QtGui.QPalette.Disabled) + # Draw text as disabled -> to mimic placeholder color + option.state &= ~QtWidgets.QStyle.State_Enabled painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option) return From 71454b6fa4e31ce08028b08b813974c942f0ece6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Feb 2025 11:54:56 +0100 Subject: [PATCH 06/26] usa same placeholder color as elsewhere --- .../tools/utils/multiselection_combobox.py | 65 ++++++++++++------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/tools/utils/multiselection_combobox.py b/client/ayon_core/tools/utils/multiselection_combobox.py index e8bc688234..7a8c4c2fcc 100644 --- a/client/ayon_core/tools/utils/multiselection_combobox.py +++ b/client/ayon_core/tools/utils/multiselection_combobox.py @@ -1,5 +1,7 @@ from qtpy import QtCore, QtGui, QtWidgets +from ayon_core.style import get_objected_colors + from .lib import ( checkstate_int_to_enum, checkstate_enum_to_int, @@ -49,11 +51,12 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): item_spacing = 5 item_bg_color = QtGui.QColor("#31424e") + _placeholder_color = None def __init__( self, parent=None, placeholder="", separator=", ", **kwargs ): - super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs) + super().__init__(parent=parent, **kwargs) self.setObjectName("MultiSelectionComboBox") self.setFocusPolicy(QtCore.Qt.StrongFocus) @@ -206,21 +209,23 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): combotext = self._placeholder_text else: draw_text = False - if draw_text: - option.currentText = combotext - # Draw text as disabled -> to mimic placeholder color - option.state &= ~QtWidgets.QStyle.State_Enabled - painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option) - return - font_metricts = self.fontMetrics() + lines = self._lines + if draw_text: + color = self._get_placeholder_color() + pen = painter.pen() + pen.setColor(color) + painter.setPen(pen) + lines = {0: [combotext]} + + font_metrics = self.fontMetrics() if self._item_height is None: self.updateGeometry() self.update() return - for line, items in self._lines.items(): + for line, items in lines.items(): top_y = ( option.rect.top() + (line * self._item_height) @@ -228,7 +233,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): ) left_x = option.rect.left() + self.left_offset for item in items: - label_rect = font_metricts.boundingRect(item) + label_rect = font_metrics.boundingRect(item) label_height = label_rect.height() label_rect.moveTop(top_y) @@ -238,22 +243,23 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): label_rect.width() + self.left_right_padding ) - bg_rect = QtCore.QRectF(label_rect) - bg_rect.setWidth( - label_rect.width() + self.left_right_padding - ) - left_x = bg_rect.right() + self.item_spacing + if not draw_text: + bg_rect = QtCore.QRectF(label_rect) + bg_rect.setWidth( + label_rect.width() + self.left_right_padding + ) + left_x = bg_rect.right() + self.item_spacing + + bg_rect.setHeight(label_height + (2 * self.top_bottom_padding)) + bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins) + + path = QtGui.QPainterPath() + path.addRoundedRect(bg_rect, 5, 5) + + painter.fillPath(path, self.item_bg_color) label_rect.moveLeft(label_rect.x() + self.left_right_padding) - bg_rect.setHeight(label_height + (2 * self.top_bottom_padding)) - bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins) - - path = QtGui.QPainterPath() - path.addRoundedRect(bg_rect, 5, 5) - - painter.fillPath(path, self.item_bg_color) - painter.drawText( label_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, @@ -288,11 +294,11 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): line = 0 self._lines = {line: []} - font_metricts = self.fontMetrics() + font_metrics = self.fontMetrics() default_left_x = 0 + self.left_offset left_x = int(default_left_x) for item in items: - rect = font_metricts.boundingRect(item) + rect = font_metrics.boundingRect(item) width = rect.width() + (2 * self.left_right_padding) right_x = left_x + width if right_x > total_width: @@ -383,3 +389,12 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): return event.ignore() return super(MultiSelectionComboBox, self).keyPressEvent(event) + + @classmethod + def _get_placeholder_color(cls): + if cls._placeholder_color is None: + color_obj = get_objected_colors("font") + color = color_obj.get_qcolor() + color.setAlpha(67) + cls._placeholder_color = color + return cls._placeholder_color From e30b89e36d23a97a01f7fe22086c5b4f8d1291d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:09:12 +0100 Subject: [PATCH 07/26] fix formatting --- client/ayon_core/tools/utils/multiselection_combobox.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/utils/multiselection_combobox.py b/client/ayon_core/tools/utils/multiselection_combobox.py index 7a8c4c2fcc..7bd7a76abc 100644 --- a/client/ayon_core/tools/utils/multiselection_combobox.py +++ b/client/ayon_core/tools/utils/multiselection_combobox.py @@ -47,7 +47,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): top_bottom_padding = 2 left_right_padding = 3 left_offset = 4 - top_bottom_margins = 2 + top_bottom_margins = 1 item_spacing = 5 item_bg_color = QtGui.QColor("#31424e") @@ -250,7 +250,9 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): ) left_x = bg_rect.right() + self.item_spacing - bg_rect.setHeight(label_height + (2 * self.top_bottom_padding)) + bg_rect.setHeight( + label_height + (2 * self.top_bottom_padding) + ) bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins) path = QtGui.QPainterPath() From e0507d99d2829fd6296878899468146f92b5de0d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Feb 2025 22:20:27 +0800 Subject: [PATCH 08/26] add substance designer into the template --- server/settings/tools.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/server/settings/tools.py b/server/settings/tools.py index 96851be1da..0aaa88a8b0 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -484,6 +484,17 @@ DEFAULT_TOOLS_VALUES = { "task_types": [], "tasks": [], "template": "{folder[name]}_{variant}" + }, + { + "product_types": [ + "textureSet" + ], + "hosts": [ + "substancedesigner" + ], + "task_types": [], + "tasks": [], + "template": "T_{folder[name]}{variant}" } ], "filter_creator_profiles": [] @@ -548,10 +559,13 @@ DEFAULT_TOOLS_VALUES = { }, { "product_types": [ - "simpleUnrealTexture" + "simpleUnrealTexture", + "image", + "textures" ], "hosts": [ - "standalonepublisher" + "standalonepublisher", + "substancedesigner" ], "task_types": [], "task_names": [], @@ -595,10 +609,13 @@ DEFAULT_TOOLS_VALUES = { "hero_template_name_profiles": [ { "product_types": [ - "simpleUnrealTexture" + "simpleUnrealTexture", + "image", + "textures" ], "hosts": [ - "standalonepublisher" + "standalonepublisher", + "substancedesigner" ], "task_types": [], "task_names": [], From c6f5988aae75a1fb514f20ec56bb50985b145bea Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:13:29 +0100 Subject: [PATCH 09/26] use same font size for placeholder as elsewhere --- .../tools/utils/multiselection_combobox.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/utils/multiselection_combobox.py b/client/ayon_core/tools/utils/multiselection_combobox.py index 7bd7a76abc..a6198abb51 100644 --- a/client/ayon_core/tools/utils/multiselection_combobox.py +++ b/client/ayon_core/tools/utils/multiselection_combobox.py @@ -210,22 +210,36 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): else: draw_text = False - lines = self._lines if draw_text: color = self._get_placeholder_color() pen = painter.pen() pen.setColor(color) painter.setPen(pen) - lines = {0: [combotext]} - font_metrics = self.fontMetrics() + left_x = option.rect.left() + self.left_offset + + font = self.font() + # This is hardcoded point size from styles + font.setPointSize(10) + painter.setFont(font) + + label_rect = QtCore.QRect(option.rect) + label_rect.moveLeft(left_x) + + painter.drawText( + label_rect, + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, + combotext + ) + return if self._item_height is None: self.updateGeometry() self.update() return - for line, items in lines.items(): + font_metrics = self.fontMetrics() + for line, items in self._lines.items(): top_y = ( option.rect.top() + (line * self._item_height) From a409d55b5904525797f0520c4a872a7776ad1123 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 7 Feb 2025 16:53:57 +0800 Subject: [PATCH 10/26] big roy's comment - separate template for substancedesigner from standalonepublisher --- server/settings/tools.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/server/settings/tools.py b/server/settings/tools.py index 0aaa88a8b0..32c72e7a98 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -559,12 +559,21 @@ DEFAULT_TOOLS_VALUES = { }, { "product_types": [ - "simpleUnrealTexture", - "image", - "textures" + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "simpleUnrealTexture" + }, + { + "product_types": [ + "image", + "textures", ], "hosts": [ - "standalonepublisher", "substancedesigner" ], "task_types": [], @@ -609,12 +618,21 @@ DEFAULT_TOOLS_VALUES = { "hero_template_name_profiles": [ { "product_types": [ - "simpleUnrealTexture", + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "simpleUnrealTextureHero" + }, + { + "product_types": [ "image", "textures" ], "hosts": [ - "standalonepublisher", "substancedesigner" ], "task_types": [], From cdf7af65bf9ecad7fe6bb1fee7760ab22bf0cc6b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:20:56 +0100 Subject: [PATCH 11/26] pass 'AYON_USE_STAGING' instead of 'AYON_DEFAULT_SETTINGS_VARIANT' --- client/ayon_core/plugins/publish/collect_farm_env_variables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index ee88985905..2a58b580cd 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -32,7 +32,7 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): for key in [ "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", + "AYON_USE_STAGING", "AYON_IN_TESTS", # NOTE Not sure why workdir is needed? "AYON_WORKDIR", From ccd5e447842b632ffad3f0ec352db8d6ccedf799 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:52:03 +0100 Subject: [PATCH 12/26] keep filling 'AYON_DEFAULT_SETTINGS_VARIANT' for now --- client/ayon_core/plugins/publish/collect_farm_env_variables.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index 2a58b580cd..7ee3356cee 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -36,6 +36,8 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): "AYON_IN_TESTS", # NOTE Not sure why workdir is needed? "AYON_WORKDIR", + # DEPRECATED remove when deadline stops using it (added in 1.1.1) + "AYON_DEFAULT_SETTINGS_VARIANT", ]: value = os.getenv(key) if value: From 4b51f6479f391e853642a24059193fa3c244a772 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:16:03 +0100 Subject: [PATCH 13/26] change added version --- client/ayon_core/plugins/publish/collect_farm_env_variables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index 7ee3356cee..2782ea86ac 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -36,7 +36,7 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): "AYON_IN_TESTS", # NOTE Not sure why workdir is needed? "AYON_WORKDIR", - # DEPRECATED remove when deadline stops using it (added in 1.1.1) + # DEPRECATED remove when deadline stops using it (added in 1.1.2) "AYON_DEFAULT_SETTINGS_VARIANT", ]: value = os.getenv(key) From 6a57b4c4504bb19987a3e10bcf8f167e4ae121b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:13:16 +0100 Subject: [PATCH 14/26] use odd numbers for frame size --- client/ayon_core/tools/utils/nice_checkbox.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/utils/nice_checkbox.py b/client/ayon_core/tools/utils/nice_checkbox.py index 06845c397a..b47e3e62e6 100644 --- a/client/ayon_core/tools/utils/nice_checkbox.py +++ b/client/ayon_core/tools/utils/nice_checkbox.py @@ -328,6 +328,9 @@ class NiceCheckbox(QtWidgets.QFrame): if frame_rect.width() < 0 or frame_rect.height() < 0: return + frame_rect.setLeft(frame_rect.x() + (frame_rect.width() % 2)) + frame_rect.setTop(frame_rect.y() + (frame_rect.height() % 2)) + painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.Antialiasing) @@ -364,11 +367,16 @@ class NiceCheckbox(QtWidgets.QFrame): margin_size_c = 0 checkbox_rect = QtCore.QRect( - frame_rect.x() + margin_size_c, - frame_rect.y() + margin_size_c, - frame_rect.width() - (margin_size_c * 2), - frame_rect.height() - (margin_size_c * 2) + frame_rect.x(), + frame_rect.y(), + frame_rect.width(), + frame_rect.height() ) + if margin_size_c: + checkbox_rect.adjust( + margin_size_c, margin_size_c, + -margin_size_c, -margin_size_c + ) if checkbox_rect.width() > checkbox_rect.height(): radius = floor(checkbox_rect.height() * 0.5) From 21187bb28cdc76655e562f474b65d6245de449c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:13:25 +0100 Subject: [PATCH 15/26] use NoPen instead of transparent --- client/ayon_core/tools/utils/nice_checkbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/utils/nice_checkbox.py b/client/ayon_core/tools/utils/nice_checkbox.py index b47e3e62e6..3d9d63b6bc 100644 --- a/client/ayon_core/tools/utils/nice_checkbox.py +++ b/client/ayon_core/tools/utils/nice_checkbox.py @@ -383,7 +383,7 @@ class NiceCheckbox(QtWidgets.QFrame): else: radius = floor(checkbox_rect.width() * 0.5) - painter.setPen(QtCore.Qt.transparent) + painter.setPen(QtCore.Qt.NoPen) painter.setBrush(bg_color) painter.drawRoundedRect(checkbox_rect, radius, radius) From cbaefffabc756a7922481263fe12a335467e5d44 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 18:55:04 +0100 Subject: [PATCH 16/26] multiselection EnumDef allows empty items --- client/ayon_core/lib/attribute_definitions.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e8327a45b6..f3de4fc943 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -562,17 +562,18 @@ class EnumDef(AbstractAttrDef): multiselection: Optional[bool] = False, **kwargs ): - if not items: - raise ValueError(( - "Empty 'items' value. {} must have" + if multiselection is None: + multiselection = False + + if not items and not multiselection: + raise ValueError( + f"Empty 'items' value. {self.__class__.__name__} must have" " defined values on initialization." - ).format(self.__class__.__name__)) + ) items = self.prepare_enum_items(items) item_values = [item["value"] for item in items] item_values_set = set(item_values) - if multiselection is None: - multiselection = False if multiselection: if default is None: From 8a27bd5bc3a680d144e23c1f4a3f4a08ac3957ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:34:36 +0100 Subject: [PATCH 17/26] implemented 'PlaceholderPlainTextEdit' --- client/ayon_core/tools/utils/__init__.py | 2 ++ client/ayon_core/tools/utils/widgets.py | 41 +++++++++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/utils/__init__.py b/client/ayon_core/tools/utils/__init__.py index 4714e76ea3..9206af9beb 100644 --- a/client/ayon_core/tools/utils/__init__.py +++ b/client/ayon_core/tools/utils/__init__.py @@ -5,6 +5,7 @@ from .widgets import ( ComboBox, CustomTextComboBox, PlaceholderLineEdit, + PlaceholderPlainTextEdit, ElideLabel, HintedLineEdit, ExpandingTextEdit, @@ -89,6 +90,7 @@ __all__ = ( "ComboBox", "CustomTextComboBox", "PlaceholderLineEdit", + "PlaceholderPlainTextEdit", "ElideLabel", "HintedLineEdit", "ExpandingTextEdit", diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index 059a06648b..1074b6d4fb 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -88,23 +88,48 @@ class CustomTextComboBox(ComboBox): painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option) -class PlaceholderLineEdit(QtWidgets.QLineEdit): - """Set placeholder color of QLineEdit in Qt 5.12 and higher.""" - def __init__(self, *args, **kwargs): - super(PlaceholderLineEdit, self).__init__(*args, **kwargs) - # Change placeholder palette color - if hasattr(QtGui.QPalette, "PlaceholderText"): - filter_palette = self.palette() +class _Cache: + _placeholder_color = None + + @classmethod + def get_placeholder_color(cls): + if cls._placeholder_color is None: color_obj = get_objected_colors("font") color = color_obj.get_qcolor() color.setAlpha(67) + cls._placeholder_color = color + return cls._placeholder_color + + +class PlaceholderLineEdit(QtWidgets.QLineEdit): + """Set placeholder color of QLineEdit in Qt 5.12 and higher.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Change placeholder palette color + if hasattr(QtGui.QPalette, "PlaceholderText"): + filter_palette = self.palette() filter_palette.setColor( QtGui.QPalette.PlaceholderText, - color + _Cache.get_placeholder_color() ) self.setPalette(filter_palette) +class PlaceholderPlainTextEdit(QtWidgets.QPlainTextEdit): + """Set placeholder color of QPlainTextEdit in Qt 5.12 and higher.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Change placeholder palette color + if hasattr(QtGui.QPalette, "PlaceholderText"): + viewport = self.viewport() + filter_palette = viewport.palette() + filter_palette.setColor( + QtGui.QPalette.PlaceholderText, + _Cache.get_placeholder_color() + ) + viewport.setPalette(filter_palette) + + class ElideLabel(QtWidgets.QLabel): """Label which elide text. From bfbc8baa3cb5ec684099d61a2a101fedbb30d9c9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:34:51 +0100 Subject: [PATCH 18/26] use placeholder widgets in attribute definitions --- client/ayon_core/tools/attribute_defs/widgets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 201fd5be48..e4f1ddec51 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -22,6 +22,8 @@ from ayon_core.tools.utils import ( FocusSpinBox, FocusDoubleSpinBox, MultiSelectionComboBox, + PlaceholderLineEdit, + PlaceholderPlainTextEdit, set_style_property, ) from ayon_core.tools.utils import NiceCheckbox @@ -502,9 +504,9 @@ class TextAttrWidget(_BaseAttrDefWidget): self.multiline = self.attr_def.multiline if self.multiline: - input_widget = QtWidgets.QPlainTextEdit(self) + input_widget = PlaceholderPlainTextEdit(self) else: - input_widget = QtWidgets.QLineEdit(self) + input_widget = PlaceholderLineEdit(self) # Override context menu event to add revert to default action input_widget.contextMenuEvent = self._input_widget_context_event From 416657eefa0aa5294ea70ebd6a20e14214f55243 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:05:55 +0100 Subject: [PATCH 19/26] added empty item --- .../ayon_core/tools/attribute_defs/widgets.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 93f63730f5..32671eade7 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -2,7 +2,7 @@ import copy import typing from typing import Optional -from qtpy import QtWidgets, QtCore +from qtpy import QtWidgets, QtCore, QtGui from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -655,6 +655,9 @@ class EnumAttrWidget(_BaseAttrDefWidget): for item in self.attr_def.items: input_widget.addItem(item["label"], item["value"]) + if not self.attr_def.items: + self._add_empty_item(input_widget) + idx = input_widget.findData(self.attr_def.default) if idx >= 0: input_widget.setCurrentIndex(idx) @@ -671,6 +674,20 @@ class EnumAttrWidget(_BaseAttrDefWidget): input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) input_widget.customContextMenuRequested.connect(self._on_context_menu) + def _add_empty_item(self, input_widget): + model = input_widget.model() + if not isinstance(model, QtGui.QStandardItemModel): + return + + root_item = model.invisibleRootItem() + + empty_item = QtGui.QStandardItem() + empty_item.setData("< No items to select >", QtCore.Qt.DisplayRole) + empty_item.setData("", QtCore.Qt.UserRole) + empty_item.setFlags(QtCore.Qt.NoItemFlags) + + root_item.appendRow(empty_item) + def _on_context_menu(self, pos): menu = QtWidgets.QMenu(self) From f0943753c40565dcb1217b571eaba108b0c5b2b7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:08:44 +0100 Subject: [PATCH 20/26] set column stretch for pre-create attributes --- client/ayon_core/tools/publisher/widgets/precreate_widget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/publisher/widgets/precreate_widget.py b/client/ayon_core/tools/publisher/widgets/precreate_widget.py index 5ad203d370..b786fea3b5 100644 --- a/client/ayon_core/tools/publisher/widgets/precreate_widget.py +++ b/client/ayon_core/tools/publisher/widgets/precreate_widget.py @@ -85,6 +85,8 @@ class AttributesWidget(QtWidgets.QWidget): layout.setContentsMargins(0, 0, 0, 0) layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) + layout.setColumnStretch(0, 0) + layout.setColumnStretch(1, 1) self._layout = layout From c6093f72946898ddff80010f48509bb3a75e429c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:31:28 +0100 Subject: [PATCH 21/26] use task label if is filled --- .../tools/common_models/hierarchy.py | 28 +++++++++++++++---- client/ayon_core/tools/utils/tasks_widget.py | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/common_models/hierarchy.py b/client/ayon_core/tools/common_models/hierarchy.py index 6bccb0f468..323a433a4f 100644 --- a/client/ayon_core/tools/common_models/hierarchy.py +++ b/client/ayon_core/tools/common_models/hierarchy.py @@ -1,12 +1,18 @@ +from __future__ import annotations + import time import collections import contextlib +import typing from abc import ABC, abstractmethod import ayon_api from ayon_core.lib import NestedCacheItem +if typing.TYPE_CHECKING: + from typing import Union + HIERARCHY_MODEL_SENDER = "hierarchy.model" @@ -82,19 +88,26 @@ class TaskItem: Args: task_id (str): Task id. name (str): Name of task. + name (Union[str, None]): Task label. task_type (str): Type of task. parent_id (str): Parent folder id. """ def __init__( - self, task_id, name, task_type, parent_id + self, + task_id: str, + name: str, + label: Union[str, None], + task_type: str, + parent_id: str, ): self.task_id = task_id self.name = name + self.label = label self.task_type = task_type self.parent_id = parent_id - self._label = None + self._full_label = None @property def id(self): @@ -107,15 +120,16 @@ class TaskItem: return self.task_id @property - def label(self): + def full_label(self): """Label of task item for UI. Returns: str: Label of task item. """ - if self._label is None: - self._label = "{} ({})".format(self.name, self.task_type) + if self._full_label is None: + label = self.label or self.name + self._full_label = f"{label} ({self.task_type})" return self._label def to_data(self): @@ -128,6 +142,7 @@ class TaskItem: return { "task_id": self.task_id, "name": self.name, + "label": self.label, "parent_id": self.parent_id, "task_type": self.task_type, } @@ -159,6 +174,7 @@ def _get_task_items_from_tasks(tasks): output.append(TaskItem( task["id"], task["name"], + task["label"], task["type"], folder_id )) @@ -368,7 +384,7 @@ class HierarchyModel(object): sender (Union[str, None]): Who requested the task item. Returns: - Union[TaskItem, None]: Task item found by name and folder id. + Optional[TaskItem]: Task item found by name and folder id. """ for task_item in self.get_task_items(project_name, folder_id, sender): diff --git a/client/ayon_core/tools/utils/tasks_widget.py b/client/ayon_core/tools/utils/tasks_widget.py index bba7b93925..87a4c3db3b 100644 --- a/client/ayon_core/tools/utils/tasks_widget.py +++ b/client/ayon_core/tools/utils/tasks_widget.py @@ -270,7 +270,7 @@ class TasksQtModel(QtGui.QStandardItemModel): task_type_item_by_name, task_type_icon_cache ) - item.setData(task_item.label, QtCore.Qt.DisplayRole) + item.setData(task_item.full_label, QtCore.Qt.DisplayRole) item.setData(name, ITEM_NAME_ROLE) item.setData(task_item.id, ITEM_ID_ROLE) item.setData(task_item.task_type, TASK_TYPE_ROLE) From 94a32c0a061f5aef2e851f2eea422c63fa1ca9b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:47:58 +0100 Subject: [PATCH 22/26] fix used variable name --- client/ayon_core/tools/common_models/hierarchy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/common_models/hierarchy.py b/client/ayon_core/tools/common_models/hierarchy.py index 323a433a4f..edff8471b0 100644 --- a/client/ayon_core/tools/common_models/hierarchy.py +++ b/client/ayon_core/tools/common_models/hierarchy.py @@ -130,7 +130,7 @@ class TaskItem: if self._full_label is None: label = self.label or self.name self._full_label = f"{label} ({self.task_type})" - return self._label + return self._full_label def to_data(self): """Converts task item to data. From 8bcab93793de1c35b0da2907eea68a745a70a17f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Feb 2025 10:05:15 +0100 Subject: [PATCH 23/26] :dog: calm ruff --- client/ayon_core/addon/interfaces.py | 34 +++++++++++++++++-- client/ayon_core/pipeline/traits/lifecycle.py | 10 ++++-- .../pipeline/traits/test_content_traits.py | 9 ++--- .../pipeline/traits/test_time_traits.py | 24 +++++++++++-- .../ayon_core/pipeline/traits/test_traits.py | 30 ++++++++++------ 5 files changed, 87 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py index 303cdf3941..fa7acb6380 100644 --- a/client/ayon_core/addon/interfaces.py +++ b/client/ayon_core/addon/interfaces.py @@ -83,6 +83,10 @@ class IPluginPaths(AYONInterface): """Receive launcher actions paths. Give addons ability to add launcher actions paths. + + Returns: + list[str]: List of launcher action paths. + """ return self._get_plugin_paths_by_type("actions") @@ -98,6 +102,9 @@ class IPluginPaths(AYONInterface): Args: host_name (str): For which host are the plugins meant. + Returns: + list[str]: List of create plugin paths. + """ return self._get_plugin_paths_by_type("create") @@ -113,6 +120,9 @@ class IPluginPaths(AYONInterface): Args: host_name (str): For which host are the plugins meant. + Returns: + list[str]: List of load plugin paths. + """ return self._get_plugin_paths_by_type("load") @@ -128,6 +138,9 @@ class IPluginPaths(AYONInterface): Args: host_name (str): For which host are the plugins meant. + Returns: + list[str]: List of publish plugin paths. + """ return self._get_plugin_paths_by_type("publish") @@ -143,6 +156,9 @@ class IPluginPaths(AYONInterface): Args: host_name (str): For which host are the plugins meant. + Returns: + list[str]: List of inventory action plugin paths. + """ return self._get_plugin_paths_by_type("inventory") @@ -236,7 +252,12 @@ class ITrayAddon(AYONInterface): @staticmethod def admin_submenu(tray_menu: QtWidgets.QMenu) -> QtWidgets.QMenu: - """Get or create admin submenu.""" + """Get or create admin submenu. + + Returns: + QtWidgets.QMenu: Admin submenu. + + """ if ITrayAddon._admin_submenu is None: from qtpy import QtWidgets @@ -248,7 +269,16 @@ class ITrayAddon(AYONInterface): @staticmethod def add_action_to_admin_submenu( label: str, tray_menu: QtWidgets.QMenu) -> QtWidgets.QAction: - """Add action to admin submenu.""" + """Add action to admin submenu. + + Args: + label (str): Label of action. + tray_menu (QtWidgets.QMenu): Tray menu to add action to. + + Returns: + QtWidgets.QAction: Action added to admin submenu + + """ from qtpy import QtWidgets menu = ITrayAddon.admin_submenu(tray_menu) diff --git a/client/ayon_core/pipeline/traits/lifecycle.py b/client/ayon_core/pipeline/traits/lifecycle.py index be87a86cbc..2de90824e9 100644 --- a/client/ayon_core/pipeline/traits/lifecycle.py +++ b/client/ayon_core/pipeline/traits/lifecycle.py @@ -26,8 +26,10 @@ class Transient(TraitBase): Args: representation (Representation): Representation model. - Returns: - bool: True if representation is valid, False otherwise. + Raises: + TraitValidationError: If representation is marked as both + Persistent and Transient. + """ if representation.contains_trait(Persistent): msg = "Representation is marked as both Persistent and Transient." @@ -57,6 +59,10 @@ class Persistent(TraitBase): Args: representation (Representation): Representation model. + Raises: + TraitValidationError: If representation is marked + as both Persistent and Transient. + """ if representation.contains_trait(Transient): msg = "Representation is marked as both Persistent and Transient." diff --git a/tests/client/ayon_core/pipeline/traits/test_content_traits.py b/tests/client/ayon_core/pipeline/traits/test_content_traits.py index 78b905bab4..c1c8dcc25d 100644 --- a/tests/client/ayon_core/pipeline/traits/test_content_traits.py +++ b/tests/client/ayon_core/pipeline/traits/test_content_traits.py @@ -1,6 +1,7 @@ """Tests for the content traits.""" from __future__ import annotations +import re from pathlib import Path import pytest @@ -59,9 +60,9 @@ def test_bundles() -> None: sub_representation = Representation(name="test", traits=item) assert sub_representation.contains_trait(trait=Image) sub: MimeType = sub_representation.get_trait(trait=MimeType) - assert sub.mime_type in [ + assert sub.mime_type in { "image/jpeg", "image/tiff" - ] + } def test_file_locations_validation() -> None: @@ -94,7 +95,7 @@ def test_file_locations_validation() -> None: ) representation.add_trait(frameranged_trait) - # it should still validate fine + # it should still validate fine file_locations_trait.validate_trait(representation) # create empty file locations trait @@ -165,7 +166,7 @@ def test_get_file_location_from_frame() -> None: # test with custom regex sequence = Sequence( frame_padding=4, - frame_regex=r"boo_(?P(?P0*)\d+)\.exr") + frame_regex=re.compile("boo_(?P(?P0*)\d+)\.exr")) file_locations_list = [ FileLocation( file_path=Path(f"/path/to/boo_{frame}.exr"), diff --git a/tests/client/ayon_core/pipeline/traits/test_time_traits.py b/tests/client/ayon_core/pipeline/traits/test_time_traits.py index 100e4ed2b5..28ace89910 100644 --- a/tests/client/ayon_core/pipeline/traits/test_time_traits.py +++ b/tests/client/ayon_core/pipeline/traits/test_time_traits.py @@ -1,6 +1,7 @@ """Tests for the time related traits.""" from __future__ import annotations +import re from pathlib import Path import pytest @@ -183,6 +184,26 @@ def test_sequence_validations() -> None: with pytest.raises(TraitValidationError): representation.validate() + representation = Representation(name="test_7", traits=[ + FileLocations(file_paths=[ + FileLocation( + file_path=Path(f"/path/to/file.{frame}.exr"), + file_size=1024, + file_hash=None, + ) + for frame in range(996, 1050 + 1) # because range is zero based + ]), + Sequence( + frame_padding=4, + frame_regex=re.compile( + r"img\.(?P(?P0*)\d{4})\.png$")), + Handles( + frame_start_handle=5, + frame_end_handle=5, + inclusive=False + ) + ]) + representation.validate() def test_list_spec_to_frames() -> None: @@ -204,7 +225,7 @@ def test_list_spec_to_frames() -> None: assert Sequence.list_spec_to_frames("1") == [1] with pytest.raises( ValueError, - match="Invalid frame number in the list: .*"): + match=r"Invalid frame number in the list: .*"): Sequence.list_spec_to_frames("a") @@ -225,4 +246,3 @@ def test_sequence_get_frame_padding() -> None: assert Sequence.get_frame_padding( file_locations=representation.get_trait(FileLocations)) == 4 - diff --git a/tests/client/ayon_core/pipeline/traits/test_traits.py b/tests/client/ayon_core/pipeline/traits/test_traits.py index 42933056fa..b990c074d3 100644 --- a/tests/client/ayon_core/pipeline/traits/test_traits.py +++ b/tests/client/ayon_core/pipeline/traits/test_traits.py @@ -36,19 +36,27 @@ REPRESENTATION_DATA: dict = { }, } + class UpgradedImage(Image): """Upgraded image class.""" id = "ayon.2d.Image.v2" @classmethod def upgrade(cls, data: dict) -> UpgradedImage: # noqa: ARG003 - """Upgrade the trait.""" + """Upgrade the trait. + + Returns: + UpgradedImage: Upgraded image instance. + + """ return cls() + class InvalidTrait: """Invalid trait class.""" foo = "bar" + @pytest.fixture def representation() -> Representation: """Return a traits data instance.""" @@ -59,10 +67,11 @@ def representation() -> Representation: Planar(**REPRESENTATION_DATA[Planar.id]), ]) + def test_representation_errors(representation: Representation) -> None: """Test errors in representation.""" with pytest.raises(ValueError, - match="Invalid trait .* - ID is required."): + match=r"Invalid trait .* - ID is required."): representation.add_trait(InvalidTrait()) with pytest.raises(ValueError, @@ -70,9 +79,10 @@ def test_representation_errors(representation: Representation) -> None: representation.add_trait(Image()) with pytest.raises(ValueError, - match="Trait with ID .* not found."): + match=r"Trait with ID .* not found."): representation.remove_trait_by_id("foo") + def test_representation_traits(representation: Representation) -> None: """Test setting and getting traits.""" assert representation.get_trait_by_id( @@ -143,11 +153,12 @@ def test_representation_traits(representation: Representation) -> None: assert representation.contains_traits_by_id( trait_ids=[FileLocation.id, Bundle.id]) is False + def test_trait_removing(representation: Representation) -> None: """Test removing traits.""" - assert representation.contains_trait_by_id("nonexistent") is False + assert representation.contains_trait_by_id("nonexistent") is False with pytest.raises( - ValueError, match="Trait with ID nonexistent not found."): + ValueError, match=r"Trait with ID nonexistent not found."): representation.remove_trait_by_id("nonexistent") assert representation.contains_trait(trait=FileLocation) is True @@ -168,6 +179,7 @@ def test_trait_removing(representation: Representation) -> None: ValueError, match=f"Trait with ID {Image.id} not found."): representation.remove_trait(Image) + def test_representation_dict_properties( representation: Representation) -> None: """Test representation as dictionary.""" @@ -224,6 +236,7 @@ def test_get_version_from_id() -> None: assert TestMimeType(mime_type="foo/bar").get_version() is None + def test_get_versionless_id() -> None: """Test getting versionless trait ID.""" assert Image().get_versionless_id() == "ayon.2d.Image" @@ -271,7 +284,7 @@ def test_from_dict() -> None: }, } - with pytest.raises(ValueError, match="Trait model with ID .* not found."): + with pytest.raises(ValueError, match=r"Trait model with ID .* not found."): representation = Representation.from_dict( "test", trait_data=traits_data) @@ -302,6 +315,7 @@ def test_from_dict() -> None: "test", trait_data=traits_data) """ + def test_representation_equality() -> None: """Test representation equality.""" # rep_a and rep_b are equal @@ -348,7 +362,6 @@ def test_representation_equality() -> None: Planar(planar_configuration="RGBA"), ]) - # lets assume ids are the same (because ids are randomly generated) rep_b.representation_id = rep_d.representation_id = rep_a.representation_id rep_c.representation_id = rep_e.representation_id = rep_a.representation_id @@ -365,6 +378,3 @@ def test_representation_equality() -> None: assert rep_d != rep_e # because of the trait difference assert rep_d != rep_f - - - From dd05199aed2e7df0bf4c5239e8374b23e6fc659d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Feb 2025 10:08:28 +0100 Subject: [PATCH 24/26] :dog: calm ruff even more --- client/ayon_core/addon/interfaces.py | 38 ++++++++++++++----- .../pipeline/traits/test_content_traits.py | 2 +- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py index fa7acb6380..51242e1fc1 100644 --- a/client/ayon_core/addon/interfaces.py +++ b/client/ayon_core/addon/interfaces.py @@ -230,8 +230,8 @@ class ITrayAddon(AYONInterface): self, title: str, message: str, - icon: Optional[QtWidgets.QSystemTrayIcon]=None, - msecs: Optional[int]=None) -> None: + icon: Optional[QtWidgets.QSystemTrayIcon] = None, + msecs: Optional[int] = None) -> None: """Show tray message. Args: @@ -325,11 +325,11 @@ class ITrayAction(ITrayAddon): action.triggered.connect(self.on_action_trigger) self._action_item = action - def tray_start(self) -> None: + def tray_start(self) -> None: # noqa: PLR6301 """Start procedure in tray tool.""" return - def tray_exit(self) -> None: + def tray_exit(self) -> None: # noqa: PLR6301 """Cleanup method which is executed on tray shutdown.""" return @@ -357,7 +357,12 @@ class ITrayService(ITrayAddon): @staticmethod def services_submenu(tray_menu: QtWidgets.QMenu) -> QtWidgets.QMenu: - """Get or create services submenu.""" + """Get or create services submenu. + + Returns: + QtWidgets.QMenu: Services submenu. + + """ if ITrayService._services_submenu is None: from qtpy import QtWidgets @@ -390,21 +395,36 @@ class ITrayService(ITrayAddon): @staticmethod def get_icon_running() -> QtWidgets.QIcon: - """Get running icon.""" + """Get running icon. + + Returns: + QtWidgets.QIcon: Returns "running" icon. + + """ if ITrayService._icon_running is None: ITrayService._load_service_icons() return ITrayService._icon_running @staticmethod def get_icon_idle() -> QtWidgets.QIcon: - """Get idle icon.""" + """Get idle icon. + + Returns: + QtWidgets.QIcon: Returns "idle" icon. + + """ if ITrayService._icon_idle is None: ITrayService._load_service_icons() return ITrayService._icon_idle @staticmethod def get_icon_failed() -> QtWidgets.QIcon: - """Get failed icon.""" + """Get failed icon. + + Returns: + QtWidgets.QIcon: Returns "failed" icon. + + """ if ITrayService._icon_failed is None: ITrayService._load_service_icons() return ITrayService._icon_failed @@ -447,7 +467,7 @@ class IHostAddon(AYONInterface): def host_name(self) -> str: """Name of host which addon represents.""" - def get_workfile_extensions(self) -> list[str]: + def get_workfile_extensions(self) -> list[str]: # noqa: PLR6301 """Define workfile extensions for host. Not all hosts support workfiles thus this is optional implementation. diff --git a/tests/client/ayon_core/pipeline/traits/test_content_traits.py b/tests/client/ayon_core/pipeline/traits/test_content_traits.py index c1c8dcc25d..4aa149b8ee 100644 --- a/tests/client/ayon_core/pipeline/traits/test_content_traits.py +++ b/tests/client/ayon_core/pipeline/traits/test_content_traits.py @@ -166,7 +166,7 @@ def test_get_file_location_from_frame() -> None: # test with custom regex sequence = Sequence( frame_padding=4, - frame_regex=re.compile("boo_(?P(?P0*)\d+)\.exr")) + frame_regex=re.compile(r"boo_(?P(?P0*)\d+)\.exr")) file_locations_list = [ FileLocation( file_path=Path(f"/path/to/boo_{frame}.exr"), From 21d1dae0f5b24a0dd1ebf8968acccb5010fc13ad Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Feb 2025 10:09:51 +0100 Subject: [PATCH 25/26] :dog: calm ruff even even more --- .../ayon_core/pipeline/traits/test_two_dimesional_traits.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/client/ayon_core/pipeline/traits/test_two_dimesional_traits.py b/tests/client/ayon_core/pipeline/traits/test_two_dimesional_traits.py index 328bd83469..f09d2b0864 100644 --- a/tests/client/ayon_core/pipeline/traits/test_two_dimesional_traits.py +++ b/tests/client/ayon_core/pipeline/traits/test_two_dimesional_traits.py @@ -42,6 +42,7 @@ def test_get_file_location_for_udim() -> None: udim=1001 ) == file_locations_list[0] + def test_get_udim_from_file_location() -> None: """Test get_udim_from_file_location.""" file_location_1 = FileLocation( From 71b69c4ded6707cd66118332133f30450fc89489 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 13 Feb 2025 11:56:36 +0100 Subject: [PATCH 26/26] :bug: fix padding --- client/ayon_core/pipeline/traits/temporal.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/traits/temporal.py b/client/ayon_core/pipeline/traits/temporal.py index 286336ea55..ad4b65d8ee 100644 --- a/client/ayon_core/pipeline/traits/temporal.py +++ b/client/ayon_core/pipeline/traits/temporal.py @@ -384,7 +384,13 @@ class Sequence(TraitBase): """ src_collection = Sequence._get_collection(file_locations) - return len(str(max(src_collection.indexes))) + padding = src_collection.padding + # sometimes Clique doens't get the padding right so + # we need to calculate it manually + if padding == 0: + padding = len(str(max(src_collection.indexes))) + + return padding @staticmethod def get_frame_list(