From 31c37efce2501fbdf1cc78e2ce7d2b75e4ca1b46 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:18:43 +0100 Subject: [PATCH 01/40] use single variable to cache folders --- client/ayon_core/pipeline/create/context.py | 39 ++++++++------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 4b7e323737..d7fdad6fdb 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -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 = {} @@ -1485,38 +1483,31 @@ class CreateContext: remainder_paths = set() for folder_path in output: # Skip empty/invalid folder paths - if folder_path is None or "/" not in folder_path: + if "/" 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 folders + for path in remainder_paths - found_paths: + self._folder_entities_by_path[path] = None return output @@ -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 From d860919b22cb7832df2963bf9c23d2aabfcad919 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:21:31 +0100 Subject: [PATCH 02/40] remove unnecessary check --- client/ayon_core/pipeline/create/context.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index d7fdad6fdb..a0145dee29 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1478,11 +1478,11 @@ 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 + # Skip invalid folder paths (e.g. if only folder name + # is passed in) if "/" not in folder_path: continue @@ -1505,7 +1505,7 @@ class CreateContext: output[folder_path] = folder_entity self._folder_entities_by_path[folder_path] = folder_entity - # Cache empty folders + # Cache empty folder entities for path in remainder_paths - found_paths: self._folder_entities_by_path[path] = None From ce4e8a1b04acb42bfd81ca65b4f17d6270e940ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:22:47 +0100 Subject: [PATCH 03/40] handle empty paths --- client/ayon_core/pipeline/create/context.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a0145dee29..3cff5e03b1 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1481,9 +1481,8 @@ class CreateContext: } remainder_paths = set() for folder_path in output: - # Skip invalid folder paths (e.g. if only folder name - # is passed in) - if "/" 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_entities_by_path: From b4009b718ae6bdcfe398361dc3cd4f1176cfcee1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:23:15 +0100 Subject: [PATCH 04/40] discard None from folder paths --- client/ayon_core/pipeline/create/context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 3cff5e03b1..6bfd64b822 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1666,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") From eb561dd371cb0ae7825ac2fc281ffb7fd1cfe720 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:07:31 +0100 Subject: [PATCH 05/40] Remove `os.link` compatibility for Windows. Support for Windows exists since Py 3.2 See: https://docs.python.org/3/library/os.html#os.link --- client/ayon_core/lib/path_tools.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index 5c81fbfebf..efe2556afe 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -38,31 +38,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): From 0a970abed6bcccb2755f70af189959ca00c6b3d9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:11:52 +0100 Subject: [PATCH 06/40] Remove Python 2 code. `unicode` does not exist in Py3+ --- client/ayon_core/lib/log.py | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index 36c39f9d84..1ed16b36ff 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -11,12 +11,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. """ @@ -47,27 +47,7 @@ class LogStreamHandler(logging.StreamHandler): 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 From 9f9c03179a6ab3bec5dfe3f8e757be6baf8cc055 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:16:16 +0100 Subject: [PATCH 07/40] Fix enabled check + fix docstrings --- client/ayon_core/lib/log.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index 1ed16b36ff..d619310eb8 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -25,21 +25,21 @@ 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: return try: msg = self.format(record) From 9343e3cca98bf9a3f546efdfd77b43a5e78d9da3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:20:27 +0100 Subject: [PATCH 08/40] Remove `Logger.mongo_process_id` --- client/ayon_core/lib/log.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index 36c39f9d84..03edc93c74 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -141,8 +141,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): From 300e086c8d4075b39e5627e1a68e7cfa6a6c8793 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:28:59 +0100 Subject: [PATCH 09/40] Update client/ayon_core/lib/log.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index d619310eb8..d392610f32 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -39,7 +39,7 @@ class LogStreamHandler(logging.StreamHandler): self.enabled = False def emit(self, record): - if not self.enabled: + if not self.enabled or self.stream is None: return try: msg = self.format(record) From 01bd1594f5bbabb21cfb85ee312064bbbe9c7a7b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:29:31 +0100 Subject: [PATCH 10/40] Remove condition that was moved up --- client/ayon_core/lib/log.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index d392610f32..692df93dad 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -45,8 +45,6 @@ class LogStreamHandler(logging.StreamHandler): msg = self.format(record) msg = Terminal.log(msg) stream = self.stream - if stream is None: - return stream.write(f"{msg}\n") self.flush() except (KeyboardInterrupt, SystemExit): From d4ace0706c90e032bdea5139cdc8e690b2f2c673 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:30:53 +0100 Subject: [PATCH 11/40] Remove unused import --- client/ayon_core/lib/path_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index efe2556afe..c6a833f43a 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -1,7 +1,6 @@ import os import re import logging -import platform import clique From df9821f928af8f26863f502737f863cd39778ef9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:31:21 +0100 Subject: [PATCH 12/40] Fix typo --- client/ayon_core/lib/path_tools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index c6a833f43a..ebd22b43c5 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -185,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) @@ -193,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: + sorted(filtered_files) + return filtered_files[-1] return None From a02954d908507538e62de6818d3339ee945ce4fe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 23:19:23 +0100 Subject: [PATCH 13/40] Fix sorting Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/path_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index ebd22b43c5..31baac168c 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -196,7 +196,7 @@ def get_last_version_from_path(path_dir, filter): filtered_files.append(file) if filtered_files: - sorted(filtered_files) + filtered_files.sort() return filtered_files[-1] return None From 8c87a66cfe29c1eb8dc4a059364a38e3175aebf1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 1 Nov 2024 12:45:13 +0100 Subject: [PATCH 14/40] Remove unused import --- client/ayon_core/lib/log.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index f48d3767c9..0c2fe5e2d4 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -1,6 +1,5 @@ import os import sys -import uuid import getpass import logging import platform From 1d053c6b7cee154b7e0fc19aa841428c632e60e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:40:04 +0100 Subject: [PATCH 15/40] action upload to ynput cloud --- .github/workflows/upload_to_ynput_cloud.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/upload_to_ynput_cloud.yml diff --git a/.github/workflows/upload_to_ynput_cloud.yml b/.github/workflows/upload_to_ynput_cloud.yml new file mode 100644 index 0000000000..7745a8e016 --- /dev/null +++ b/.github/workflows/upload_to_ynput_cloud.yml @@ -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 }} From ead5d9963a63413f917ad21329979f7e1147de79 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 1 Nov 2024 14:42:52 +0000 Subject: [PATCH 16/40] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index b2480af462..af93f3e660 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.5+dev" +__version__ = "1.0.6" diff --git a/package.py b/package.py index 38d930189f..b3bc01d0d0 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.5+dev" +version = "1.0.6" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 31acb3f8b4..cccc81c069 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.5+dev" +version = "1.0.6" description = "" authors = ["Ynput Team "] readme = "README.md" From 348614745a8fc383d16bdf408f5eaa35bc124999 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 1 Nov 2024 14:43:31 +0000 Subject: [PATCH 17/40] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index af93f3e660..2b2af81e18 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.6" +__version__ = "1.0.6+dev" diff --git a/package.py b/package.py index b3bc01d0d0..59f0e82be0 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.6" +version = "1.0.6+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index cccc81c069..ca626eff00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.6" +version = "1.0.6+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 7d2e676f522d956c406adc021ced1287004d1b1a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:17:57 +0100 Subject: [PATCH 18/40] create attributes widget can show overriden values --- .../publisher/widgets/product_attributes.py | 67 ++++++++++++++++--- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 61d5ca111d..b49f846ffa 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -1,6 +1,9 @@ +from typing import Dict + from qtpy import QtWidgets, QtCore from ayon_core.lib.attribute_definitions import UnknownDef +from ayon_core.tools.utils import set_style_property from ayon_core.tools.attribute_defs import create_widget_for_attr_def from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend from ayon_core.tools.publisher.constants import ( @@ -9,6 +12,22 @@ from ayon_core.tools.publisher.constants import ( ) +def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): + set_style_property( + label, + "overriden", + "1" if overriden else "" + ) + + +class _CreateAttrDefInfo: + def __init__(self, attr_def, instance_ids, defaults, label_widget): + self.attr_def = attr_def + self.instance_ids = instance_ids + self.defaults = defaults + self.label_widget = label_widget + + class CreatorAttrsWidget(QtWidgets.QWidget): """Widget showing creator specific attributes for selected instances. @@ -51,8 +70,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 +99,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 +114,20 @@ class CreatorAttrsWidget(QtWidgets.QWidget): content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) row = 0 - for attr_def, instance_ids, values in result: + for attr_def, info_by_id in result: widget = create_widget_for_attr_def(attr_def, content_widget) + default_values = set() if attr_def.is_value_def: + default_values = [] + 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 +136,10 @@ 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 + 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,8 +151,14 @@ 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) tooltip = attr_def.tooltip @@ -138,6 +174,8 @@ class CreatorAttrsWidget(QtWidgets.QWidget): ) if not attr_def.is_label_horizontal: row += 1 + attr_def_info.label_widget = label_widget + _set_label_overriden(label_widget, is_overriden) content_layout.addWidget( widget, row, col_num, 1, expand_cols @@ -165,12 +203,19 @@ class CreatorAttrsWidget(QtWidgets.QWidget): 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 + _set_label_overriden(attr_def_info.label_widget, 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 ) From ecc7d2bde9d2755a74248c05a81482d2fe906c29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:18:15 +0100 Subject: [PATCH 19/40] change return type of 'get_creator_attribute_definitions' --- client/ayon_core/tools/publisher/abstract.py | 2 +- .../tools/publisher/models/create.py | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index a6ae93cecd..5de3f72de1 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -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 diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 9c13d8ae2f..e4c208f1e8 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -769,7 +769,7 @@ class CreateModel: 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,12 +796,23 @@ 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( From a1f74f2c783473fd2692efb7a9321dad36f87199 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:04:52 +0100 Subject: [PATCH 20/40] added styling to overriden label --- client/ayon_core/style/data.json | 4 ++++ client/ayon_core/style/style.css | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/client/ayon_core/style/data.json b/client/ayon_core/style/data.json index 7389387d97..d4a8b6180b 100644 --- a/client/ayon_core/style/data.json +++ b/client/ayon_core/style/data.json @@ -60,7 +60,11 @@ "icon-alert-tools": "#AA5050", "icon-entity-default": "#bfccd6", "icon-entity-disabled": "#808080", + "font-entity-deprecated": "#666666", + + "font-overridden": "#FF8C1A", + "overlay-messages": { "close-btn": "#D3D8DE", "bg-success": "#458056", diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 3d84d917a4..0d1d4f710e 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -44,6 +44,10 @@ QLabel { background: transparent; } +QLabel[overriden="1"] { + color: {color:font-overridden}; +} + /* Inputs */ QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { border: 1px solid {color:border}; From 0e0620770ff03039346e1eb29852531dbceed42c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:32:14 +0100 Subject: [PATCH 21/40] fix variable definition --- client/ayon_core/tools/publisher/widgets/product_attributes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index b49f846ffa..ab41812e4e 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -116,9 +116,8 @@ class CreatorAttrsWidget(QtWidgets.QWidget): row = 0 for attr_def, info_by_id in result: widget = create_widget_for_attr_def(attr_def, content_widget) - default_values = set() + default_values = [] if attr_def.is_value_def: - default_values = [] values = [] for item in info_by_id.values(): values.append(item["value"]) From 9484c42b9a646690769c21b1794f354b49ee7fee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:09:46 +0100 Subject: [PATCH 22/40] 'get_publish_attribute_definitions' returns default values too --- client/ayon_core/tools/publisher/abstract.py | 2 +- .../ayon_core/tools/publisher/models/create.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 5de3f72de1..7fad2b8176 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -383,7 +383,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ) -> List[Tuple[ str, List[AbstractAttrDef], - Dict[str, List[Tuple[str, Any]]] + Dict[str, List[Tuple[str, Any, Any]]] ]]: pass diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index e4c208f1e8..8763d79712 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -846,7 +846,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. @@ -876,21 +876,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(): @@ -904,7 +904,7 @@ class CreateModel: output.append(( plugin_name, attr_defs_by_plugin_name[plugin_name], - all_plugin_values + all_plugin_values[plugin_name], )) return output From d99dff1610f4243f8217a35968d73c54e7a24804 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:10:01 +0100 Subject: [PATCH 23/40] modified publish attributes to display overrides --- .../publisher/widgets/product_attributes.py | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index ab41812e4e..206eebc6f1 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -1,8 +1,9 @@ -from typing import Dict +import typing +from typing import Dict, List, Any from qtpy import QtWidgets, QtCore -from ayon_core.lib.attribute_definitions import UnknownDef +from ayon_core.lib.attribute_definitions import AbstractAttrDef, UnknownDef from ayon_core.tools.utils import set_style_property from ayon_core.tools.attribute_defs import create_widget_for_attr_def from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend @@ -11,6 +12,9 @@ from ayon_core.tools.publisher.constants import ( INPUTS_LAYOUT_VSPACING, ) +if typing.TYPE_CHECKING: + from typing import Union + def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): set_style_property( @@ -28,6 +32,22 @@ class _CreateAttrDefInfo: self.label_widget = label_widget +class _PublishAttrDefInfo: + def __init__( + self, + attr_def: AbstractAttrDef, + plugin_name: str, + instance_ids: List["Union[str, None]"], + defaults: List[Any], + label_widget: "Union[None, QtWidgets.QLabel]", + ): + self.attr_def = attr_def + self.plugin_name = plugin_name + self.instance_ids = instance_ids + self.defaults = defaults + self.label_widget = label_widget + + class CreatorAttrsWidget(QtWidgets.QWidget): """Widget showing creator specific attributes for selected instances. @@ -267,9 +287,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 @@ -298,9 +316,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 @@ -319,9 +335,7 @@ 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 @@ -334,6 +348,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: @@ -368,35 +383,58 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget.value_changed.connect(self._input_value_changed) - 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: + _set_label_overriden(label_widget, 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 + _set_label_overriden(attr_def_info.label_widget, 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_instance_attr_defs_change(self, event): From 98674eb4366bb063d9d09789223b3403db74f7bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:07:40 +0100 Subject: [PATCH 24/40] change overriden color --- client/ayon_core/style/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/style/data.json b/client/ayon_core/style/data.json index d4a8b6180b..748a51238a 100644 --- a/client/ayon_core/style/data.json +++ b/client/ayon_core/style/data.json @@ -63,7 +63,7 @@ "font-entity-deprecated": "#666666", - "font-overridden": "#FF8C1A", + "font-overridden": "#33B461", "overlay-messages": { "close-btn": "#D3D8DE", From 46acbacaac83de7245404e978fe9d9423aa5b36a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:51:29 +0100 Subject: [PATCH 25/40] added typehints and tiny docstring --- .../publisher/widgets/product_attributes.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 206eebc6f1..b0b2640640 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -25,14 +25,22 @@ def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): class _CreateAttrDefInfo: - def __init__(self, attr_def, instance_ids, defaults, label_widget): - self.attr_def = attr_def - self.instance_ids = instance_ids - self.defaults = defaults - self.label_widget = label_widget + """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[None, QtWidgets.QLabel]", + ): + self.attr_def: AbstractAttrDef = attr_def + self.instance_ids: List["Union[str, None]"] = instance_ids + self.defaults: List[Any] = defaults + self.label_widget: "Union[None, QtWidgets.QLabel]" = label_widget class _PublishAttrDefInfo: + """Helper class to store information about publish attribute definition.""" def __init__( self, attr_def: AbstractAttrDef, @@ -41,11 +49,11 @@ class _PublishAttrDefInfo: defaults: List[Any], label_widget: "Union[None, QtWidgets.QLabel]", ): - self.attr_def = attr_def - self.plugin_name = plugin_name - self.instance_ids = instance_ids - self.defaults = defaults - self.label_widget = label_widget + 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[None, QtWidgets.QLabel]" = label_widget class CreatorAttrsWidget(QtWidgets.QWidget): From 994dc956c264daccffb5b8bbe7aa8786589be2d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:16:57 +0100 Subject: [PATCH 26/40] use blue color --- client/ayon_core/style/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/style/data.json b/client/ayon_core/style/data.json index 748a51238a..24629ec085 100644 --- a/client/ayon_core/style/data.json +++ b/client/ayon_core/style/data.json @@ -63,7 +63,7 @@ "font-entity-deprecated": "#666666", - "font-overridden": "#33B461", + "font-overridden": "#91CDFC", "overlay-messages": { "close-btn": "#D3D8DE", From 3a8a9ec4831be2b72d6ae3f7a2ba35fc20a32482 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:25:20 +0100 Subject: [PATCH 27/40] implemented logic to revert to default values --- client/ayon_core/tools/publisher/abstract.py | 17 ++++ client/ayon_core/tools/publisher/control.py | 12 +++ .../tools/publisher/models/create.py | 98 ++++++++++++------- 3 files changed, 93 insertions(+), 34 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 7fad2b8176..4ed91813d3 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -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, @@ -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, diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 347755d557..98fdda08cf 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -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, diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 8763d79712..ca26749b65 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -40,6 +40,7 @@ from ayon_core.tools.publisher.abstract import ( ) CREATE_EVENT_SOURCE = "publisher.create.model" +_DEFAULT_VALUE = object() class CreatorType: @@ -752,20 +753,12 @@ 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] @@ -816,28 +809,18 @@ class CreateModel: 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, @@ -1064,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 From 56a07fe9183eaa33ad202261afb2d880499d2805 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:28:30 +0100 Subject: [PATCH 28/40] added 'AttributeDefinitionsLabel' helper label widget --- .../ayon_core/tools/attribute_defs/widgets.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 026aea00ad..e1977cca2c 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -20,11 +20,14 @@ from ayon_core.tools.utils import ( FocusSpinBox, FocusDoubleSpinBox, MultiSelectionComboBox, + set_style_property, ) from ayon_core.tools.utils import NiceCheckbox from .files_widget import FilesWidget +_REVERT_TO_DEFAULT_LABEL = "Revert to default" + def create_widget_for_attr_def(attr_def, parent=None): widget = _create_widget_for_attr_def(attr_def, parent) @@ -74,6 +77,52 @@ def _create_widget_for_attr_def(attr_def, parent=None): )) +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): """Create widgets for attribute definitions in grid layout. From 33a5195b7156e444139232b55545058f34e173bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:28:46 +0100 Subject: [PATCH 29/40] added 'AttributeDefinitionsLabel' to init --- client/ayon_core/tools/attribute_defs/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/attribute_defs/__init__.py b/client/ayon_core/tools/attribute_defs/__init__.py index f991fdec3d..7f6cbb41be 100644 --- a/client/ayon_core/tools/attribute_defs/__init__.py +++ b/client/ayon_core/tools/attribute_defs/__init__.py @@ -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", ) From 7d0f1f97e4ded1cc2a855e7db51fcceb2affc560 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:29:48 +0100 Subject: [PATCH 30/40] use new label in product attributes --- .../publisher/widgets/product_attributes.py | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index b0b2640640..07cbfb1907 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -4,8 +4,10 @@ from typing import Dict, List, Any from qtpy import QtWidgets, QtCore from ayon_core.lib.attribute_definitions import AbstractAttrDef, UnknownDef -from ayon_core.tools.utils import set_style_property -from ayon_core.tools.attribute_defs import create_widget_for_attr_def +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, @@ -16,14 +18,6 @@ if typing.TYPE_CHECKING: from typing import Union -def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): - set_style_property( - label, - "overriden", - "1" if overriden else "" - ) - - class _CreateAttrDefInfo: """Helper class to store information about create attribute definition.""" def __init__( @@ -31,12 +25,14 @@ class _CreateAttrDefInfo: attr_def: AbstractAttrDef, instance_ids: List["Union[str, None]"], defaults: List[Any], - label_widget: "Union[None, QtWidgets.QLabel]", + 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[None, QtWidgets.QLabel]" = label_widget + self.label_widget: "Union[AttributeDefinitionsLabel, None]" = ( + label_widget + ) class _PublishAttrDefInfo: @@ -47,13 +43,15 @@ class _PublishAttrDefInfo: plugin_name: str, instance_ids: List["Union[str, None]"], defaults: List[Any], - label_widget: "Union[None, QtWidgets.QLabel]", + 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[None, QtWidgets.QLabel]" = label_widget + self.label_widget: "Union[AttributeDefinitionsLabel, None]" = ( + label_widget + ) class CreatorAttrsWidget(QtWidgets.QWidget): @@ -187,7 +185,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): 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) @@ -202,7 +202,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): if not attr_def.is_label_horizontal: row += 1 attr_def_info.label_widget = label_widget - _set_label_overriden(label_widget, is_overriden) + label_widget.set_overridden(is_overriden) content_layout.addWidget( widget, row, col_num, 1, expand_cols @@ -237,7 +237,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): if attr_def_info.label_widget is not None: defaults = attr_def_info.defaults is_overriden = len(defaults) != 1 or value not in defaults - _set_label_overriden(attr_def_info.label_widget, is_overriden) + attr_def_info.label_widget.set_overridden(is_overriden) self._controller.set_instances_create_attr_values( attr_def_info.instance_ids, @@ -367,7 +367,9 @@ 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 + ) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) @@ -423,7 +425,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget.set_value(values[0]) if label_widget is not None: - _set_label_overriden(label_widget, is_overriden) + label_widget.set_overridden(is_overriden) self._scroll_area.setWidget(content_widget) self._content_widget = content_widget @@ -436,7 +438,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): if attr_def_info.label_widget is not None: defaults = attr_def_info.defaults is_overriden = len(defaults) != 1 or value not in defaults - _set_label_overriden(attr_def_info.label_widget, is_overriden) + attr_def_info.label_widget.set_overridden(is_overriden) self._controller.set_instances_publish_attr_values( attr_def_info.instance_ids, From 5569c95aefb02dec9ef1760fa6dd7a0eb497707b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:29:56 +0100 Subject: [PATCH 31/40] change style of label --- client/ayon_core/style/style.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 0d1d4f710e..bd96a3aeed 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -44,10 +44,6 @@ QLabel { background: transparent; } -QLabel[overriden="1"] { - color: {color:font-overridden}; -} - /* Inputs */ QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { border: 1px solid {color:border}; @@ -1589,6 +1585,10 @@ CreateNextPageOverlay { } /* Attribute Definition widgets */ +AttributeDefinitionsLabel[overridden="1"] { + color: {color:font-overridden}; +} + AttributeDefinitionsWidget QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { padding: 1px; } From 9de6a74789a4d8ba92b66806b1005e37e3a977b7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:31:23 +0100 Subject: [PATCH 32/40] base attribute widget can handle reset to default logic --- .../ayon_core/tools/attribute_defs/widgets.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index e1977cca2c..09637a9696 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -241,11 +241,18 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): 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) @@ -254,6 +261,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( From bbe1d9e3fd1b8e6ca284614ccf5e6ee5d212efd5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:32:19 +0100 Subject: [PATCH 33/40] 'create_widget_for_attr_def' can pass in all init args --- .../ayon_core/tools/attribute_defs/widgets.py | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 09637a9696..d3f51a196c 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -1,4 +1,6 @@ import copy +import typing +from typing import Optional from qtpy import QtWidgets, QtCore @@ -26,11 +28,20 @@ from ayon_core.tools.utils import NiceCheckbox from .files_widget import FilesWidget +if typing.TYPE_CHECKING: + from typing import Union + _REVERT_TO_DEFAULT_LABEL = "Revert to default" -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) @@ -39,42 +50,50 @@ 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): From c23cf6746d8014f46921fcc610d894b36f027d43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:32:58 +0100 Subject: [PATCH 34/40] 'AttributeDefinitionsWidget' shows overriden values on labels --- .../ayon_core/tools/attribute_defs/widgets.py | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index d3f51a196c..94121e51bc 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -151,16 +151,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() @@ -202,6 +204,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 @@ -213,7 +216,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) @@ -228,6 +237,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 ) @@ -236,7 +248,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 @@ -249,13 +261,26 @@ 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 From 47973464fdd7fd923a208d8b44b78da15bcd69f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:33:38 +0100 Subject: [PATCH 35/40] remoe python 2 super calls --- client/ayon_core/tools/attribute_defs/widgets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 94121e51bc..118f4b5f64 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -364,7 +364,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) @@ -373,7 +373,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: @@ -381,7 +381,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): @@ -596,7 +596,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): @@ -723,7 +723,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: From b5d018c071341474e91c97f60b389a21e45f30b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:34:41 +0100 Subject: [PATCH 36/40] publisher does handle revert to default --- .../publisher/widgets/product_attributes.py | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 07cbfb1907..cb165d1be7 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -141,7 +141,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): row = 0 for attr_def, info_by_id in result: - widget = create_widget_for_attr_def(attr_def, content_widget) + widget = create_widget_for_attr_def( + attr_def, content_widget, handle_revert_to_default=False + ) default_values = [] if attr_def.is_value_def: values = [] @@ -161,6 +163,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): widget.set_value(values, True) widget.value_changed.connect(self._input_value_changed) + 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 ) @@ -203,6 +208,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): 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 @@ -245,6 +253,15 @@ class CreatorAttrsWidget(QtWidgets.QWidget): 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, + ) + class PublishPluginAttrsWidget(QtWidgets.QWidget): """Widget showing publish plugin attributes for selected instances. @@ -346,7 +363,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): 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 @@ -370,6 +387,9 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): 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) @@ -392,6 +412,9 @@ 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 + ) instance_ids = [] values = [] @@ -447,6 +470,17 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): 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, + ) + def _on_instance_attr_defs_change(self, event): for instance_id in event.data: if ( From 2d51436da71fb9b6b95409779bc1f33715e837af Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:46:35 +0100 Subject: [PATCH 37/40] refresh content --- client/ayon_core/tools/publisher/widgets/product_attributes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index cb165d1be7..3ff295c986 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -261,6 +261,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): attr_def_info.instance_ids, attr_def_info.attr_def.key, ) + self._refresh_content() class PublishPluginAttrsWidget(QtWidgets.QWidget): @@ -480,6 +481,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): 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: From 9be42980bdb46578b6a04a7424d1a04b165e507e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:58:21 +0100 Subject: [PATCH 38/40] implemented request restart logic for most of widgets --- .../tools/attribute_defs/_constants.py | 1 + .../ayon_core/tools/attribute_defs/widgets.py | 58 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 client/ayon_core/tools/attribute_defs/_constants.py diff --git a/client/ayon_core/tools/attribute_defs/_constants.py b/client/ayon_core/tools/attribute_defs/_constants.py new file mode 100644 index 0000000000..b58a05bac6 --- /dev/null +++ b/client/ayon_core/tools/attribute_defs/_constants.py @@ -0,0 +1 @@ +REVERT_TO_DEFAULT_LABEL = "Revert to default" diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 118f4b5f64..03482c1006 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -26,13 +26,12 @@ from ayon_core.tools.utils import ( ) 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 -_REVERT_TO_DEFAULT_LABEL = "Revert to default" - def create_widget_for_attr_def( attr_def: AbstractAttrDef, @@ -133,7 +132,7 @@ class AttributeDefinitionsLabel(QtWidgets.QLabel): def _on_context_menu(self, point: QtCore.QPoint): menu = QtWidgets.QMenu(self) action = QtWidgets.QAction(menu) - action.setText(_REVERT_TO_DEFAULT_LABEL) + action.setText(REVERT_TO_DEFAULT_LABEL) action.triggered.connect(self._request_revert_to_default) menu.addAction(action) menu.exec_(self.mapToGlobal(point)) @@ -393,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) @@ -430,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() @@ -495,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") @@ -516,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() @@ -568,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) @@ -631,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: From cc45af7a96023b4ee9d39e81968bf0cce2290508 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:58:55 +0100 Subject: [PATCH 39/40] implemented request revert on files widget --- .../tools/attribute_defs/files_widget.py | 72 ++++++++++++------- .../ayon_core/tools/attribute_defs/widgets.py | 15 ++++ 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/files_widget.py b/client/ayon_core/tools/attribute_defs/files_widget.py index 95091bed5a..46399c5fce 100644 --- a/client/ayon_core/tools/attribute_defs/files_widget.py +++ b/client/ayon_core/tools/attribute_defs/files_widget.py @@ -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() diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 03482c1006..22f4bfe535 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -811,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() From 53a839b34fcf04669e094e728448e95ba792d4f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:15:09 +0100 Subject: [PATCH 40/40] fix condition triggering refresh of values in UI --- .../ayon_core/tools/publisher/widgets/product_attributes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 3ff295c986..2b9f316d41 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -232,7 +232,7 @@ 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 @@ -498,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