From c31cdf94d18da2946d2c620460a7c5980527667b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 27 Jul 2021 10:30:26 +0200 Subject: [PATCH 01/71] initial support for configurable dirmap --- openpype/hosts/maya/api/__init__.py | 18 ++++++++++ .../defaults/project_settings/maya.json | 13 ++++++++ .../defaults/project_settings/unreal.json | 3 +- .../projects_schema/schema_project_maya.json | 33 +++++++++++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 4697d212de..027fa871e8 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -26,6 +26,24 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") def install(): + from openpype.settings import get_project_settings + + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {} + if mapping.get("source-path") and project_settings["maya"]["maya-dirmap"]["enabled"] is True: + log.info("Processing directory mapping ...") + cmds.dirmap(en=True) + for k, sp in enumerate(mapping["source-path"]): + try: + print("{} -> {}".format(sp, mapping["destination-path"][k])) + cmds.dirmap(m=[sp, mapping["destination-path"][k]]) + cmds.dirmap(m=[mapping["destination-path"][k], sp]) + except IndexError: + # missing corresponding destination path + log.error(("invalid dirmap mapping, missing corresponding" + " destination directory.")) + break + pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) avalon.register_plugin_path(avalon.Creator, CREATE_PATH) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 284a1a0040..b92dc52b92 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -7,6 +7,19 @@ "workfile": "ma", "yetiRig": "ma" }, + "maya-dirmap": { + "enabled": true, + "paths": { + "source-path": [ + "foo1", + "foo2" + ], + "destination-path": [ + "bar1", + "bar2" + ] + } + }, "create": { "CreateLook": { "enabled": true, diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 46b9ca2a18..dad61cd1f0 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,6 +1,5 @@ { "project_setup": { - "dev_mode": true, - "install_unreal_python_engine": false + "dev_mode": true } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 0a59cab510..e70c0da708 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -14,6 +14,39 @@ "type": "text" } }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "maya-dirmap", + "label": "Maya Directory Mapping", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "dict", + "key": "paths", + "children": [ + { + "type": "list", + "object_type": "text", + "key": "source-path", + "label": "Source Path" + }, + { + "type": "list", + "object_type": "text", + "key": "destination-path", + "label": "Destination Path" + } + ] + } + ] + }, { "type": "schema", "name": "schema_maya_create" From 7a7d44e628f212293ef0a4d71ee8885420944d7d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Aug 2021 16:15:09 +0200 Subject: [PATCH 02/71] better error handling --- openpype/hosts/maya/api/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 027fa871e8..7af22e2ca8 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -30,19 +30,25 @@ def install(): project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {} - if mapping.get("source-path") and project_settings["maya"]["maya-dirmap"]["enabled"] is True: + mapping_enabled = project_settings["maya"]["maya-dirmap"]["enabled"] + if mapping.get("source-path") and mapping_enabled is True: log.info("Processing directory mapping ...") cmds.dirmap(en=True) for k, sp in enumerate(mapping["source-path"]): try: print("{} -> {}".format(sp, mapping["destination-path"][k])) - cmds.dirmap(m=[sp, mapping["destination-path"][k]]) - cmds.dirmap(m=[mapping["destination-path"][k], sp]) + cmds.dirmap(m=(sp, mapping["destination-path"][k])) + cmds.dirmap(m=(mapping["destination-path"][k], sp)) except IndexError: # missing corresponding destination path log.error(("invalid dirmap mapping, missing corresponding" " destination directory.")) break + except RuntimeError: + log.error("invalid path {} -> {}, mapping not registered".format( + sp, mapping["destination-path"][k] + )) + continue pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) From 0a9c335f9078f77c39c1efbf810ae0554b701797 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Aug 2021 16:32:10 +0200 Subject: [PATCH 03/71] =?UTF-8?q?=E2=86=A9=EF=B8=8F=20backward=20compatibi?= =?UTF-8?q?lity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/maya/api/__init__.py | 56 ++++++++++++++++++----------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 7af22e2ca8..9219da407f 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -29,26 +29,8 @@ def install(): from openpype.settings import get_project_settings project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) - mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {} - mapping_enabled = project_settings["maya"]["maya-dirmap"]["enabled"] - if mapping.get("source-path") and mapping_enabled is True: - log.info("Processing directory mapping ...") - cmds.dirmap(en=True) - for k, sp in enumerate(mapping["source-path"]): - try: - print("{} -> {}".format(sp, mapping["destination-path"][k])) - cmds.dirmap(m=(sp, mapping["destination-path"][k])) - cmds.dirmap(m=(mapping["destination-path"][k], sp)) - except IndexError: - # missing corresponding destination path - log.error(("invalid dirmap mapping, missing corresponding" - " destination directory.")) - break - except RuntimeError: - log.error("invalid path {} -> {}, mapping not registered".format( - sp, mapping["destination-path"][k] - )) - continue + # process path mapping + process_dirmap(project_settings) pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) @@ -77,6 +59,40 @@ def install(): avalon.data["familiesStateToggled"] = ["imagesequence"] +def process_dirmap(project_settings): + # type: (dict) -> None + """Go through all paths in Settings and set them using `dirmap`. + + Args: + project_settings (dict): Settings for current project. + + """ + if not project_settings["maya"].get("maya-dirmap"): + return + mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {} + mapping_enabled = project_settings["maya"]["maya-dirmap"]["enabled"] + if not mapping or not mapping_enabled: + return + if mapping.get("source-path") and mapping_enabled is True: + log.info("Processing directory mapping ...") + cmds.dirmap(en=True) + for k, sp in enumerate(mapping["source-path"]): + try: + print("{} -> {}".format(sp, mapping["destination-path"][k])) + cmds.dirmap(m=(sp, mapping["destination-path"][k])) + cmds.dirmap(m=(mapping["destination-path"][k], sp)) + except IndexError: + # missing corresponding destination path + log.error(("invalid dirmap mapping, missing corresponding" + " destination directory.")) + break + except RuntimeError: + log.error("invalid path {} -> {}, mapping not registered".format( + sp, mapping["destination-path"][k] + )) + continue + + def uninstall(): pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) From 066765427e622fb4ef7988a6bc0828e5cda4a5ce Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Aug 2021 16:54:06 +0200 Subject: [PATCH 04/71] =?UTF-8?q?add=20documentation=20=F0=9F=A7=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/docs/admin_hosts_maya.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 5e0aa15345..7a928483bb 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -94,4 +94,9 @@ You can add your custom tools menu into Maya by extending definitions in **Maya :::note Work in progress This is still work in progress. Menu definition will be handled more friendly with widgets and not raw json. -::: \ No newline at end of file +::: + +## Multiplatform path mapping +You can configure path mapping using Maya `dirmap` command. This will add bi-directional mapping between +list of paths specified in **Settings**. You can find it in **Settings -> Project Settings -> Maya -> Maya Directory Mapping** +![Dirmap settings](assets/maya-admin_dirmap_settings.png) From af6282953f7952c9c682364cee11e5122c193093 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Aug 2021 16:55:04 +0200 Subject: [PATCH 05/71] =?UTF-8?q?add=20image=20=F0=9F=96=BC=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/assets/maya-admin_dirmap_settings.png | Bin 0 -> 15234 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/assets/maya-admin_dirmap_settings.png diff --git a/website/docs/assets/maya-admin_dirmap_settings.png b/website/docs/assets/maya-admin_dirmap_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..9d5780dfc815a49832bc33ea70f444adf38b3c20 GIT binary patch literal 15234 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 H#(w_?-F&;- literal 0 HcmV?d00001 From 29b1334f911d515bf7f678dfcd9730acce7d0214 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Aug 2021 14:11:33 +0200 Subject: [PATCH 06/71] standalone: validator asset parents --- openpype/plugins/publish/validate_editorial_asset_name.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index f13e3b4f38..28344af98e 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -14,8 +14,10 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): label = "Validate Editorial Asset Name" def process(self, context): + project_entity = context.data["projectEntity"] asset_and_parents = self.get_parents(context) + self.log.debug("__ asset_and_parents: {}".format(asset_and_parents)) if not io.Session: io.install() @@ -25,7 +27,8 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): self.log.debug("__ db_assets: {}".format(db_assets)) asset_db_docs = { - str(e["name"]): e["data"]["parents"] for e in db_assets} + str(e["name"]): [project_entity["name"]] + e["data"]["parents"] + for e in db_assets} self.log.debug("__ project_entities: {}".format( pformat(asset_db_docs))) From 52f4a877cfcb227fb287f64a16d0578d88c6718b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Aug 2021 15:24:32 +0200 Subject: [PATCH 07/71] standalone: systematic fix rather then workaround patch addressing issue https://github.com/pypeclub/OpenPype/issues/1918 --- openpype/plugins/publish/validate_editorial_asset_name.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index 28344af98e..eebba61af3 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -14,7 +14,6 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): label = "Validate Editorial Asset Name" def process(self, context): - project_entity = context.data["projectEntity"] asset_and_parents = self.get_parents(context) self.log.debug("__ asset_and_parents: {}".format(asset_and_parents)) @@ -27,7 +26,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): self.log.debug("__ db_assets: {}".format(db_assets)) asset_db_docs = { - str(e["name"]): [project_entity["name"]] + e["data"]["parents"] + str(e["name"]): e["data"]["parents"] for e in db_assets} self.log.debug("__ project_entities: {}".format( @@ -110,6 +109,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): parents = instance.data["parents"] return_dict.update({ - asset: [p["entity_name"] for p in parents] + asset: [p["entity_name"] for p in parents + if p["entity_type"].lower() != "project"] }) return return_dict From 90b945215242202de38a780e8ec96230b9ef1ed7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 16:01:26 +0200 Subject: [PATCH 08/71] dont use spacer widget --- openpype/tools/settings/settings/dict_mutable_widget.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index df6525e86a..833b7ac4d2 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -5,8 +5,7 @@ from Qt import QtWidgets, QtCore from .base import BaseWidget from .widgets import ( ExpandingWidget, - IconButton, - SpacerWidget + IconButton ) from openpype.tools.settings import ( BTN_FIXED_SIZE, @@ -61,7 +60,6 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): def create_addible_ui(self): add_btn = create_add_btn(self) remove_btn = create_remove_btn(self) - spacer_widget = SpacerWidget(self) remove_btn.setEnabled(False) @@ -70,13 +68,12 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): layout.setSpacing(3) layout.addWidget(add_btn, 0) layout.addWidget(remove_btn, 0) - layout.addWidget(spacer_widget, 1) + layout.addStretch(1) add_btn.clicked.connect(self._on_add_clicked) self.add_btn = add_btn self.remove_btn = remove_btn - self.spacer_widget = spacer_widget def _on_focus_lose(self): if self.key_input.hasFocus() or self.key_label_input.hasFocus(): From b8be1371ba7f6800650b1129da0c544f131fa618 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 16:01:40 +0200 Subject: [PATCH 09/71] swap key and label in item label --- openpype/tools/settings/settings/dict_mutable_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 833b7ac4d2..f8436769f5 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -431,7 +431,7 @@ class ModifiableDictItem(QtWidgets.QWidget): key_value = self.key_input.text() key_label_value = self.key_label_input.text() if key_label_value: - label = "{} ({})".format(key_label_value, key_value) + label = "{} ({})".format(key_value, key_label_value) else: label = key_value self.wrapper_widget.label_widget.setText(label) From b53c452a9b76ad41004637df917c88b65c586b1c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 17:29:08 +0200 Subject: [PATCH 10/71] added confirmation button to modifiable dict item --- .../settings/settings/dict_mutable_widget.py | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index f8436769f5..74a7a9793b 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -1,6 +1,6 @@ from uuid import uuid4 -from Qt import QtWidgets, QtCore +from Qt import QtWidgets, QtCore, QtGui from .base import BaseWidget from .widgets import ( @@ -14,6 +14,63 @@ from openpype.tools.settings import ( from openpype.settings.constants import KEY_REGEX +class PaintHelper: + cached_icons = {} + + @classmethod + def _draw_image(cls, width, height, brush): + image = QtGui.QPixmap(width, height) + image.fill(QtCore.Qt.transparent) + + icon_path_stroker = QtGui.QPainterPathStroker() + icon_path_stroker.setCapStyle(QtCore.Qt.RoundCap) + icon_path_stroker.setJoinStyle(QtCore.Qt.RoundJoin) + icon_path_stroker.setWidth(height / 5) + + painter = QtGui.QPainter(image) + painter.setPen(QtCore.Qt.transparent) + painter.setBrush(brush) + rect = QtCore.QRect(0, 0, image.width(), image.height()) + fifteenth = rect.height() / 15 + # Left point + p1 = QtCore.QPoint( + rect.x() + (5 * fifteenth), + rect.y() + (9 * fifteenth) + ) + # Middle bottom point + p2 = QtCore.QPoint( + rect.center().x(), + rect.y() + (11 * fifteenth) + ) + # Top right point + p3 = QtCore.QPoint( + rect.x() + (10 * fifteenth), + rect.y() + (5 * fifteenth) + ) + + path = QtGui.QPainterPath(p1) + path.lineTo(p2) + path.lineTo(p3) + + stroked_path = icon_path_stroker.createStroke(path) + painter.drawPath(stroked_path) + + painter.end() + + return image + + @classmethod + def get_confirm_icon(cls, width, height): + key = "{}x{}-confirm_image".format(width, height) + icon = cls.cached_icons.get(key) + + if icon is None: + image = cls._draw_image(width, height, QtCore.Qt.white) + icon = QtGui.QIcon(image) + cls.cached_icons[key] = icon + return icon + + def create_add_btn(parent): add_btn = QtWidgets.QPushButton("+", parent) add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) @@ -30,6 +87,19 @@ def create_remove_btn(parent): return remove_btn +def create_confirm_btn(parent): + confirm_btn = QtWidgets.QPushButton(parent) + + icon = PaintHelper.get_confirm_icon( + BTN_FIXED_SIZE, BTN_FIXED_SIZE + ) + confirm_btn.setIcon(icon) + confirm_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + confirm_btn.setProperty("btn-type", "tool-item") + confirm_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) + return confirm_btn + + class ModifiableDictEmptyItem(QtWidgets.QWidget): def __init__(self, entity_widget, store_as_list, parent): super(ModifiableDictEmptyItem, self).__init__(parent) @@ -41,6 +111,8 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): self.is_duplicated = False self.key_is_valid = store_as_list + self.confirm_btn = None + if self.collapsible_key: self.create_collapsible_ui() else: @@ -108,7 +180,16 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): self.is_duplicated = self.entity_widget.is_key_duplicated(key) key_input_state = "" # Collapsible key and empty key are not invalid - if self.collapsible_key and self.key_input.text() == "": + key_value = self.key_input.text() + if self.confirm_btn is not None: + conf_disabled = ( + key_value == "" + or not self.key_is_valid + or self.is_duplicated + ) + self.confirm_btn.setEnabled(not conf_disabled) + + if self.collapsible_key and key_value == "": pass elif self.is_duplicated or not self.key_is_valid: key_input_state = "invalid" @@ -138,11 +219,15 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): key_input_label_widget = QtWidgets.QLabel("Key:", self) key_label_input_label_widget = QtWidgets.QLabel("Label:", self) + confirm_btn = create_confirm_btn(self) + confirm_btn.setEnabled(False) + wrapper_widget = ExpandingWidget("", self) wrapper_widget.add_widget_after_label(key_input_label_widget) wrapper_widget.add_widget_after_label(key_input) wrapper_widget.add_widget_after_label(key_label_input_label_widget) wrapper_widget.add_widget_after_label(key_label_input) + wrapper_widget.add_widget_after_label(confirm_btn) wrapper_widget.hide_toolbox() layout = QtWidgets.QVBoxLayout(self) @@ -154,9 +239,12 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): key_input.returnPressed.connect(self._on_enter_press) key_label_input.returnPressed.connect(self._on_enter_press) + confirm_btn.clicked.connect(self._on_enter_press) + self.key_input = key_input self.key_label_input = key_label_input self.wrapper_widget = wrapper_widget + self.confirm_btn = confirm_btn class ModifiableDictItem(QtWidgets.QWidget): @@ -187,6 +275,8 @@ class ModifiableDictItem(QtWidgets.QWidget): self.key_label_input = None + self.confirm_btn = None + if collapsible_key: self.create_collapsible_ui() else: @@ -274,6 +364,9 @@ class ModifiableDictItem(QtWidgets.QWidget): edit_btn.setProperty("btn-type", "tool-item-icon") edit_btn.setFixedHeight(BTN_FIXED_SIZE) + confirm_btn = create_confirm_btn(self) + confirm_btn.setVisible(False) + remove_btn = create_remove_btn(self) key_input_label_widget = QtWidgets.QLabel("Key:") @@ -283,6 +376,7 @@ class ModifiableDictItem(QtWidgets.QWidget): wrapper_widget.add_widget_after_label(key_input) wrapper_widget.add_widget_after_label(key_label_input_label_widget) wrapper_widget.add_widget_after_label(key_label_input) + wrapper_widget.add_widget_after_label(confirm_btn) wrapper_widget.add_widget_after_label(remove_btn) key_input.textChanged.connect(self._on_key_change) @@ -292,6 +386,7 @@ class ModifiableDictItem(QtWidgets.QWidget): key_label_input.returnPressed.connect(self._on_enter_press) edit_btn.clicked.connect(self.on_edit_pressed) + confirm_btn.clicked.connect(self._on_enter_press) remove_btn.clicked.connect(self.on_remove_clicked) # Hide edit inputs @@ -307,6 +402,7 @@ class ModifiableDictItem(QtWidgets.QWidget): self.key_label_input_label_widget = key_label_input_label_widget self.wrapper_widget = wrapper_widget self.edit_btn = edit_btn + self.confirm_btn = confirm_btn self.remove_btn = remove_btn self.content_widget = content_widget @@ -412,6 +508,14 @@ class ModifiableDictItem(QtWidgets.QWidget): self.temp_key, key, self ) self.temp_key = key + if self.confirm_btn is not None: + conf_disabled = ( + key == "" + or not self.key_is_valid + or is_key_duplicated + ) + self.confirm_btn.setEnabled(not conf_disabled) + if is_key_duplicated or not self.key_is_valid: return @@ -454,6 +558,7 @@ class ModifiableDictItem(QtWidgets.QWidget): self.key_input.setVisible(enabled) self.key_input_label_widget.setVisible(enabled) self.key_label_input.setVisible(enabled) + self.confirm_btn.setVisible(enabled) if not self.is_required: self.remove_btn.setVisible(enabled) if enabled: From 7df70b63d9435387ba37900aaa9c906eb24eef92 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 17:29:14 +0200 Subject: [PATCH 11/71] added key tooltip --- openpype/tools/settings/settings/dict_mutable_widget.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 74a7a9793b..4f2800156c 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -14,6 +14,12 @@ from openpype.tools.settings import ( from openpype.settings.constants import KEY_REGEX +KEY_INPUT_TOOLTIP = ( + "Keys can't be duplicated and may contain alphabetical character (a-Z)" + "\nnumerical characters (0-9) dash (\"-\") or underscore (\"_\")." +) + + class PaintHelper: cached_icons = {} @@ -202,6 +208,7 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): def create_collapsible_ui(self): key_input = QtWidgets.QLineEdit(self) key_input.setObjectName("DictKey") + key_input.setToolTip(KEY_INPUT_TOOLTIP) key_label_input = QtWidgets.QLineEdit(self) @@ -281,6 +288,8 @@ class ModifiableDictItem(QtWidgets.QWidget): self.create_collapsible_ui() else: self.create_addible_ui() + + self.key_input.setToolTip(KEY_INPUT_TOOLTIP) self.update_style() @property From e4334d0e7572feb1f185de95cab85eac878f1fd6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 17:52:34 +0200 Subject: [PATCH 12/71] enum may have defined default item in schema --- openpype/settings/entities/enum_entity.py | 36 ++++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 4f6a2886bc..361ad38dc5 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -73,21 +73,41 @@ class EnumEntity(BaseEnumEntity): def _item_initalization(self): self.multiselection = self.schema_data.get("multiselection", False) self.enum_items = self.schema_data.get("enum_items") + # Default is optional and non breaking attribute + enum_default = self.schema_data.get("default") - valid_keys = set() + all_keys = [] for item in self.enum_items or []: - valid_keys.add(tuple(item.keys())[0]) + key = tuple(item.keys())[0] + all_keys.append(key) - self.valid_keys = valid_keys + self.valid_keys = set(all_keys) if self.multiselection: self.valid_value_types = (list, ) - self.value_on_not_set = [] + value_on_not_set = [] + if enum_default: + if not isinstance(enum_default, list): + enum_default = [enum_default] + + for item in enum_default: + if item in all_keys: + value_on_not_set.append(item) + + self.value_on_not_set = value_on_not_set + else: - for key in valid_keys: - if self.value_on_not_set is NOT_SET: - self.value_on_not_set = key - break + if isinstance(enum_default, list) and enum_default: + enum_default = enum_default[0] + + if enum_default in self.valid_keys: + self.value_on_not_set = enum_default + + else: + for key in all_keys: + if self.value_on_not_set is NOT_SET: + self.value_on_not_set = key + break self.valid_value_types = (STRING_TYPE, ) From 39976e8bc4b47e778e87e4cef2b7f4d223bbc801 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 17:53:04 +0200 Subject: [PATCH 13/71] conditional dict may have defined default item for enum --- openpype/settings/entities/dict_conditional.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d275d8ac3d..b7c64f173f 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -141,6 +141,7 @@ class DictConditionalEntity(ItemEntity): self.enum_key = self.schema_data.get("enum_key") self.enum_label = self.schema_data.get("enum_label") self.enum_children = self.schema_data.get("enum_children") + self.enum_default = self.schema_data.get("enum_default") self.enum_entity = None @@ -277,15 +278,22 @@ class DictConditionalEntity(ItemEntity): if isinstance(item, dict) and "key" in item: valid_enum_items.append(item) + enum_keys = [] enum_items = [] for item in valid_enum_items: item_key = item["key"] + enum_keys.append(item_key) item_label = item.get("label") or item_key enum_items.append({item_key: item_label}) if not enum_items: return + if self.enum_default in enum_keys: + default_key = self.enum_default + else: + default_key = enum_keys[0] + # Create Enum child first enum_key = self.enum_key or "invalid" enum_schema = { @@ -293,7 +301,8 @@ class DictConditionalEntity(ItemEntity): "multiselection": False, "enum_items": enum_items, "key": enum_key, - "label": self.enum_label + "label": self.enum_label, + "default": default_key } enum_entity = self.create_schema_object(enum_schema, self) From c708388c2084fa11d32ae47db46512a0a4649a90 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 10 Aug 2021 17:54:04 +0200 Subject: [PATCH 14/71] add support and documentation from maya scene patching --- .../plugins/publish/submit_maya_deadline.py | 158 ++++++++++++------ .../defaults/project_settings/deadline.json | 3 +- .../schema_project_deadline.json | 27 ++- website/docs/admin_hosts_maya.md | 20 +++ ...maya-admin_submit_maya_job_to_deadline.png | Bin 0 -> 28550 bytes 5 files changed, 152 insertions(+), 56 deletions(-) create mode 100644 website/docs/assets/maya-admin_submit_maya_job_to_deadline.png diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index a652da7786..b607d472bd 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -199,7 +199,7 @@ def get_renderer_variables(renderlayer, root): if extension is None: extension = "png" - if extension == "exr (multichannel)" or extension == "exr (deep)": + if extension in ["exr (multichannel)", "exr (deep)"]: extension = "exr" prefix_attr = "vraySettings.fileNamePrefix" @@ -295,57 +295,70 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): instance.data["toBeRenderedOn"] = "deadline" filepath = None + patches = ( + context.data["project_settings"].get( + "deadline", {}).get( + "publish", {}).get( + "MayaSubmitDeadline", {}).get( + "scene_patches", {}) + ) # Handle render/export from published scene or not ------------------ if self.use_published: + patched_files = [] for i in context: - if "workfile" in i.data["families"]: - assert i.data["publish"] is True, ( - "Workfile (scene) must be published along") - template_data = i.data.get("anatomyData") - rep = i.data.get("representations")[0].get("name") - template_data["representation"] = rep - template_data["ext"] = rep - template_data["comment"] = None - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] - filepath = os.path.normpath(template_filled) - self.log.info("Using published scene for render {}".format( - filepath)) + if "workfile" not in i.data["families"]: + continue + assert i.data["publish"] is True, ( + "Workfile (scene) must be published along") + template_data = i.data.get("anatomyData") + rep = i.data.get("representations")[0].get("name") + template_data["representation"] = rep + template_data["ext"] = rep + template_data["comment"] = None + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled["publish"]["path"] + filepath = os.path.normpath(template_filled) + self.log.info("Using published scene for render {}".format( + filepath)) - if not os.path.exists(filepath): - self.log.error("published scene does not exist!") - raise - # now we need to switch scene in expected files - # because token will now point to published - # scene file and that might differ from current one - new_scene = os.path.splitext( - os.path.basename(filepath))[0] - orig_scene = os.path.splitext( - os.path.basename(context.data["currentFile"]))[0] - exp = instance.data.get("expectedFiles") + if not os.path.exists(filepath): + self.log.error("published scene does not exist!") + raise + # now we need to switch scene in expected files + # because token will now point to published + # scene file and that might differ from current one + new_scene = os.path.splitext( + os.path.basename(filepath))[0] + orig_scene = os.path.splitext( + os.path.basename(context.data["currentFile"]))[0] + exp = instance.data.get("expectedFiles") - if isinstance(exp[0], dict): - # we have aovs and we need to iterate over them - new_exp = {} - for aov, files in exp[0].items(): - replaced_files = [] - for f in files: - replaced_files.append( - f.replace(orig_scene, new_scene) - ) - new_exp[aov] = replaced_files - instance.data["expectedFiles"] = [new_exp] - else: - new_exp = [] - for f in exp: - new_exp.append( + if isinstance(exp[0], dict): + # we have aovs and we need to iterate over them + new_exp = {} + for aov, files in exp[0].items(): + replaced_files = [] + for f in files: + replaced_files.append( f.replace(orig_scene, new_scene) ) - instance.data["expectedFiles"] = [new_exp] - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) + new_exp[aov] = replaced_files + instance.data["expectedFiles"] = [new_exp] + else: + new_exp = [] + for f in exp: + new_exp.append( + f.replace(orig_scene, new_scene) + ) + instance.data["expectedFiles"] = [new_exp] + self.log.info("Scene name was switched {} -> {}".format( + orig_scene, new_scene + )) + # patch workfile is needed + if filepath not in patched_files: + patched_file = self._patch_workfile(filepath, patches) + patched_files.append(patched_file) all_instances = [] for result in context.data["results"]: @@ -868,10 +881,11 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): payload["JobInfo"].update(job_info_ext) payload["PluginInfo"].update(plugin_info_ext) - envs = [] - for k, v in payload["JobInfo"].items(): - if k.startswith("EnvironmentKeyValue"): - envs.append(v) + envs = [ + v + for k, v in payload["JobInfo"].items() + if k.startswith("EnvironmentKeyValue") + ] # add app name to environment envs.append( @@ -892,11 +906,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): envs.append( "OPENPYPE_ASS_EXPORT_STEP={}".format(1)) - i = 0 - for e in envs: + for i, e in enumerate(envs): payload["JobInfo"]["EnvironmentKeyValue{}".format(i)] = e - i += 1 - return payload def _get_vray_render_payload(self, data): @@ -1003,7 +1014,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): """ if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa + kwargs['verify'] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) # add 10sec timeout before bailing out kwargs['timeout'] = 10 return requests.post(*args, **kwargs) @@ -1022,7 +1033,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): """ if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa + kwargs['verify'] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) # add 10sec timeout before bailing out kwargs['timeout'] = 10 return requests.get(*args, **kwargs) @@ -1069,3 +1080,42 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): result = filename_zero.replace("\\", "/") return result + + def _patch_workfile(self, file, patches): + # type: (str, dict) -> Union[str, None] + """Patch Maya scene. + + This will take list of patches (lines to add) and apply them to + *published* Maya scene file (that is used later for rendering). + + Patches are dict with following structure:: + { + "name": "Name of patch", + "regex": "regex of line before patch", + "line": "line to insert" + } + + Args: + file (str): File to patch. + patches (dict): Dictionary defining patches. + + Returns: + str: Patched file path or None + + """ + if os.path.splitext(file)[1].lower() != ".ma" or not patches: + return None + + compiled_regex = [re.compile(p["regex"]) for p in patches] + with open(file, "r+") as pf: + scene_data = pf.readlines() + for ln, line in enumerate(scene_data): + for i, r in enumerate(compiled_regex): + if re.match(r, line): + scene_data.insert(ln + 1, patches[i]["line"]) + pf.seek(0) + pf.writelines(scene_data) + pf.truncate() + self.log.info( + "Applied {} patch to scene.".format(patches[i]["name"])) + return file diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 0f2da9f5b0..efeafbb1ac 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -45,7 +45,8 @@ "group": "none", "limit": [], "jobInfo": {}, - "pluginInfo": {} + "pluginInfo": {}, + "scene_patches": [] }, "NukeSubmitDeadline": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 8e6a4b10e4..53c6bf48c0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -151,7 +151,7 @@ "type": "dict", "collapsible": true, "key": "MayaSubmitDeadline", - "label": "Submit maya job to deadline", + "label": "Submit Maya job to Deadline", "checkbox_key": "enabled", "children": [ { @@ -213,6 +213,31 @@ "type": "raw-json", "key": "pluginInfo", "label": "Additional PluginInfo data" + }, + { + "type": "list", + "key": "scene_patches", + "label": "Scene patches", + "required_keys": ["name", "regex", "line"], + "object_type": { + "type": "dict", + "children": [ + { + "key": "name", + "label": "Patch name", + "type": "text" + }, { + "key": "regex", + "label": "Patch regex", + "type": "text" + }, { + "key": "line", + "label": "Patch line", + "type": "text" + } + ] + + } } ] }, diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 5e0aa15345..47447983b9 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -87,6 +87,26 @@ When you publish your model with top group named like `foo_GRP` it will fail. Bu All regexes used here are in Python variant. ::: +### Maya > Deadline submitter +This plugin provides connection between Maya and Deadline. It is using [Deadline Webservice](https://docs.thinkboxsoftware.com/products/deadline/10.0/1_User%20Manual/manual/web-service.html) to submit jobs to farm. +![Maya > Deadline Settings](assets/maya-admin_submit_maya_job_to_deadline.png) + +You can set various aspects of scene submission to farm with per-project settings in **Setting UI**. + + - **Optional** will mark sumission plugin optional + - **Active** will enable/disable plugin + - **Tile Assembler Plugin** will set what should be used to assemble tiles on Deadline. Either **Open Image IO** will be used +or Deadlines **Draft Tile Assembler**. + - **Use Published scene** enable to render from published scene instead of scene in work area. Rendering from published files is much safer. + - **Use Asset dependencies** will mark job pending on farm until asset dependencies are fulfilled - for example Deadline will wait for scene file to be synced to cloud, etc. + - **Group name** use specific Deadline group for the job. + - **Limit Groups** use these Deadline Limit groups for the job. + - **Additional `JobInfo` data** JSON of additional Deadline options that will be embedded in `JobInfo` part of the submission data. + - **Additional `PluginInfo` data** JSON of additional Deadline options that will be embedded in `PluginInfo` part of the submission data. + - **Scene patches** - configure mechanism to add additional lines to published Maya Ascii scene files before they are used for rendering. +This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation. +`Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending. + ## Custom Menu You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**. ![Custom menu definition](assets/maya-admin_scriptsmenu.png) diff --git a/website/docs/assets/maya-admin_submit_maya_job_to_deadline.png b/website/docs/assets/maya-admin_submit_maya_job_to_deadline.png new file mode 100644 index 0000000000000000000000000000000000000000..56b720dc5deea7e891ed8747edac7bdd1f2f041e GIT binary patch literal 28550 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 Date: Wed, 11 Aug 2021 10:33:15 +0200 Subject: [PATCH 15/71] fix removing of duplicated keys --- .../tools/settings/settings/dict_mutable_widget.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 4f2800156c..3526dc60b5 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -792,10 +792,6 @@ class DictMutableKeysWidget(BaseWidget): def remove_key(self, widget): key = self.entity.get_child_key(widget.entity) self.entity.pop(key) - # Poping of key from entity should remove the entity and input field. - # this is kept for testing purposes. - if widget in self.input_fields: - self.remove_row(widget) def change_key(self, new_key, widget): if not new_key or widget.is_key_duplicated: @@ -862,6 +858,11 @@ class DictMutableKeysWidget(BaseWidget): return input_field def remove_row(self, widget): + if widget.is_key_duplicated: + new_key = widget.uuid_key + if new_key is None: + new_key = str(uuid4()) + self.validate_key_duplication(widget.temp_key, new_key, widget) self.input_fields.remove(widget) self.content_layout.removeWidget(widget) widget.deleteLater() @@ -945,7 +946,10 @@ class DictMutableKeysWidget(BaseWidget): _input_field.set_entity_value() else: - if input_field.key_value() != key: + if ( + not input_field.is_key_duplicated + and input_field.key_value() != key + ): changed = True input_field.set_key(key) From 0503a3a9ff670d5cb721a4b8dee7d9c95df93a9e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 10:51:38 +0200 Subject: [PATCH 16/71] added default to readme --- openpype/settings/entities/schemas/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 42a8973f43..ff19a439d9 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -195,6 +195,7 @@ - all items in `enum_children` must have at least `key` key which represents value stored under `enum_key` - items can define `label` for UI purposes - most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`) +- to set default value for `enum_key` set it with `enum_default` - entity must have defined `"label"` if is not used as widget - is set as group if any parent is not group - if `"label"` is entetered there which will be shown in GUI @@ -359,6 +360,8 @@ How output of the schema could look like on save: - values are defined under value of key `"enum_items"` as list - each item in list is simple dictionary where value is label and key is value which will be stored - should be possible to enter single dictionary if order of items doesn't matter +- it is possible to set default selected value/s with `default` attribute + - it is recommended to use this option only in single selection mode ``` { From 3001f4c3ccdd980559ccf5ff1aea6bef7a1fd5cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 10:53:25 +0200 Subject: [PATCH 17/71] added one more comment line --- openpype/settings/entities/schemas/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index ff19a439d9..b437340f5b 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -362,6 +362,7 @@ How output of the schema could look like on save: - should be possible to enter single dictionary if order of items doesn't matter - it is possible to set default selected value/s with `default` attribute - it is recommended to use this option only in single selection mode + - at the end this option is used only when defying default settings value or in dynamic items ``` { From 2f28514c1818983342755962d164a5d905b54e6d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 10:56:03 +0200 Subject: [PATCH 18/71] fixed typo --- openpype/settings/entities/schemas/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index b437340f5b..2034d4e463 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -375,7 +375,7 @@ How output of the schema could look like on save: {"ftrackreview": "Add to Ftrack"}, {"delete": "Delete output"}, {"slate-frame": "Add slate frame"}, - {"no-hnadles": "Skip handle frames"} + {"no-handles": "Skip handle frames"} ] } ``` From 32630ca703a74881ed7856cef4c3afc3ac82f998 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 11 Aug 2021 12:03:00 +0200 Subject: [PATCH 19/71] Fix - validate takes repre["files"] as list all the time repre["files"] might be list or str, loop for string wasn't working --- .../plugins/publish/validate_expected_and_rendered_files.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index c71b5106ec..305c71b035 100644 --- a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -181,6 +181,10 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): """Returns set of file names from metadata.json""" expected_files = set() - for file_name in repre["files"]: + files = repre["files"] + if not isinstance(files, list): + files = [files] + + for file_name in files: expected_files.add(file_name) return expected_files From 112aac785d3e538b35a14d389b549ddb7ae22107 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 11 Aug 2021 12:47:51 +0200 Subject: [PATCH 20/71] =?UTF-8?q?fix=20hound=20=F0=9F=90=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index b607d472bd..6b52e4b387 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -1082,7 +1082,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): return result def _patch_workfile(self, file, patches): - # type: (str, dict) -> Union[str, None] + # type: (str, dict) -> [str, None] """Patch Maya scene. This will take list of patches (lines to add) and apply them to @@ -1117,5 +1117,6 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): pf.writelines(scene_data) pf.truncate() self.log.info( - "Applied {} patch to scene.".format(patches[i]["name"])) + "Applied {} patch to scene.".format( + patches[i]["name"])) return file From 62cea5a26b59b81b382885f1ffaf7c7309416bd4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 11 Aug 2021 14:39:18 +0200 Subject: [PATCH 21/71] check for missing python when using pyenv --- README.md | 13 +++++++++++-- tools/create_env.ps1 | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6b4495c9b6..209af24c75 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The main things you will need to run and build OpenPype are: - PowerShell 5.0+ (Windows) - Bash (Linux) - [**Python 3.7.8**](#python) or higher -- [**MongoDB**](#database) +- [**MongoDB**](#database) (needed only for local development) It can be built and ran on all common platforms. We develop and test on the following: @@ -126,6 +126,16 @@ pyenv local 3.7.9 ### Linux +#### Docker +Easiest way to build OpenPype on Linux is using [Docker](https://www.docker.com/). Just run: + +```sh +sudo ./tools/docker_build.sh +``` + +If all is successful, you'll find built OpenPype in `./build/` folder. + +#### Manual build You will need [Python 3.7](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll also need [curl](https://curl.se) on systems that doesn't have one preinstalled. To build Python related stuff, you need Python header files installed (`python3-dev` on Ubuntu for example). @@ -133,7 +143,6 @@ To build Python related stuff, you need Python header files installed (`python3- You'll need also other tools to build some OpenPype dependencies like [CMake](https://cmake.org/). Python 3 should be part of all modern distributions. You can use your package manager to install **git** and **cmake**. -
Details for Ubuntu Install git, cmake and curl diff --git a/tools/create_env.ps1 b/tools/create_env.ps1 index e2ec401bb3..f19a98f11b 100644 --- a/tools/create_env.ps1 +++ b/tools/create_env.ps1 @@ -50,8 +50,18 @@ function Install-Poetry() { Write-Host "Installing Poetry ... " $python = "python" if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { + if (-not (Test-Path -PathType Leaf -Path "$($openpype_root)\.python-version")) { + $result = & pyenv global + if ($result -eq "no global version configured") { + Write-Host "!!! " -NoNewline -ForegroundColor Red + Write-Host "Using pyenv but having no local or global version of Python set." + Exit-WithCode 1 + } + } $python = & pyenv which python + } + $env:POETRY_HOME="$openpype_root\.poetry" (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | & $($python) - } From 2199d8bed52d3ca6e298c20b60e1cc1dc6ba5bf3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Aug 2021 14:53:54 +0200 Subject: [PATCH 22/71] Nuke: submit to farm failed due `ftrack` family remove --- openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py | 1 - openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py index 5611591b56..b0d3ec6241 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py @@ -51,7 +51,6 @@ class ExtractReviewDataLut(openpype.api.Extractor): if "render.farm" in families: instance.data["families"].remove("review") - instance.data["families"].remove("ftrack") self.log.debug( "_ lutPath: {}".format(instance.data["lutPath"])) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 5032e602a2..cea7d86c26 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -45,7 +45,6 @@ class ExtractReviewDataMov(openpype.api.Extractor): if "render.farm" in families: instance.data["families"].remove("review") - instance.data["families"].remove("ftrack") data = exporter.generate_mov(farm=True) self.log.debug( From 9a4fba11c430da6370c24e8c84ced78dca1bded2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 16:12:48 +0200 Subject: [PATCH 23/71] replaced svg logos with newer version without "text" nodes --- website/static/img/logos/pypeclub_black.svg | 20 +++++++++++--- .../static/img/logos/pypeclub_color_white.svg | 26 ++++++++++++++----- website/static/img/logos/pypeclub_white.svg | 20 +++++++++++--- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/website/static/img/logos/pypeclub_black.svg b/website/static/img/logos/pypeclub_black.svg index b749edbdb3..6c209977fe 100644 --- a/website/static/img/logos/pypeclub_black.svg +++ b/website/static/img/logos/pypeclub_black.svg @@ -1,8 +1,8 @@ - - + + @@ -20,7 +20,21 @@ - .club + + + + + + + + + + + + + + + diff --git a/website/static/img/logos/pypeclub_color_white.svg b/website/static/img/logos/pypeclub_color_white.svg index c82946d82b..ffa194aa47 100644 --- a/website/static/img/logos/pypeclub_color_white.svg +++ b/website/static/img/logos/pypeclub_color_white.svg @@ -1,26 +1,40 @@ - - + + - + - + - + - .club + + + + + + + + + + + + + + + diff --git a/website/static/img/logos/pypeclub_white.svg b/website/static/img/logos/pypeclub_white.svg index b634c210b1..3bf4159f9c 100644 --- a/website/static/img/logos/pypeclub_white.svg +++ b/website/static/img/logos/pypeclub_white.svg @@ -1,8 +1,8 @@ - - + + @@ -20,7 +20,21 @@ - .club + + + + + + + + + + + + + + + From 1e5bf01ea8ea0eb090263157e86accc4c4a7d984 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 16:13:06 +0200 Subject: [PATCH 24/71] saved new defaults for unreal --- openpype/settings/defaults/project_settings/unreal.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 46b9ca2a18..dad61cd1f0 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,6 +1,5 @@ { "project_setup": { - "dev_mode": true, - "install_unreal_python_engine": false + "dev_mode": true } } \ No newline at end of file From 6dbd3b0df3cfc3c47344ecf3325b54ffb3740020 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 17:42:16 +0200 Subject: [PATCH 25/71] initial version of breadcrumbs widget --- .../settings/settings/breadcrumb_widget.py | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 openpype/tools/settings/settings/breadcrumb_widget.py diff --git a/openpype/tools/settings/settings/breadcrumb_widget.py b/openpype/tools/settings/settings/breadcrumb_widget.py new file mode 100644 index 0000000000..306ac14218 --- /dev/null +++ b/openpype/tools/settings/settings/breadcrumb_widget.py @@ -0,0 +1,387 @@ +import os +import sys +sys.path.append(r"C:\Users\jakub.trllo\Desktop\pype\pype3_2\.venv\Lib\site-packages") + +from Qt import QtWidgets, QtGui, QtCore + +# px, size of generated semi-transparent icons +TRANSP_ICON_SIZE = 40, 40 + +PREFIX_ROLE = QtCore.Qt.UserRole + 1 +LAST_SEGMENT_ROLE = QtCore.Qt.UserRole + 2 + + +class BreadcrumbItem(QtGui.QStandardItem): + def __init__(self, *args, **kwargs): + self._display_value = None + self._edit_value = None + super(BreadcrumbItem, self).__init__(*args, **kwargs) + + def data(self, role=None): + if role == QtCore.Qt.DisplayRole: + return self._display_value + + if role == QtCore.Qt.EditRole: + return self._edit_value + + if role is None: + args = tuple() + else: + args = (role, ) + return super(BreadcrumbItem, self).data(*args) + + def setData(self, value, role): + if role == QtCore.Qt.DisplayRole: + self._display_value = value + return True + + if role == QtCore.Qt.EditRole: + self._edit_value = value + return True + + if role is None: + args = (value, ) + else: + args = (value, role) + return super(BreadcrumbItem, self).setData(*args) + + +class BreadcrumbsModel(QtGui.QStandardItemModel): + def __init__(self): + super(BreadcrumbsModel, self).__init__() + self.current_path = "" + + self.reset() + + def reset(self): + root_item = self.invisibleRootItem() + rows = root_item.rowCount() + if rows > 0: + root_item.removeRows(0, rows) + + paths = [ + "project_settings", + "project_settings/blabla", + "project_settings/blabla2", + "project_settings/blabla2/dada" + ] + items = [] + for path in paths: + if not path: + continue + path_items = path.split("/") + value = path + label = path_items.pop(-1) + prefix = "/".join(path_items) + if prefix: + prefix += "/" + + item = QtGui.QStandardItem(value) + item.setData(label, LAST_SEGMENT_ROLE) + item.setData(prefix, PREFIX_ROLE) + + items.append(item) + + root_item.appendRows(items) + + +class BreadcrumbsProxy(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(BreadcrumbsProxy, self).__init__(*args, **kwargs) + + self._current_path = "" + + def set_path_prefix(self, prefix): + path = prefix + if not prefix.endswith("/"): + path_items = path.split("/") + if len(path_items) == 1: + path = "" + else: + path_items.pop(-1) + path = "/".join(path_items) + "/" + + if path == self._current_path: + return + + self._current_path = prefix + + self.invalidateFilter() + + def filterAcceptsRow(self, row, parent): + index = self.sourceModel().index(row, 0, parent) + prefix_path = index.data(PREFIX_ROLE) + return prefix_path == self._current_path + + +class BreadcrumbsHintMenu(QtWidgets.QMenu): + def __init__(self, model, path_prefix, parent): + super(BreadcrumbsHintMenu, self).__init__(parent) + + self._path_prefix = path_prefix + self._model = model + + def showEvent(self, event): + self.clear() + + self._model.set_path_prefix(self._path_prefix) + + row_count = self._model.rowCount() + if row_count == 0: + action = self.addAction("* Nothing") + action.setData(".") + else: + for row in range(self._model.rowCount()): + index = self._model.index(row, 0) + label = index.data(LAST_SEGMENT_ROLE) + value = index.data(QtCore.Qt.EditRole) + action = self.addAction(label) + action.setData(value) + + super(BreadcrumbsHintMenu, self).showEvent(event) + + +class ClickableWidget(QtWidgets.QWidget): + clicked = QtCore.Signal() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit() + super(ClickableWidget, self).mouseReleaseEvent(event) + + +class BreadcrumbsPathInput(QtWidgets.QLineEdit): + cancelled = QtCore.Signal() + confirmed = QtCore.Signal() + + def __init__(self, model, parent): + super(BreadcrumbsPathInput, self).__init__(parent) + + self.setFrame(False) + + completer = QtWidgets.QCompleter(self) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + completer.setModel(model) + + popup = completer.popup() + popup.setUniformItemSizes(True) + popup.setLayoutMode(QtWidgets.QListView.Batched) + + self.setCompleter(completer) + + completer.activated.connect(self._on_completer_activated) + self.textEdited.connect(self._on_text_change) + + self._completer = completer + self._model = model + + self._context_menu_visible = False + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Escape: + self.cancelled.emit() + elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): + self.confirmed.emit() + else: + super(BreadcrumbsPathInput, self).keyPressEvent(event) + + def focusOutEvent(self, event): + if not self._context_menu_visible: + self.cancelled.emit() + + self._context_menu_visible = False + super(BreadcrumbsPathInput, self).focusOutEvent(event) + + def contextMenuEvent(self, event): + self._context_menu_visible = True + super(BreadcrumbsPathInput, self).contextMenuEvent(event) + + def _on_completer_activated(self, path): + self.confirmed.emit(path) + + def _on_text_change(self, path): + self._model.set_path_prefix(path) + + +class BreadcrumbsButton(QtWidgets.QToolButton): + path_selected = QtCore.Signal(str) + + def __init__(self, path, model, parent): + super(BreadcrumbsButton, self).__init__(parent) + + path_prefix = path + "/" + + self.setAutoRaise(True) + self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) + + self.setMouseTracking(True) + + self.setText(path.split("/")[-1]) + + menu = BreadcrumbsHintMenu(model, path_prefix, self) + + self.setMenu(menu) + + # fixed size breadcrumbs + self.setMinimumSize(self.minimumSizeHint()) + size_policy = self.sizePolicy() + size_policy.setVerticalPolicy(size_policy.Minimum) + self.setSizePolicy(size_policy) + + menu.triggered.connect(self._on_menu_click) + self.clicked.connect(self._on_click) + + self._path = path + self._path_prefix = path_prefix + self._model = model + self._menu = menu + + def _on_click(self): + self.path_selected.emit(self._path) + + def _on_menu_click(self, action): + item = action.data() + self.path_selected.emit(item) + + +class BreadcrumbsAddressBar(QtWidgets.QFrame): + "Windows Explorer-like address bar" + path_selected = QtCore.Signal(str) + + def __init__(self, parent=None): + super(BreadcrumbsAddressBar, self).__init__(parent) + + model = BreadcrumbsModel() + proxy_model = BreadcrumbsProxy() + proxy_model.setSourceModel(model) + + self.setAutoFillBackground(True) + self.setFrameShape(self.StyledPanel) + + # Edit presented path textually + path_input = BreadcrumbsPathInput(proxy_model, self) + path_input.setVisible(False) + + path_input.cancelled.connect(self._on_input_cancel) + path_input.confirmed.connect(self._on_input_confirm) + + # Container for `crumbs_panel` + crumbs_container = QtWidgets.QWidget(self) + + # Container for breadcrumbs + crumbs_panel = QtWidgets.QWidget(crumbs_container) + + crumbs_layout = QtWidgets.QHBoxLayout() + crumbs_layout.setContentsMargins(0, 0, 0, 0) + crumbs_layout.setSpacing(0) + + crumbs_cont_layout = QtWidgets.QHBoxLayout(crumbs_container) + crumbs_cont_layout.setContentsMargins(0, 0, 0, 0) + crumbs_cont_layout.setSpacing(0) + crumbs_cont_layout.addWidget(crumbs_panel) + + # Clicking on empty space to the right puts the bar into edit mode + switch_space = ClickableWidget(self) + + crumb_panel_layout = QtWidgets.QHBoxLayout(crumbs_panel) + crumb_panel_layout.setContentsMargins(0, 0, 0, 0) + crumb_panel_layout.setSpacing(0) + crumb_panel_layout.addLayout(crumbs_layout, 0) + crumb_panel_layout.addWidget(switch_space, 1) + + switch_space.clicked.connect(self.switch_space_mouse_up) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(path_input) + layout.addWidget(crumbs_container) + + self.setMaximumHeight(path_input.height()) + + self.crumbs_layout = crumbs_layout + self.crumbs_panel = crumbs_panel + self.switch_space = switch_space + self.path_input = path_input + self.crumbs_container = crumbs_container + + self.model = model + self.proxy_model = proxy_model + + self._current_path = None + + self.set_path("project_settings") + + def _on_input_confirm(self): + self.set_path(self.path_input.text()) + self._show_address_field(False) + + def _on_input_cancel(self): + self._cancel_edit() + + def _clear_crumbs(self): + while self.crumbs_layout.count(): + widget = self.crumbs_layout.takeAt(0).widget() + if widget: + widget.deleteLater() + + def _insert_crumb(self, path): + btn = BreadcrumbsButton(path, self.proxy_model, self.crumbs_panel) + + self.crumbs_layout.insertWidget(0, btn) + + btn.path_selected.connect(self._on_crumb_clicked) + + def _on_crumb_clicked(self, path): + "Breadcrumb was clicked" + self.set_path(path) + + def set_path(self, path): + if path is None or path == ".": + path = self._current_path + + # exit edit mode + self._cancel_edit() + + self._clear_crumbs() + self._current_path = path + self.path_input.setText(path) + path_items = [ + item + for item in path.split("/") + if item + ] + while path_items: + item = "/".join(path_items) + self._insert_crumb(item) + path_items.pop(-1) + + self.path_selected.emit(self._current_path) + + def _cancel_edit(self): + "Set edit line text back to current path and switch to view mode" + # revert path + self.path_input.setText(self.path()) + # switch back to breadcrumbs view + self._show_address_field(False) + + def path(self): + "Get path displayed in this BreadcrumbsAddressBar" + return self._current_path + + def switch_space_mouse_up(self): + "EVENT: switch_space mouse clicked" + self._show_address_field(True) + + def _show_address_field(self, show=True): + "Show text address field" + self.crumbs_container.setVisible(not show) + self.path_input.setVisible(show) + if show: + self.path_input.setFocus() + self.path_input.selectAll() + + def minimumSizeHint(self): + result = super(BreadcrumbsAddressBar, self).minimumSizeHint() + result.setHeight(self.path_input.height()) + return result From 30e131685ece0a01622048a786502b0812c3c338 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 17:42:31 +0200 Subject: [PATCH 26/71] use breadcrumbs widget in category --- openpype/tools/settings/settings/categories.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 8be3eddfa8..fa00c8a814 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -31,6 +31,7 @@ from openpype.settings.entities import ( from openpype.settings import SaveWarningExc from .widgets import ProjectListWidget +from .breadcrumb_widget import BreadcrumbsAddressBar from .base import GUIWidget from .list_item_widget import ListWidget @@ -175,6 +176,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): scroll_widget = QtWidgets.QScrollArea(self) scroll_widget.setObjectName("GroupWidget") content_widget = QtWidgets.QWidget(scroll_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(3, 3, 3, 3) content_layout.setSpacing(5) @@ -197,6 +199,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): if self.user_role == "developer": self._add_developer_ui(footer_layout) + breadcrumbs_widget = BreadcrumbsAddressBar(content_widget) + save_btn = QtWidgets.QPushButton("Save", footer_widget) require_restart_label = QtWidgets.QLabel(footer_widget) require_restart_label.setAlignment(QtCore.Qt.AlignCenter) @@ -207,6 +211,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): configurations_layout.setContentsMargins(0, 0, 0, 0) configurations_layout.setSpacing(0) + configurations_layout.addWidget(breadcrumbs_widget, 0) configurations_layout.addWidget(scroll_widget, 1) configurations_layout.addWidget(footer_widget, 0) From f236acdc08a69d81b678a3e8c2f3d630021486bb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 17:44:50 +0200 Subject: [PATCH 27/71] fixed height issues --- openpype/tools/settings/settings/breadcrumb_widget.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/breadcrumb_widget.py b/openpype/tools/settings/settings/breadcrumb_widget.py index 306ac14218..05f7b983c2 100644 --- a/openpype/tools/settings/settings/breadcrumb_widget.py +++ b/openpype/tools/settings/settings/breadcrumb_widget.py @@ -383,5 +383,8 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): def minimumSizeHint(self): result = super(BreadcrumbsAddressBar, self).minimumSizeHint() - result.setHeight(self.path_input.height()) + result.setHeight(max( + self.path_input.height(), + self.crumbs_container.height() + )) return result From 048da648e967c26ad7d44223c706c8fe6395016a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 11:34:24 +0200 Subject: [PATCH 28/71] diverse between path changed and path edited --- .../tools/settings/settings/breadcrumb_widget.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumb_widget.py b/openpype/tools/settings/settings/breadcrumb_widget.py index 05f7b983c2..9584186419 100644 --- a/openpype/tools/settings/settings/breadcrumb_widget.py +++ b/openpype/tools/settings/settings/breadcrumb_widget.py @@ -246,7 +246,8 @@ class BreadcrumbsButton(QtWidgets.QToolButton): class BreadcrumbsAddressBar(QtWidgets.QFrame): "Windows Explorer-like address bar" - path_selected = QtCore.Signal(str) + path_changed = QtCore.Signal(str) + path_edited = QtCore.Signal(str) def __init__(self, parent=None): super(BreadcrumbsAddressBar, self).__init__(parent) @@ -310,10 +311,9 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): self._current_path = None - self.set_path("project_settings") def _on_input_confirm(self): - self.set_path(self.path_input.text()) + self.change_path(self.path_input.text()) self._show_address_field(False) def _on_input_cancel(self): @@ -334,7 +334,11 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): def _on_crumb_clicked(self, path): "Breadcrumb was clicked" + self.change_path(path) + + def change_path(self, path): self.set_path(path) + self.path_edited.emit(path) def set_path(self, path): if path is None or path == ".": @@ -356,7 +360,7 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): self._insert_crumb(item) path_items.pop(-1) - self.path_selected.emit(self._current_path) + self.path_changed.emit(self._current_path) def _cancel_edit(self): "Set edit line text back to current path and switch to view mode" From a4ccb3eb6deec734e84806eefb5daf3611327412 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 11:34:40 +0200 Subject: [PATCH 29/71] model is not set by default --- openpype/tools/settings/settings/breadcrumb_widget.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumb_widget.py b/openpype/tools/settings/settings/breadcrumb_widget.py index 9584186419..76f08e5f8e 100644 --- a/openpype/tools/settings/settings/breadcrumb_widget.py +++ b/openpype/tools/settings/settings/breadcrumb_widget.py @@ -252,9 +252,7 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): def __init__(self, parent=None): super(BreadcrumbsAddressBar, self).__init__(parent) - model = BreadcrumbsModel() proxy_model = BreadcrumbsProxy() - proxy_model.setSourceModel(model) self.setAutoFillBackground(True) self.setFrameShape(self.StyledPanel) @@ -306,11 +304,14 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): self.path_input = path_input self.crumbs_container = crumbs_container - self.model = model - self.proxy_model = proxy_model + self._model = None + self._proxy_model = proxy_model self._current_path = None + def set_model(self, model): + self._model = model + self._proxy_model.setSourceModel(model) def _on_input_confirm(self): self.change_path(self.path_input.text()) @@ -326,7 +327,7 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): widget.deleteLater() def _insert_crumb(self, path): - btn = BreadcrumbsButton(path, self.proxy_model, self.crumbs_panel) + btn = BreadcrumbsButton(path, self._proxy_model, self.crumbs_panel) self.crumbs_layout.insertWidget(0, btn) From ca1c04f14210439022d9cb52dfc7a3f026c3fc85 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 11:34:54 +0200 Subject: [PATCH 30/71] added base concept of settings models --- .../settings/settings/breadcrumb_widget.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumb_widget.py b/openpype/tools/settings/settings/breadcrumb_widget.py index 76f08e5f8e..d63c8bb980 100644 --- a/openpype/tools/settings/settings/breadcrumb_widget.py +++ b/openpype/tools/settings/settings/breadcrumb_widget.py @@ -53,18 +53,32 @@ class BreadcrumbsModel(QtGui.QStandardItemModel): self.reset() + def reset(self): + return + + +class SettingsBreadcrumbs(BreadcrumbsModel): + def __init__(self): + self.entity = None + + super(SettingsBreadcrumbs, self).__init__() + + def set_entity(self, entity): + self.entity = entity + self.reset() + + +class SystemSettingsBreadcrumbs(SettingsBreadcrumbs): def reset(self): root_item = self.invisibleRootItem() rows = root_item.rowCount() if rows > 0: root_item.removeRows(0, rows) - paths = [ - "project_settings", - "project_settings/blabla", - "project_settings/blabla2", - "project_settings/blabla2/dada" - ] + if self.entity is None: + return + + paths = [] items = [] for path in paths: if not path: @@ -85,6 +99,10 @@ class BreadcrumbsModel(QtGui.QStandardItemModel): root_item.appendRows(items) +class ProjectSettingsBreadcrumbs(SettingsBreadcrumbs): + pass + + class BreadcrumbsProxy(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): super(BreadcrumbsProxy, self).__init__(*args, **kwargs) From e04cedb2d15aeb0fe5f33fbc3e30b3a2d66dd29f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 11:35:44 +0200 Subject: [PATCH 31/71] added base logic of breadcrumbs in category widget --- .../tools/settings/settings/categories.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index fa00c8a814..55528a3d0d 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -31,7 +31,11 @@ from openpype.settings.entities import ( from openpype.settings import SaveWarningExc from .widgets import ProjectListWidget -from .breadcrumb_widget import BreadcrumbsAddressBar +from .breadcrumb_widget import ( + BreadcrumbsAddressBar, + SystemSettingsBreadcrumbs, + ProjectSettingsBreadcrumbs +) from .base import GUIWidget from .list_item_widget import ListWidget @@ -222,6 +226,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): save_btn.clicked.connect(self._save) refresh_btn.clicked.connect(self._on_refresh) + breadcrumbs_widget.path_edited.connect(self._on_path_edit) self.save_btn = save_btn self.refresh_btn = refresh_btn @@ -230,6 +235,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.content_layout = content_layout self.content_widget = content_widget self.configurations_widget = configurations_widget + self.breadcrumbs_widget = breadcrumbs_widget + self.breadcrumbs_model = None self.main_layout = main_layout self.ui_tweaks() @@ -237,6 +244,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): def ui_tweaks(self): return + def _on_path_edit(self, path): + print("Path edited:", path) + def _add_developer_ui(self, footer_layout): modify_defaults_widget = QtWidgets.QWidget() modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget) @@ -432,10 +442,16 @@ class SettingsCategoryWidget(QtWidgets.QWidget): def _on_reset_crash(self): self.save_btn.setEnabled(False) + if self.breadcrumbs_model is not None: + self.breadcrumbs_model.set_entity(None) + def _on_reset_success(self): if not self.save_btn.isEnabled(): self.save_btn.setEnabled(True) + if self.breadcrumbs_model is not None: + self.breadcrumbs_model.set_entity(self.entity) + def add_children_gui(self): for child_obj in self.entity.children: item = self.create_ui_for_entity(self, child_obj, self) @@ -526,6 +542,10 @@ class SystemWidget(SettingsCategoryWidget): self.modify_defaults_checkbox.setChecked(True) self.modify_defaults_checkbox.setEnabled(False) + def ui_tweaks(self): + self.breadcrumbs_model = SystemSettingsBreadcrumbs() + self.breadcrumbs_widget.set_model(self.breadcrumbs_model) + def _on_modify_defaults(self): if self.modify_defaults_checkbox.isChecked(): if not self.entity.is_in_defaults_state(): @@ -540,6 +560,9 @@ class ProjectWidget(SettingsCategoryWidget): self.project_name = None def ui_tweaks(self): + self.breadcrumbs_model = ProjectSettingsBreadcrumbs() + self.breadcrumbs_widget.set_model(self.breadcrumbs_model) + project_list_widget = ProjectListWidget(self) self.main_layout.insertWidget(0, project_list_widget, 0) From 2ba972a4824944132a561127d264bb305b1a4e8b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 12:32:26 +0200 Subject: [PATCH 32/71] fix signal emit --- openpype/tools/settings/settings/breadcrumb_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/breadcrumb_widget.py b/openpype/tools/settings/settings/breadcrumb_widget.py index d63c8bb980..2de23163d3 100644 --- a/openpype/tools/settings/settings/breadcrumb_widget.py +++ b/openpype/tools/settings/settings/breadcrumb_widget.py @@ -215,7 +215,7 @@ class BreadcrumbsPathInput(QtWidgets.QLineEdit): super(BreadcrumbsPathInput, self).contextMenuEvent(event) def _on_completer_activated(self, path): - self.confirmed.emit(path) + self.confirmed.emit() def _on_text_change(self, path): self._model.set_path_prefix(path) From 91eaa205ce643ce02e5ba84578fc513426485f5c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 12:33:15 +0200 Subject: [PATCH 33/71] added ability to collect static entities by path --- openpype/settings/entities/base_entity.py | 8 ++++++++ openpype/settings/entities/dict_conditional.py | 5 +++++ .../entities/dict_immutable_keys_entity.py | 12 ++++++++++++ openpype/settings/entities/input_entities.py | 5 +++++ openpype/settings/entities/item_entities.py | 15 +++++++++++++++ openpype/settings/entities/root_entities.py | 8 ++++++++ 6 files changed, 53 insertions(+) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index b4ebe885f5..851684520b 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -174,6 +174,14 @@ class BaseItemEntity(BaseEntity): roles = [roles] self.roles = roles + @abstractmethod + def collect_static_entities_by_path(self): + """Collect all paths of all static path entities. + + Static path is entity which is not dynamic or under dynamic entity. + """ + pass + @property def require_restart_on_change(self): return self._require_restart_on_change diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d275d8ac3d..b898588322 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -318,6 +318,11 @@ class DictConditionalEntity(ItemEntity): self.non_gui_children[item_key][child_obj.key] = child_obj + def collect_static_entities_by_path(self): + if self.is_dynamic_item or self.is_in_dynamic_item: + return {} + return {self.path: self} + def get_child_path(self, child_obj): """Get hierarchical path of child entity. diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index bde5304787..73b08f101a 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -203,6 +203,18 @@ class DictImmutableKeysEntity(ItemEntity): ) self.show_borders = self.schema_data.get("show_borders", True) + def collect_static_entities_by_path(self): + output = {} + if self.is_dynamic_item or self.is_in_dynamic_item: + return output + + output[self.path] = self + for children in self.non_gui_children.values(): + result = children.collect_static_entities_by_path() + if result: + output.update(result) + return output + def get_child_path(self, child_obj): """Get hierarchical path of child entity. diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 6952529963..336d1f5c1e 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -53,6 +53,11 @@ class EndpointEntity(ItemEntity): def _settings_value(self): pass + def collect_static_entities_by_path(self): + if self.is_dynamic_item or self.is_in_dynamic_item: + return {} + return {self.path: self} + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index 7e84f8c801..ecce7a22c0 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -106,6 +106,9 @@ class PathEntity(ItemEntity): self.valid_value_types = valid_value_types self.child_obj = self.create_schema_object(item_schema, self) + def collect_static_entities_by_path(self): + return self.child_obj.collect_static_entities_by_path() + def get_child_path(self, _child_obj): return self.path @@ -222,6 +225,18 @@ class ListStrictEntity(ItemEntity): super(ListStrictEntity, self).schema_validations() + def collect_static_entities_by_path(self): + output = {} + if self.is_dynamic_item or self.is_in_dynamic_item: + return output + + output[self.path] = self + for child_obj in self.children: + result = child_obj.collect_static_entities_by_path() + if result: + output.update(result) + return output + def get_child_path(self, child_obj): result_idx = None for idx, _child_obj in enumerate(self.children): diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 00677480e8..4a06d2d591 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -242,6 +242,14 @@ class RootEntity(BaseItemEntity): """Whan any children has changed.""" self.on_change() + def collect_static_entities_by_path(self): + output = {} + for child_obj in self.non_gui_children.values(): + result = child_obj.collect_static_entities_by_path() + if result: + output.update(result) + return output + def get_child_path(self, child_entity): """Return path of children entity""" for key, _child_entity in self.non_gui_children.items(): From f174aff062d4d598c2a448765b9af75921f2657b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 12:33:33 +0200 Subject: [PATCH 34/71] implemented base of model --- .../settings/settings/breadcrumb_widget.py | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumb_widget.py b/openpype/tools/settings/settings/breadcrumb_widget.py index 2de23163d3..d8a35daa5a 100644 --- a/openpype/tools/settings/settings/breadcrumb_widget.py +++ b/openpype/tools/settings/settings/breadcrumb_widget.py @@ -61,9 +61,14 @@ class SettingsBreadcrumbs(BreadcrumbsModel): def __init__(self): self.entity = None + self.entities_by_path = {} + self.dynamic_paths = set() + super(SettingsBreadcrumbs, self).__init__() def set_entity(self, entity): + self.entities_by_path = {} + self.dynamic_paths = set() self.entity = entity self.reset() @@ -78,9 +83,10 @@ class SystemSettingsBreadcrumbs(SettingsBreadcrumbs): if self.entity is None: return - paths = [] + entities_by_path = self.entity.collect_static_entities_by_path() + self.entities_by_path = entities_by_path items = [] - for path in paths: + for path in entities_by_path.keys(): if not path: continue path_items = path.split("/") @@ -100,7 +106,35 @@ class SystemSettingsBreadcrumbs(SettingsBreadcrumbs): class ProjectSettingsBreadcrumbs(SettingsBreadcrumbs): - pass + def reset(self): + root_item = self.invisibleRootItem() + rows = root_item.rowCount() + if rows > 0: + root_item.removeRows(0, rows) + + if self.entity is None: + return + + entities_by_path = self.entity.collect_static_entities_by_path() + self.entities_by_path = entities_by_path + items = [] + for path in entities_by_path.keys(): + if not path: + continue + path_items = path.split("/") + value = path + label = path_items.pop(-1) + prefix = "/".join(path_items) + if prefix: + prefix += "/" + + item = QtGui.QStandardItem(value) + item.setData(label, LAST_SEGMENT_ROLE) + item.setData(prefix, PREFIX_ROLE) + + items.append(item) + + root_item.appendRows(items) class BreadcrumbsProxy(QtCore.QSortFilterProxyModel): @@ -270,12 +304,11 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): def __init__(self, parent=None): super(BreadcrumbsAddressBar, self).__init__(parent) - proxy_model = BreadcrumbsProxy() - self.setAutoFillBackground(True) self.setFrameShape(self.StyledPanel) # Edit presented path textually + proxy_model = BreadcrumbsProxy() path_input = BreadcrumbsPathInput(proxy_model, self) path_input.setVisible(False) From 19c6a4a72e5054bdfb8a4839ad47b22efd609ff5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 12:33:46 +0200 Subject: [PATCH 35/71] fixed layouts --- .../tools/settings/settings/categories.py | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 55528a3d0d..9edd22ed89 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -181,6 +181,15 @@ class SettingsCategoryWidget(QtWidgets.QWidget): scroll_widget.setObjectName("GroupWidget") content_widget = QtWidgets.QWidget(scroll_widget) + breadcrumbs_label = QtWidgets.QLabel("Path:", content_widget) + breadcrumbs_widget = BreadcrumbsAddressBar(content_widget) + + breadcrumbs_layout = QtWidgets.QHBoxLayout() + breadcrumbs_layout.setContentsMargins(5, 0, 5, 0) + breadcrumbs_layout.setSpacing(5) + breadcrumbs_layout.addWidget(breadcrumbs_label) + breadcrumbs_layout.addWidget(breadcrumbs_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(3, 3, 3, 3) content_layout.setSpacing(5) @@ -189,40 +198,39 @@ class SettingsCategoryWidget(QtWidgets.QWidget): scroll_widget.setWidgetResizable(True) scroll_widget.setWidget(content_widget) - configurations_widget = QtWidgets.QWidget(self) - - footer_widget = QtWidgets.QWidget(configurations_widget) - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - refresh_icon = qtawesome.icon("fa.refresh", color="white") - refresh_btn = QtWidgets.QPushButton(footer_widget) + refresh_btn = QtWidgets.QPushButton(self) refresh_btn.setIcon(refresh_icon) - footer_layout.addWidget(refresh_btn, 0) - + footer_layout = QtWidgets.QHBoxLayout() if self.user_role == "developer": self._add_developer_ui(footer_layout) - breadcrumbs_widget = BreadcrumbsAddressBar(content_widget) - - save_btn = QtWidgets.QPushButton("Save", footer_widget) - require_restart_label = QtWidgets.QLabel(footer_widget) + save_btn = QtWidgets.QPushButton("Save", self) + require_restart_label = QtWidgets.QLabel(self) require_restart_label.setAlignment(QtCore.Qt.AlignCenter) + + footer_layout.addWidget(refresh_btn, 0) footer_layout.addWidget(require_restart_label, 1) footer_layout.addWidget(save_btn, 0) - configurations_layout = QtWidgets.QVBoxLayout(configurations_widget) + configurations_layout = QtWidgets.QVBoxLayout() configurations_layout.setContentsMargins(0, 0, 0, 0) configurations_layout.setSpacing(0) - configurations_layout.addWidget(breadcrumbs_widget, 0) configurations_layout.addWidget(scroll_widget, 1) - configurations_layout.addWidget(footer_widget, 0) + configurations_layout.addLayout(footer_layout, 0) - main_layout = QtWidgets.QHBoxLayout(self) + conf_wrapper_layout = QtWidgets.QHBoxLayout() + conf_wrapper_layout.setContentsMargins(0, 0, 0, 0) + conf_wrapper_layout.setSpacing(0) + conf_wrapper_layout.addLayout(configurations_layout, 1) + + main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) - main_layout.addWidget(configurations_widget, 1) + main_layout.addLayout(breadcrumbs_layout, 0) + main_layout.addLayout(conf_wrapper_layout, 1) save_btn.clicked.connect(self._save) refresh_btn.clicked.connect(self._on_refresh) @@ -234,9 +242,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.scroll_widget = scroll_widget self.content_layout = content_layout self.content_widget = content_widget - self.configurations_widget = configurations_widget self.breadcrumbs_widget = breadcrumbs_widget self.breadcrumbs_model = None + self.conf_wrapper_layout = conf_wrapper_layout self.main_layout = main_layout self.ui_tweaks() @@ -565,7 +573,7 @@ class ProjectWidget(SettingsCategoryWidget): project_list_widget = ProjectListWidget(self) - self.main_layout.insertWidget(0, project_list_widget, 0) + self.conf_wrapper_layout.insertWidget(0, project_list_widget, 0) project_list_widget.project_changed.connect(self._on_project_change) From 6479e09ecbb660bc7c7afd44d360a2a7ac780812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 12 Aug 2021 14:49:47 +0200 Subject: [PATCH 36/71] repair accident deletion --- openpype/settings/defaults/project_settings/maya.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index dc95632bb8..592b424fd8 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -177,12 +177,14 @@ }, "ValidateModelName": { "enabled": false, + "database": true, "material_file": { "windows": "", "darwin": "", "linux": "" }, - "regex": "(.*)_(\\\\d)*_(.*)_(GEO)" + "regex": "(.*)_(\\d)*_(?P.*)_(GEO)", + "top_level_regex": ".*_GRP" }, "ValidateTransformNamingSuffix": { "enabled": true, From 9cf06f1673e09dfdb4c8d03b2b1fdb25f9a98931 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 15:09:12 +0200 Subject: [PATCH 37/71] added ability to set focus and scroll to a widget --- openpype/tools/settings/settings/base.py | 24 +++++++++++++++++++ .../tools/settings/settings/categories.py | 9 ++++++- .../settings/settings/dict_conditional.py | 19 +++++++++++++++ .../settings/settings/dict_mutable_widget.py | 22 +++++++++++++++++ .../tools/settings/settings/item_widgets.py | 22 +++++++++++++++++ .../settings/settings/list_item_widget.py | 22 +++++++++++++++++ .../settings/settings/list_strict_widget.py | 13 ++++++++++ openpype/tools/settings/settings/widgets.py | 3 +++ 8 files changed, 133 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index eb5f82ab9a..6775e8fc82 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -25,6 +25,21 @@ class BaseWidget(QtWidgets.QWidget): self.label_widget = None self.create_ui() + def scroll_to(self, widget): + self.category_widget.scroll_to(widget) + + def set_focus(self, scroll_to=False): + if scroll_to: + self.scroll_to(self) + self.setFocus() + + def make_sure_is_visible(self, path, scroll_to): + raise NotImplementedError( + "{} not implemented `make_sure_is_visible`".format( + self.__class__.__name__ + ) + ) + def trigger_hierarchical_style_update(self): self.category_widget.hierarchical_style_update() @@ -337,6 +352,12 @@ class InputWidget(BaseWidget): ) ) + def make_sure_is_visible(self, path, scroll_to): + if path: + entity_path = self.entity.path + if entity_path == path: + self.set_focus(scroll_to) + def update_style(self): has_unsaved_changes = self.entity.has_unsaved_changes if not has_unsaved_changes and self.entity.group_item: @@ -427,6 +448,9 @@ class GUIWidget(BaseWidget): def hierarchical_style_update(self): pass + def make_sure_is_visible(self, *args, **kwargs): + pass + def get_invalid(self): return [] diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 9edd22ed89..c791786609 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -253,7 +253,14 @@ class SettingsCategoryWidget(QtWidgets.QWidget): return def _on_path_edit(self, path): - print("Path edited:", path) + for input_field in self.input_fields: + input_field.make_sure_is_visible(path, True) + + def scroll_to(self, widget): + if not widget: + return + + self.scroll_widget.ensureWidgetVisible(widget) def _add_developer_ui(self, footer_layout): modify_defaults_widget = QtWidgets.QWidget() diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 31a4fa9fab..222cca03f9 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -213,6 +213,25 @@ class DictConditionalWidget(BaseWidget): else: body_widget.hide_toolbox(hide_content=False) + def make_sure_is_visible(self, path, scroll_to): + if not path: + return + + entity_path = self.entity.path + if entity_path == path: + self.set_focus(scroll_to) + return + + if not path.startswith(entity_path): + return + + if self.body_widget and not self.body_widget.is_expanded(): + self.body_widget.toggle_content(True) + QtWidgets.QApplication.processEvents() + + for input_field in self.input_fields: + input_field.make_sure_is_visible(path, scroll_to) + def add_widget_to_layout(self, widget, label=None): if not widget.entity: map_id = widget.id diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index df6525e86a..596923fee4 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -319,6 +319,9 @@ class ModifiableDictItem(QtWidgets.QWidget): self.category_widget, self.entity, self ) + def make_sure_is_visible(self, *args, **kwargs): + self.input_field.make_sure_is_visible(*args, **kwargs) + def get_style_state(self): if self.is_invalid: return "invalid" @@ -846,6 +849,25 @@ class DictMutableKeysWidget(BaseWidget): if changed: self.on_shuffle() + def make_sure_is_visible(self, path, scroll_to): + if not path: + return + + entity_path = self.entity.path + if entity_path == path: + self.set_focus(scroll_to) + return + + if not path.startswith(entity_path): + return + + if self.body_widget and not self.body_widget.is_expanded(): + self.body_widget.toggle_content(True) + QtWidgets.QApplication.processEvents() + + for input_field in self.input_fields: + input_field.make_sure_is_visible(path, scroll_to) + def set_entity_value(self): while self.input_fields: self.remove_row(self.input_fields[0]) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 82afbb0a13..6724b913d4 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -154,6 +154,25 @@ class DictImmutableKeysWidget(BaseWidget): else: body_widget.hide_toolbox(hide_content=False) + def make_sure_is_visible(self, path, scroll_to): + if not path: + return + + entity_path = self.entity.path + if entity_path == path: + self.set_focus(scroll_to) + return + + if not path.startswith(entity_path): + return + + if self.body_widget and not self.body_widget.is_expanded(): + self.body_widget.toggle_content(True) + QtWidgets.QApplication.processEvents() + + for input_field in self.input_fields: + input_field.make_sure_is_visible(path, scroll_to) + def add_widget_to_layout(self, widget, label=None): if self.checkbox_child and widget.entity is self.checkbox_child: self.body_widget.add_widget_before_label(widget) @@ -562,6 +581,9 @@ class PathWidget(BaseWidget): def set_entity_value(self): self.input_field.set_entity_value() + def make_sure_is_visible(self, *args, **kwargs): + self.input_field.make_sure_is_visible(*args, **kwargs) + def hierarchical_style_update(self): self.update_style() self.input_field.hierarchical_style_update() diff --git a/openpype/tools/settings/settings/list_item_widget.py b/openpype/tools/settings/settings/list_item_widget.py index c9df5caf01..5abd9b0b20 100644 --- a/openpype/tools/settings/settings/list_item_widget.py +++ b/openpype/tools/settings/settings/list_item_widget.py @@ -129,6 +129,9 @@ class ListItem(QtWidgets.QWidget): *args, **kwargs ) + def make_sure_is_visible(self, *args, **kwargs): + self.input_field.make_sure_is_visible(*args, **kwargs) + @property def is_invalid(self): return self.input_field.is_invalid @@ -275,6 +278,25 @@ class ListWidget(InputWidget): invalid.extend(input_field.get_invalid()) return invalid + def make_sure_is_visible(self, path, scroll_to): + if not path: + return + + entity_path = self.entity.path + if entity_path == path: + self.set_focus(scroll_to) + return + + if not path.startswith(entity_path): + return + + if self.body_widget and not self.body_widget.is_expanded(): + self.body_widget.toggle_content(True) + QtWidgets.QApplication.processEvents() + + for input_field in self.input_fields: + input_field.make_sure_is_visible(path, scroll_to) + def _on_entity_change(self): # TODO do less inefficient childen_order = [] diff --git a/openpype/tools/settings/settings/list_strict_widget.py b/openpype/tools/settings/settings/list_strict_widget.py index 340db2e8c6..94a6ff53e2 100644 --- a/openpype/tools/settings/settings/list_strict_widget.py +++ b/openpype/tools/settings/settings/list_strict_widget.py @@ -65,6 +65,19 @@ class ListStrictWidget(BaseWidget): invalid.extend(input_field.get_invalid()) return invalid + def make_sure_is_visible(self, path, scroll_to): + if not path: + return + + entity_path = self.entity.path + if entity_path == path: + self.set_focus(scroll_to) + return + + if path.startswith(entity_path): + for input_field in self.input_fields: + input_field.make_sure_is_visible(path, scroll_to) + def add_widget_to_layout(self, widget, label=None): # Horizontally added children if self.entity.is_horizontal: diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index b20ce5ed66..2f9b504525 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -215,6 +215,9 @@ class ExpandingWidget(QtWidgets.QWidget): self.main_layout.addWidget(content_widget) self.content_widget = content_widget + def is_expanded(self): + return self.button_toggle.isChecked() + def _btn_clicked(self): self.toggle_content(self.button_toggle.isChecked()) From ad52e0f1400157fafc6d238826c5b3826236a04a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 15:25:21 +0200 Subject: [PATCH 38/71] change path based on focus --- openpype/tools/settings/settings/base.py | 17 ++++++++++++++++- openpype/tools/settings/settings/categories.py | 7 ++++--- openpype/tools/settings/settings/widgets.py | 2 ++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 6775e8fc82..b10ec58142 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -28,6 +28,9 @@ class BaseWidget(QtWidgets.QWidget): def scroll_to(self, widget): self.category_widget.scroll_to(widget) + def set_path(self, path): + self.category_widget.set_path(path) + def set_focus(self, scroll_to=False): if scroll_to: self.scroll_to(self) @@ -292,11 +295,23 @@ class BaseWidget(QtWidgets.QWidget): if to_run: to_run() + def focused_in(self): + if self.entity is not None: + self.set_path(self.entity.path) + def mouseReleaseEvent(self, event): if self.allow_actions and event.button() == QtCore.Qt.RightButton: return self.show_actions_menu() - return super(BaseWidget, self).mouseReleaseEvent(event) + focused_in = False + if event.button() == QtCore.Qt.LeftButton: + focused_in = True + self.focused_in() + + result = super(BaseWidget, self).mouseReleaseEvent(event) + if focused_in and not event.isAccepted(): + event.accept() + return result class InputWidget(BaseWidget): diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index c791786609..1506e5bbfd 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -257,10 +257,11 @@ class SettingsCategoryWidget(QtWidgets.QWidget): input_field.make_sure_is_visible(path, True) def scroll_to(self, widget): - if not widget: - return + if widget: + self.scroll_widget.ensureWidgetVisible(widget) - self.scroll_widget.ensureWidgetVisible(widget) + def set_path(self, path): + self.breadcrumbs_widget.set_path(path) def _add_developer_ui(self, footer_layout): modify_defaults_widget = QtWidgets.QWidget() diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 2f9b504525..ac5870eabc 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -383,6 +383,8 @@ class GridLabelWidget(QtWidgets.QWidget): def mouseReleaseEvent(self, event): if self.input_field: + if event and event.button() == QtCore.Qt.LeftButton: + self.input_field.focused_in() return self.input_field.show_actions_menu(event) return super(GridLabelWidget, self).mouseReleaseEvent(event) From d4b8c7be863d173048f0180e54eab47228d5e53b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 15:48:47 +0200 Subject: [PATCH 39/71] got rid of spacer widgets --- .../settings/settings/dict_mutable_widget.py | 4 +-- .../tools/settings/settings/item_widgets.py | 5 +-- .../settings/settings/list_item_widget.py | 13 +------ openpype/tools/settings/settings/widgets.py | 35 ++++++------------- 4 files changed, 14 insertions(+), 43 deletions(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 596923fee4..b733727e4e 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -61,7 +61,6 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): def create_addible_ui(self): add_btn = create_add_btn(self) remove_btn = create_remove_btn(self) - spacer_widget = SpacerWidget(self) remove_btn.setEnabled(False) @@ -70,13 +69,12 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): layout.setSpacing(3) layout.addWidget(add_btn, 0) layout.addWidget(remove_btn, 0) - layout.addWidget(spacer_widget, 1) + layout.addStretch(1) add_btn.clicked.connect(self._on_add_clicked) self.add_btn = add_btn self.remove_btn = remove_btn - self.spacer_widget = spacer_widget def _on_focus_lose(self): if self.key_input.hasFocus() or self.key_label_input.hasFocus(): diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 6724b913d4..57631f4530 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -289,11 +289,8 @@ class BoolWidget(InputWidget): height=checkbox_height, parent=self.content_widget ) - spacer = QtWidgets.QWidget(self.content_widget) - spacer.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.content_layout.addWidget(self.input_field, 0) - self.content_layout.addWidget(spacer, 1) + self.content_layout.addStretch(1) self.setFocusProxy(self.input_field) diff --git a/openpype/tools/settings/settings/list_item_widget.py b/openpype/tools/settings/settings/list_item_widget.py index 5abd9b0b20..52b96a658f 100644 --- a/openpype/tools/settings/settings/list_item_widget.py +++ b/openpype/tools/settings/settings/list_item_widget.py @@ -18,8 +18,6 @@ class EmptyListItem(QtWidgets.QWidget): add_btn = QtWidgets.QPushButton("+", self) remove_btn = QtWidgets.QPushButton("-", self) - spacer_widget = QtWidgets.QWidget(self) - spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) remove_btn.setEnabled(False) @@ -35,13 +33,12 @@ class EmptyListItem(QtWidgets.QWidget): layout.setSpacing(3) layout.addWidget(add_btn, 0) layout.addWidget(remove_btn, 0) - layout.addWidget(spacer_widget, 1) + layout.addStretch(1) add_btn.clicked.connect(self._on_add_clicked) self.add_btn = add_btn self.remove_btn = remove_btn - self.spacer_widget = spacer_widget def _on_add_clicked(self): self.entity_widget.add_new_item() @@ -101,12 +98,6 @@ class ListItem(QtWidgets.QWidget): self.category_widget, self.entity, self ) - spacer_widget = QtWidgets.QWidget(self) - spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - spacer_widget.setVisible(False) - - layout.addWidget(spacer_widget, 1) - layout.addWidget(up_btn, 0) layout.addWidget(down_btn, 0) @@ -115,8 +106,6 @@ class ListItem(QtWidgets.QWidget): self.up_btn = up_btn self.down_btn = down_btn - self.spacer_widget = spacer_widget - self._row = -1 self._is_last = False diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index ac5870eabc..17eab5c89f 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -160,15 +160,13 @@ class ExpandingWidget(QtWidgets.QWidget): after_label_layout = QtWidgets.QHBoxLayout(after_label_widget) after_label_layout.setContentsMargins(0, 0, 0, 0) - spacer_widget = QtWidgets.QWidget(side_line_widget) - side_line_layout = QtWidgets.QHBoxLayout(side_line_widget) side_line_layout.setContentsMargins(5, 10, 0, 10) side_line_layout.addWidget(button_toggle) side_line_layout.addWidget(before_label_widget) side_line_layout.addWidget(label_widget) side_line_layout.addWidget(after_label_widget) - side_line_layout.addWidget(spacer_widget, 1) + side_line_layout.addStretch(1) top_part_layout = QtWidgets.QHBoxLayout(top_part) top_part_layout.setContentsMargins(0, 0, 0, 0) @@ -176,7 +174,6 @@ class ExpandingWidget(QtWidgets.QWidget): before_label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) after_label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) @@ -344,31 +341,21 @@ class GridLabelWidget(QtWidgets.QWidget): self.properties = {} + label_widget = QtWidgets.QLabel(label, self) + + label_proxy_layout = QtWidgets.QHBoxLayout() + label_proxy_layout.setContentsMargins(0, 0, 0, 0) + label_proxy_layout.setSpacing(0) + + label_proxy_layout.addWidget(label_widget, 0, QtCore.Qt.AlignRight) + layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 2, 0, 0) layout.setSpacing(0) - label_proxy = QtWidgets.QWidget(self) + layout.addLayout(label_proxy_layout, 0) + layout.addStretch(1) - label_proxy_layout = QtWidgets.QHBoxLayout(label_proxy) - label_proxy_layout.setContentsMargins(0, 0, 0, 0) - label_proxy_layout.setSpacing(0) - - label_widget = QtWidgets.QLabel(label, label_proxy) - spacer_widget_h = SpacerWidget(label_proxy) - label_proxy_layout.addWidget( - spacer_widget_h, 0, alignment=QtCore.Qt.AlignRight - ) - label_proxy_layout.addWidget( - label_widget, 0, alignment=QtCore.Qt.AlignRight - ) - - spacer_widget_v = SpacerWidget(self) - - layout.addWidget(label_proxy, 0) - layout.addWidget(spacer_widget_v, 1) - - label_proxy.setAttribute(QtCore.Qt.WA_TranslucentBackground) label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.label_widget = label_widget From 3ebc4b34af41232953aeea9596b7f1c9c97e5bd4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 15:55:13 +0200 Subject: [PATCH 40/71] handle text input focus --- .../tools/settings/settings/item_widgets.py | 26 ++++++++++++++----- openpype/tools/settings/settings/widgets.py | 16 ++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 57631f4530..c98c3ba0b8 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -7,7 +7,9 @@ from .widgets import ( NumberSpinBox, GridLabelWidget, ComboBox, - NiceCheckbox + NiceCheckbox, + SettingsPlainTextEdit, + SettingsLineEdit ) from .multiselection_combobox import MultiSelectionComboBox from .wrapper_widgets import ( @@ -313,9 +315,9 @@ class TextWidget(InputWidget): def _add_inputs_to_layout(self): multiline = self.entity.multiline if multiline: - self.input_field = QtWidgets.QPlainTextEdit(self.content_widget) + self.input_field = SettingsPlainTextEdit(self.content_widget) else: - self.input_field = QtWidgets.QLineEdit(self.content_widget) + self.input_field = SettingsLineEdit(self.content_widget) placeholder_text = self.entity.placeholder_text if placeholder_text: @@ -329,8 +331,12 @@ class TextWidget(InputWidget): self.content_layout.addWidget(self.input_field, 1, **layout_kwargs) + self.input_field.focused_in.connect(self._on_input_focus) self.input_field.textChanged.connect(self._on_value_change) + def _on_input_focus(self): + self.focused_in() + def _on_entity_change(self): if self.entity.value != self.input_value(): self.set_entity_value() @@ -382,7 +388,7 @@ class NumberWidget(InputWidget): self.entity.set(self.input_field.value()) -class RawJsonInput(QtWidgets.QPlainTextEdit): +class RawJsonInput(SettingsPlainTextEdit): tab_length = 4 def __init__(self, valid_type, *args, **kwargs): @@ -444,15 +450,18 @@ class RawJsonWidget(InputWidget): QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding ) - self.setFocusProxy(self.input_field) self.content_layout.addWidget( self.input_field, 1, alignment=QtCore.Qt.AlignTop ) + self.input_field.focused_in.connect(self._on_input_focus) self.input_field.textChanged.connect(self._on_value_change) + def _on_input_focus(self): + self.focused_in() + def set_entity_value(self): self.input_field.set_value(self.entity.value) self._is_invalid = self.input_field.has_invalid_value() @@ -651,14 +660,19 @@ class PathWidget(BaseWidget): class PathInputWidget(InputWidget): def _add_inputs_to_layout(self): - self.input_field = QtWidgets.QLineEdit(self.content_widget) + self.input_field = SettingsLineEdit(self.content_widget) placeholder = self.entity.placeholder_text if placeholder: self.input_field.setPlaceholderText(placeholder) self.setFocusProxy(self.input_field) self.content_layout.addWidget(self.input_field) + self.input_field.textChanged.connect(self._on_value_change) + self.input_field.focused_in.connect(self._on_input_focus) + + def _on_input_focus(self): + self.focused_in() def _on_entity_change(self): if self.entity.value != self.input_value(): diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 17eab5c89f..c727b22169 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -9,6 +9,22 @@ from avalon.mongodb import ( from openpype.settings.lib import get_system_settings +class SettingsLineEdit(QtWidgets.QLineEdit): + focused_in = QtCore.Signal() + + def focusInEvent(self, event): + super(SettingsLineEdit, self).focusInEvent(event) + self.focused_in.emit() + + +class SettingsPlainTextEdit(QtWidgets.QPlainTextEdit): + focused_in = QtCore.Signal() + + def focusInEvent(self, event): + super(SettingsPlainTextEdit, self).focusInEvent(event) + self.focused_in.emit() + + class ShadowWidget(QtWidgets.QWidget): def __init__(self, message, parent): super(ShadowWidget, self).__init__(parent) From 8491018e68927c94afb9a7ab198536a000f2b444 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 16:02:36 +0200 Subject: [PATCH 41/71] added focus in signals to comboboxes --- openpype/tools/settings/settings/item_widgets.py | 8 ++++++-- .../settings/settings/multiselection_combobox.py | 6 ++++++ openpype/tools/settings/settings/widgets.py | 11 ++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index c98c3ba0b8..97a63c7838 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -6,7 +6,7 @@ from .widgets import ( ExpandingWidget, NumberSpinBox, GridLabelWidget, - ComboBox, + SettingsComboBox, NiceCheckbox, SettingsPlainTextEdit, SettingsLineEdit @@ -495,7 +495,7 @@ class EnumeratorWidget(InputWidget): ) else: - self.input_field = ComboBox(self.content_widget) + self.input_field = SettingsComboBox(self.content_widget) for enum_item in self.entity.enum_items: for value, label in enum_item.items(): @@ -505,8 +505,12 @@ class EnumeratorWidget(InputWidget): self.setFocusProxy(self.input_field) + self.input_field.focused_in.connect(self._on_input_focus) self.input_field.value_changed.connect(self._on_value_change) + def _on_input_focus(self): + self.focused_in() + def _on_entity_change(self): if self.entity.value != self.input_field.value(): self.set_entity_value() diff --git a/openpype/tools/settings/settings/multiselection_combobox.py b/openpype/tools/settings/settings/multiselection_combobox.py index 30ecb7b84b..2e625faa2d 100644 --- a/openpype/tools/settings/settings/multiselection_combobox.py +++ b/openpype/tools/settings/settings/multiselection_combobox.py @@ -21,6 +21,8 @@ class ComboItemDelegate(QtWidgets.QStyledItemDelegate): class MultiSelectionComboBox(QtWidgets.QComboBox): value_changed = QtCore.Signal() + focused_in = QtCore.Signal() + ignored_keys = { QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, @@ -56,6 +58,10 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): self.lines = {} self.item_height = None + def focusInEvent(self, event): + self.focused_in.emit() + return super(SettingsComboBox, self).focusInEvent(event) + def mousePressEvent(self, event): """Reimplemented.""" self._popup_is_shown = False diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index c727b22169..f7c379eb3e 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -109,18 +109,23 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox): return output -class ComboBox(QtWidgets.QComboBox): +class SettingsComboBox(QtWidgets.QComboBox): value_changed = QtCore.Signal() + focused_in = QtCore.Signal() def __init__(self, *args, **kwargs): - super(ComboBox, self).__init__(*args, **kwargs) + super(SettingsComboBox, self).__init__(*args, **kwargs) self.currentIndexChanged.connect(self._on_change) self.setFocusPolicy(QtCore.Qt.StrongFocus) def wheelEvent(self, event): if self.hasFocus(): - return super(ComboBox, self).wheelEvent(event) + return super(SettingsComboBox, self).wheelEvent(event) + + def focusInEvent(self, event): + self.focused_in.emit() + return super(SettingsComboBox, self).focusInEvent(event) def _on_change(self, *args, **kwargs): self.value_changed.emit() From fd138b53be2f487a23e670dd19f16ebb8bfcdadc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 16:07:37 +0200 Subject: [PATCH 42/71] set names of breadcrumb widgets to be able style them --- openpype/tools/settings/settings/breadcrumb_widget.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/tools/settings/settings/breadcrumb_widget.py b/openpype/tools/settings/settings/breadcrumb_widget.py index d8a35daa5a..ccd55b7bda 100644 --- a/openpype/tools/settings/settings/breadcrumb_widget.py +++ b/openpype/tools/settings/settings/breadcrumb_widget.py @@ -209,6 +209,8 @@ class BreadcrumbsPathInput(QtWidgets.QLineEdit): def __init__(self, model, parent): super(BreadcrumbsPathInput, self).__init__(parent) + self.setObjectName("BreadcrumbsPathInput") + self.setFrame(False) completer = QtWidgets.QCompleter(self) @@ -261,6 +263,8 @@ class BreadcrumbsButton(QtWidgets.QToolButton): def __init__(self, path, model, parent): super(BreadcrumbsButton, self).__init__(parent) + self.setObjectName("BreadcrumbsButton") + path_prefix = path + "/" self.setAutoRaise(True) @@ -304,6 +308,8 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): def __init__(self, parent=None): super(BreadcrumbsAddressBar, self).__init__(parent) + self.setObjectName("BreadcrumbsAddressBar") + self.setAutoFillBackground(True) self.setFrameShape(self.StyledPanel) From a9e41a704c27060a218c7c1b70250a494c5f7462 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 16:08:02 +0200 Subject: [PATCH 43/71] renamed file with breadcrumbs content --- .../settings/{breadcrumb_widget.py => breadcrumbs_widget.py} | 0 openpype/tools/settings/settings/categories.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename openpype/tools/settings/settings/{breadcrumb_widget.py => breadcrumbs_widget.py} (100%) diff --git a/openpype/tools/settings/settings/breadcrumb_widget.py b/openpype/tools/settings/settings/breadcrumbs_widget.py similarity index 100% rename from openpype/tools/settings/settings/breadcrumb_widget.py rename to openpype/tools/settings/settings/breadcrumbs_widget.py diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 1506e5bbfd..3f332378eb 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -31,7 +31,7 @@ from openpype.settings.entities import ( from openpype.settings import SaveWarningExc from .widgets import ProjectListWidget -from .breadcrumb_widget import ( +from .breadcrumbs_widget import ( BreadcrumbsAddressBar, SystemSettingsBreadcrumbs, ProjectSettingsBreadcrumbs From fee1d7340719015ce8fa6fbeef7c18e8a221f8d4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 12 Aug 2021 16:45:29 +0200 Subject: [PATCH 44/71] Fix - Deadline publish on Linux started Tray instead of headless publishing --- vendor/deadline/custom/plugins/GlobalJobPreLoad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vendor/deadline/custom/plugins/GlobalJobPreLoad.py b/vendor/deadline/custom/plugins/GlobalJobPreLoad.py index 41df9d4dc9..8631b035cf 100644 --- a/vendor/deadline/custom/plugins/GlobalJobPreLoad.py +++ b/vendor/deadline/custom/plugins/GlobalJobPreLoad.py @@ -55,9 +55,9 @@ def inject_openpype_environment(deadlinePlugin): "AVALON_TASK, AVALON_APP_NAME" raise RuntimeError(msg) - print("args::{}".format(args)) + print("args:::{}".format(args)) - exit_code = subprocess.call(args, shell=True) + exit_code = subprocess.call(args, cwd=os.path.dirname(openpype_app)) if exit_code != 0: raise RuntimeError("Publishing failed, check worker's log") From c8eb053cb732455384a378e25d695b1b6f57c2b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 17:13:18 +0200 Subject: [PATCH 45/71] insert empty item on init --- openpype/tools/settings/settings/breadcrumbs_widget.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumbs_widget.py b/openpype/tools/settings/settings/breadcrumbs_widget.py index ccd55b7bda..8f010de136 100644 --- a/openpype/tools/settings/settings/breadcrumbs_widget.py +++ b/openpype/tools/settings/settings/breadcrumbs_widget.py @@ -265,14 +265,19 @@ class BreadcrumbsButton(QtWidgets.QToolButton): self.setObjectName("BreadcrumbsButton") - path_prefix = path + "/" + path_prefix = path + if path: + path_prefix += "/" self.setAutoRaise(True) self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) self.setMouseTracking(True) - self.setText(path.split("/")[-1]) + if path: + self.setText(path.split("/")[-1]) + else: + self.setProperty("empty", "1") menu = BreadcrumbsHintMenu(model, path_prefix, self) @@ -417,6 +422,7 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): item = "/".join(path_items) self._insert_crumb(item) path_items.pop(-1) + self._insert_crumb("") self.path_changed.emit(self._current_path) From 6064471b71f5b3e2cefc212c35af8ab7f93fdc0f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 17:17:06 +0200 Subject: [PATCH 46/71] added styles --- .../settings/settings/breadcrumbs_widget.py | 3 +- .../tools/settings/settings/style/style.css | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumbs_widget.py b/openpype/tools/settings/settings/breadcrumbs_widget.py index 8f010de136..085034a1f8 100644 --- a/openpype/tools/settings/settings/breadcrumbs_widget.py +++ b/openpype/tools/settings/settings/breadcrumbs_widget.py @@ -313,8 +313,6 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): def __init__(self, parent=None): super(BreadcrumbsAddressBar, self).__init__(parent) - self.setObjectName("BreadcrumbsAddressBar") - self.setAutoFillBackground(True) self.setFrameShape(self.StyledPanel) @@ -331,6 +329,7 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): # Container for breadcrumbs crumbs_panel = QtWidgets.QWidget(crumbs_container) + crumbs_panel.setObjectName("BreadcrumbsPanel") crumbs_layout = QtWidgets.QHBoxLayout() crumbs_layout.setContentsMargins(0, 0, 0, 0) diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css index 3ce9837a8b..6a6eb7209a 100644 --- a/openpype/tools/settings/settings/style/style.css +++ b/openpype/tools/settings/settings/style/style.css @@ -388,4 +388,31 @@ QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { background: #3d8ec9; -} \ No newline at end of file +} + +#BreadcrumbsPathInput { + padding: 2px; +} + +#BreadcrumbsButton { + padding-right: 12px; + font-size: 9pt; +} + +#BreadcrumbsButton[empty="1"] { + padding-right: 0px; +} + +#BreadcrumbsButton::menu-button { + width: 12px; + background: rgba(127, 127, 127, 60); +} +#BreadcrumbsButton::menu-button:hover { + background: rgba(127, 127, 127, 90); +} + +#BreadcrumbsPanel { + border: 1px solid #4e5254; + border-radius: 5px; + background: #21252B;; +} From d1191ab7e0742850fd5b90bb84b6a2a5069bcf8a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 17:17:17 +0200 Subject: [PATCH 47/71] fixed GUIWIdget --- openpype/tools/settings/settings/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index b10ec58142..0541c36ffe 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -458,7 +458,7 @@ class GUIWidget(BaseWidget): layout.addWidget(splitter_item) def set_entity_value(self): - return + pass def hierarchical_style_update(self): pass @@ -466,6 +466,9 @@ class GUIWidget(BaseWidget): def make_sure_is_visible(self, *args, **kwargs): pass + def set_path(self, *args, **kwargs): + pass + def get_invalid(self): return [] From d17d6d801bbfa15f38b3a5bd32ead1c3341c7665 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 17:17:42 +0200 Subject: [PATCH 48/71] use only path input reference --- openpype/tools/settings/settings/breadcrumbs_widget.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumbs_widget.py b/openpype/tools/settings/settings/breadcrumbs_widget.py index 085034a1f8..ce3d06dbc3 100644 --- a/openpype/tools/settings/settings/breadcrumbs_widget.py +++ b/openpype/tools/settings/settings/breadcrumbs_widget.py @@ -450,8 +450,5 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): def minimumSizeHint(self): result = super(BreadcrumbsAddressBar, self).minimumSizeHint() - result.setHeight(max( - self.path_input.height(), - self.crumbs_container.height() - )) + result.setHeight(self.path_input.minimumSizeHint().height()) return result From 37b3af142ef153d6f1fb11ec80f5fa4318777727 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 17:18:35 +0200 Subject: [PATCH 49/71] set font size for path input --- openpype/tools/settings/settings/style/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css index 6a6eb7209a..250c15063f 100644 --- a/openpype/tools/settings/settings/style/style.css +++ b/openpype/tools/settings/settings/style/style.css @@ -392,6 +392,7 @@ QTableView::item:selected:active, QTreeView::item:selected:active, QListView::it #BreadcrumbsPathInput { padding: 2px; + font-size: 9pt; } #BreadcrumbsButton { From 58983c21793939bdeea818951919afb9a1654eea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 17:18:44 +0200 Subject: [PATCH 50/71] reset path after refresh --- openpype/tools/settings/settings/categories.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 3f332378eb..88e987e793 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -466,7 +466,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.save_btn.setEnabled(True) if self.breadcrumbs_model is not None: + path = self.breadcrumbs_widget.path() + self.breadcrumbs_widget.set_path("") self.breadcrumbs_model.set_entity(self.entity) + self.breadcrumbs_widget.change_path(path) def add_children_gui(self): for child_obj in self.entity.children: From f5712bb99130688b93a6c82251a23173329a8757 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 17:21:37 +0200 Subject: [PATCH 51/71] path is also changed for number inputs --- openpype/tools/settings/settings/item_widgets.py | 4 ++++ openpype/tools/settings/settings/widgets.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 97a63c7838..4b60ef8d1c 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -374,6 +374,10 @@ class NumberWidget(InputWidget): self.content_layout.addWidget(self.input_field, 1) self.input_field.valueChanged.connect(self._on_value_change) + self.input_field.focused_in.connect(self._on_input_focus) + + def _on_input_focus(self): + self.focused_in() def _on_entity_change(self): if self.entity.value != self.input_field.value(): diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index f7c379eb3e..34b222dd8e 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -86,6 +86,8 @@ class IconButton(QtWidgets.QPushButton): class NumberSpinBox(QtWidgets.QDoubleSpinBox): + focused_in = QtCore.Signal() + def __init__(self, *args, **kwargs): min_value = kwargs.pop("minimum", -99999) max_value = kwargs.pop("maximum", 99999) @@ -96,6 +98,10 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox): self.setMinimum(min_value) self.setMaximum(max_value) + def focusInEvent(self, event): + super(NumberSpinBox, self).focusInEvent(event) + self.focused_in.emit() + def wheelEvent(self, event): if self.hasFocus(): super(NumberSpinBox, self).wheelEvent(event) From bda4d311c5ec4175db9f73da690299a9ac803046 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 17:54:42 +0200 Subject: [PATCH 52/71] enhanced filling of path --- .../settings/settings/breadcrumbs_widget.py | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumbs_widget.py b/openpype/tools/settings/settings/breadcrumbs_widget.py index ce3d06dbc3..2a9eb2dce9 100644 --- a/openpype/tools/settings/settings/breadcrumbs_widget.py +++ b/openpype/tools/settings/settings/breadcrumbs_widget.py @@ -72,6 +72,12 @@ class SettingsBreadcrumbs(BreadcrumbsModel): self.entity = entity self.reset() + def has_children(self, path): + for key in self.entities_by_path.keys(): + if key.startswith(path): + return True + return False + class SystemSettingsBreadcrumbs(SettingsBreadcrumbs): def reset(self): @@ -206,7 +212,7 @@ class BreadcrumbsPathInput(QtWidgets.QLineEdit): cancelled = QtCore.Signal() confirmed = QtCore.Signal() - def __init__(self, model, parent): + def __init__(self, model, proxy_model, parent): super(BreadcrumbsPathInput, self).__init__(parent) self.setObjectName("BreadcrumbsPathInput") @@ -215,7 +221,7 @@ class BreadcrumbsPathInput(QtWidgets.QLineEdit): completer = QtWidgets.QCompleter(self) completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) - completer.setModel(model) + completer.setModel(proxy_model) popup = completer.popup() popup.setUniformItemSizes(True) @@ -228,16 +234,39 @@ class BreadcrumbsPathInput(QtWidgets.QLineEdit): self._completer = completer self._model = model + self._proxy_model = proxy_model self._context_menu_visible = False + def set_model(self, model): + self._model = model + + def event(self, event): + if ( + event.type() == QtCore.QEvent.KeyPress + and event.key() == QtCore.Qt.Key_Tab + ): + if self._model: + find_value = self.text() + "/" + if self._model.has_children(find_value): + self.insert("/") + else: + self._completer.popup().hide() + event.accept() + return True + + return super(BreadcrumbsPathInput, self).event(event) + def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Escape: self.cancelled.emit() - elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): + return + + if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): self.confirmed.emit() - else: - super(BreadcrumbsPathInput, self).keyPressEvent(event) + return + + super(BreadcrumbsPathInput, self).keyPressEvent(event) def focusOutEvent(self, event): if not self._context_menu_visible: @@ -254,7 +283,7 @@ class BreadcrumbsPathInput(QtWidgets.QLineEdit): self.confirmed.emit() def _on_text_change(self, path): - self._model.set_path_prefix(path) + self._proxy_model.set_path_prefix(path) class BreadcrumbsButton(QtWidgets.QToolButton): @@ -318,7 +347,7 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): # Edit presented path textually proxy_model = BreadcrumbsProxy() - path_input = BreadcrumbsPathInput(proxy_model, self) + path_input = BreadcrumbsPathInput(None, proxy_model, self) path_input.setVisible(False) path_input.cancelled.connect(self._on_input_cancel) @@ -372,6 +401,7 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): def set_model(self, model): self._model = model + self.path_input.set_model(model) self._proxy_model.setSourceModel(model) def _on_input_confirm(self): From a5a29d8895bab7a332b18c748654b709792e87a6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 18:10:21 +0200 Subject: [PATCH 53/71] list items have implemented getters --- openpype/settings/entities/item_entities.py | 13 +++++++++++++ openpype/settings/entities/list_entity.py | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index ecce7a22c0..efba6dbb61 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -195,6 +195,19 @@ class PathEntity(ItemEntity): class ListStrictEntity(ItemEntity): schema_types = ["list-strict"] + def __getitem__(self, idx): + if not isinstance(idx, int): + idx = int(idx) + return self.children[idx] + + def get(self, idx, default=None): + if not isinstance(idx, int): + idx = int(idx) + + if idx < len(self.children): + return self.children[idx] + return default + def _item_initalization(self): self.valid_value_types = (list, ) self.require_key = True diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index b07441251a..bcfacbf144 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -45,6 +45,19 @@ class ListEntity(EndpointEntity): return True return False + def __getitem__(self, idx): + if not isinstance(idx, int): + idx = int(idx) + return self.children[idx] + + def get(self, idx, default=None): + if not isinstance(idx, int): + idx = int(idx) + + if idx < len(self.children): + return self.children[idx] + return default + def index(self, item): if isinstance(item, BaseEntity): for idx, child_entity in enumerate(self.children): From 689e480cd5c16713d1aad8e5b0504c959e5676e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 18:10:56 +0200 Subject: [PATCH 54/71] validate path before confirming it --- .../settings/settings/breadcrumbs_widget.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumbs_widget.py b/openpype/tools/settings/settings/breadcrumbs_widget.py index 2a9eb2dce9..90105a4b4e 100644 --- a/openpype/tools/settings/settings/breadcrumbs_widget.py +++ b/openpype/tools/settings/settings/breadcrumbs_widget.py @@ -78,6 +78,19 @@ class SettingsBreadcrumbs(BreadcrumbsModel): return True return False + def is_valid_path(self, path): + if not path: + return True + + path_items = path.split("/") + try: + entity = self.entity + for item in path_items: + entity = entity[item] + except Exception: + return False + return True + class SystemSettingsBreadcrumbs(SettingsBreadcrumbs): def reset(self): @@ -406,7 +419,6 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): def _on_input_confirm(self): self.change_path(self.path_input.text()) - self._show_address_field(False) def _on_input_cancel(self): self._cancel_edit() @@ -429,8 +441,11 @@ class BreadcrumbsAddressBar(QtWidgets.QFrame): self.change_path(path) def change_path(self, path): - self.set_path(path) - self.path_edited.emit(path) + if self._model and not self._model.is_valid_path(path): + self._show_address_field() + else: + self.set_path(path) + self.path_edited.emit(path) def set_path(self, path): if path is None or path == ".": From ea0aff363eea0685e9e2f445543d567c5709f765 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 12 Aug 2021 18:14:19 +0200 Subject: [PATCH 55/71] list have also setters --- openpype/settings/entities/item_entities.py | 5 +++++ openpype/settings/entities/list_entity.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index efba6dbb61..ac6b3e76dd 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -200,6 +200,11 @@ class ListStrictEntity(ItemEntity): idx = int(idx) return self.children[idx] + def __setitem__(self, idx, value): + if not isinstance(idx, int): + idx = int(idx) + self.children[idx].set(value) + def get(self, idx, default=None): if not isinstance(idx, int): idx = int(idx) diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index bcfacbf144..b06f4d7a2e 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -50,6 +50,11 @@ class ListEntity(EndpointEntity): idx = int(idx) return self.children[idx] + def __setitem__(self, idx, value): + if not isinstance(idx, int): + idx = int(idx) + self.children[idx].set(value) + def get(self, idx, default=None): if not isinstance(idx, int): idx = int(idx) From ec13c2228095a9791732a3ace68dc5aafc887814 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 09:22:40 +0200 Subject: [PATCH 56/71] removed unused parts --- openpype/tools/settings/settings/breadcrumbs_widget.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/tools/settings/settings/breadcrumbs_widget.py b/openpype/tools/settings/settings/breadcrumbs_widget.py index 90105a4b4e..b625a7bb07 100644 --- a/openpype/tools/settings/settings/breadcrumbs_widget.py +++ b/openpype/tools/settings/settings/breadcrumbs_widget.py @@ -1,12 +1,5 @@ -import os -import sys -sys.path.append(r"C:\Users\jakub.trllo\Desktop\pype\pype3_2\.venv\Lib\site-packages") - from Qt import QtWidgets, QtGui, QtCore -# px, size of generated semi-transparent icons -TRANSP_ICON_SIZE = 40, 40 - PREFIX_ROLE = QtCore.Qt.UserRole + 1 LAST_SEGMENT_ROLE = QtCore.Qt.UserRole + 2 From 2691d294c4236ec257ce1d54470938c9f71a1517 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 09:22:48 +0200 Subject: [PATCH 57/71] added margins to path widget --- openpype/tools/settings/settings/categories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 88e987e793..d524462600 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -185,7 +185,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): breadcrumbs_widget = BreadcrumbsAddressBar(content_widget) breadcrumbs_layout = QtWidgets.QHBoxLayout() - breadcrumbs_layout.setContentsMargins(5, 0, 5, 0) + breadcrumbs_layout.setContentsMargins(5, 5, 5, 5) breadcrumbs_layout.setSpacing(5) breadcrumbs_layout.addWidget(breadcrumbs_label) breadcrumbs_layout.addWidget(breadcrumbs_widget) From c39ea2b65409c9ee2a5adcf7d39c3bfd0e798435 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 09:22:54 +0200 Subject: [PATCH 58/71] fixed super --- openpype/tools/settings/settings/multiselection_combobox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/multiselection_combobox.py b/openpype/tools/settings/settings/multiselection_combobox.py index 2e625faa2d..176f4cab8c 100644 --- a/openpype/tools/settings/settings/multiselection_combobox.py +++ b/openpype/tools/settings/settings/multiselection_combobox.py @@ -60,7 +60,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): def focusInEvent(self, event): self.focused_in.emit() - return super(SettingsComboBox, self).focusInEvent(event) + return super(MultiSelectionComboBox, self).focusInEvent(event) def mousePressEvent(self, event): """Reimplemented.""" From 65cf4559d278b68b0d9bea699ced3a14ad073ff9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 09:50:02 +0200 Subject: [PATCH 59/71] dict stores all direct children --- openpype/tools/settings/settings/item_widgets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 4b60ef8d1c..4d344cb8fc 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -48,6 +48,7 @@ class DictImmutableKeysWidget(BaseWidget): self._ui_item_base() label = self.entity.label + self._direct_children_widgets = [] self._parent_widget_by_entity_id = {} self._added_wrapper_ids = set() self._prepare_entity_layouts( @@ -193,6 +194,8 @@ class DictImmutableKeysWidget(BaseWidget): self._added_wrapper_ids.add(wrapper.id) return + self._direct_children_widgets.append(widget) + row = self.content_layout.rowCount() if not label or isinstance(widget, WrapperWidget): self.content_layout.addWidget(widget, row, 0, 1, 2) From 4a7995e9adeaa45f39e00a2bbe608175e247b122 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 09:51:03 +0200 Subject: [PATCH 60/71] make_sure_is_visible expect result --- openpype/tools/settings/settings/base.py | 5 +++++ .../tools/settings/settings/dict_conditional.py | 10 ++++++---- .../tools/settings/settings/dict_mutable_widget.py | 10 ++++++---- openpype/tools/settings/settings/item_widgets.py | 14 ++++++++------ .../tools/settings/settings/list_item_widget.py | 12 +++++++----- .../tools/settings/settings/list_strict_widget.py | 8 +++++--- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 0541c36ffe..915a408f4e 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -372,6 +372,8 @@ class InputWidget(BaseWidget): entity_path = self.entity.path if entity_path == path: self.set_focus(scroll_to) + return True + return False def update_style(self): has_unsaved_changes = self.entity.has_unsaved_changes @@ -464,6 +466,9 @@ class GUIWidget(BaseWidget): pass def make_sure_is_visible(self, *args, **kwargs): + return False + + def focused_in(self): pass def set_path(self, *args, **kwargs): diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 222cca03f9..c30159195c 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -215,22 +215,24 @@ class DictConditionalWidget(BaseWidget): def make_sure_is_visible(self, path, scroll_to): if not path: - return + return False entity_path = self.entity.path if entity_path == path: self.set_focus(scroll_to) - return + return True if not path.startswith(entity_path): - return + return False if self.body_widget and not self.body_widget.is_expanded(): self.body_widget.toggle_content(True) QtWidgets.QApplication.processEvents() for input_field in self.input_fields: - input_field.make_sure_is_visible(path, scroll_to) + if input_field.make_sure_is_visible(path, scroll_to): + return True + return False def add_widget_to_layout(self, widget, label=None): if not widget.entity: diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index b733727e4e..5bab74fabf 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -318,7 +318,7 @@ class ModifiableDictItem(QtWidgets.QWidget): ) def make_sure_is_visible(self, *args, **kwargs): - self.input_field.make_sure_is_visible(*args, **kwargs) + return self.input_field.make_sure_is_visible(*args, **kwargs) def get_style_state(self): if self.is_invalid: @@ -854,17 +854,19 @@ class DictMutableKeysWidget(BaseWidget): entity_path = self.entity.path if entity_path == path: self.set_focus(scroll_to) - return + return True if not path.startswith(entity_path): - return + return False if self.body_widget and not self.body_widget.is_expanded(): self.body_widget.toggle_content(True) QtWidgets.QApplication.processEvents() for input_field in self.input_fields: - input_field.make_sure_is_visible(path, scroll_to) + if input_field.make_sure_is_visible(path, scroll_to): + return True + return False def set_entity_value(self): while self.input_fields: diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 4d344cb8fc..9c69ee6705 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -159,22 +159,24 @@ class DictImmutableKeysWidget(BaseWidget): def make_sure_is_visible(self, path, scroll_to): if not path: - return + return False entity_path = self.entity.path if entity_path == path: self.set_focus(scroll_to) - return + return True if not path.startswith(entity_path): - return + return False if self.body_widget and not self.body_widget.is_expanded(): self.body_widget.toggle_content(True) QtWidgets.QApplication.processEvents() - for input_field in self.input_fields: - input_field.make_sure_is_visible(path, scroll_to) + for direct_child in self._direct_children_widgets: + if direct_child.make_sure_is_visible(path, scroll_to): + return True + return False def add_widget_to_layout(self, widget, label=None): if self.checkbox_child and widget.entity is self.checkbox_child: @@ -599,7 +601,7 @@ class PathWidget(BaseWidget): self.input_field.set_entity_value() def make_sure_is_visible(self, *args, **kwargs): - self.input_field.make_sure_is_visible(*args, **kwargs) + return self.input_field.make_sure_is_visible(*args, **kwargs) def hierarchical_style_update(self): self.update_style() diff --git a/openpype/tools/settings/settings/list_item_widget.py b/openpype/tools/settings/settings/list_item_widget.py index 52b96a658f..50630986d6 100644 --- a/openpype/tools/settings/settings/list_item_widget.py +++ b/openpype/tools/settings/settings/list_item_widget.py @@ -119,7 +119,7 @@ class ListItem(QtWidgets.QWidget): ) def make_sure_is_visible(self, *args, **kwargs): - self.input_field.make_sure_is_visible(*args, **kwargs) + return self.input_field.make_sure_is_visible(*args, **kwargs) @property def is_invalid(self): @@ -269,22 +269,24 @@ class ListWidget(InputWidget): def make_sure_is_visible(self, path, scroll_to): if not path: - return + return False entity_path = self.entity.path if entity_path == path: self.set_focus(scroll_to) - return + return True if not path.startswith(entity_path): - return + return False if self.body_widget and not self.body_widget.is_expanded(): self.body_widget.toggle_content(True) QtWidgets.QApplication.processEvents() for input_field in self.input_fields: - input_field.make_sure_is_visible(path, scroll_to) + if input_field.make_sure_is_visible(path, scroll_to): + return True + return False def _on_entity_change(self): # TODO do less inefficient diff --git a/openpype/tools/settings/settings/list_strict_widget.py b/openpype/tools/settings/settings/list_strict_widget.py index 94a6ff53e2..046b6992f6 100644 --- a/openpype/tools/settings/settings/list_strict_widget.py +++ b/openpype/tools/settings/settings/list_strict_widget.py @@ -67,16 +67,18 @@ class ListStrictWidget(BaseWidget): def make_sure_is_visible(self, path, scroll_to): if not path: - return + return False entity_path = self.entity.path if entity_path == path: self.set_focus(scroll_to) - return + return True if path.startswith(entity_path): for input_field in self.input_fields: - input_field.make_sure_is_visible(path, scroll_to) + if input_field.make_sure_is_visible(path, scroll_to): + return True + return False def add_widget_to_layout(self, widget, label=None): # Horizontally added children From b859ff99ddcec420427532813ca3a5ac16a9ab51 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 09:51:23 +0200 Subject: [PATCH 61/71] wrappers use the result to be able expand themselves --- .../tools/settings/settings/wrapper_widgets.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/tools/settings/settings/wrapper_widgets.py b/openpype/tools/settings/settings/wrapper_widgets.py index 915a2cf875..b14a226912 100644 --- a/openpype/tools/settings/settings/wrapper_widgets.py +++ b/openpype/tools/settings/settings/wrapper_widgets.py @@ -19,6 +19,14 @@ class WrapperWidget(QtWidgets.QWidget): self.create_ui() + def make_sure_is_visible(self, *args, **kwargs): + changed = False + for input_field in self.input_fields: + if input_field.make_sure_is_visible(*args, **kwargs): + changed = True + break + return changed + def create_ui(self): raise NotImplementedError( "{} does not have implemented `create_ui`.".format( @@ -89,6 +97,14 @@ class CollapsibleWrapper(WrapperWidget): else: body_widget.hide_toolbox(hide_content=False) + def make_sure_is_visible(self, *args, **kwargs): + result = super(CollapsibleWrapper, self).make_sure_is_visible( + *args, **kwargs + ) + if result: + self.body_widget.toggle_content(True) + return result + def add_widget_to_layout(self, widget, label=None): self.input_fields.append(widget) From a706fecda4cef15627288c32b32e14675b2999eb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 09:56:29 +0200 Subject: [PATCH 62/71] process app events before scrolling instead of each widget --- openpype/tools/settings/settings/categories.py | 5 +++++ openpype/tools/settings/settings/dict_conditional.py | 1 - openpype/tools/settings/settings/dict_mutable_widget.py | 1 - openpype/tools/settings/settings/item_widgets.py | 1 - openpype/tools/settings/settings/list_item_widget.py | 1 - 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index d524462600..6d8a5aa6b2 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -258,6 +258,11 @@ class SettingsCategoryWidget(QtWidgets.QWidget): def scroll_to(self, widget): if widget: + # Process events which happened before ensurence + # - that is because some widgets could be not visible before + # this method was called and have incorrect size + QtWidgets.QApplication.processEvents() + # Scroll to widget self.scroll_widget.ensureWidgetVisible(widget) def set_path(self, path): diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index c30159195c..3e3270cac9 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -227,7 +227,6 @@ class DictConditionalWidget(BaseWidget): if self.body_widget and not self.body_widget.is_expanded(): self.body_widget.toggle_content(True) - QtWidgets.QApplication.processEvents() for input_field in self.input_fields: if input_field.make_sure_is_visible(path, scroll_to): diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 5bab74fabf..25908f5a60 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -861,7 +861,6 @@ class DictMutableKeysWidget(BaseWidget): if self.body_widget and not self.body_widget.is_expanded(): self.body_widget.toggle_content(True) - QtWidgets.QApplication.processEvents() for input_field in self.input_fields: if input_field.make_sure_is_visible(path, scroll_to): diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 9c69ee6705..fed924e0bf 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -171,7 +171,6 @@ class DictImmutableKeysWidget(BaseWidget): if self.body_widget and not self.body_widget.is_expanded(): self.body_widget.toggle_content(True) - QtWidgets.QApplication.processEvents() for direct_child in self._direct_children_widgets: if direct_child.make_sure_is_visible(path, scroll_to): diff --git a/openpype/tools/settings/settings/list_item_widget.py b/openpype/tools/settings/settings/list_item_widget.py index 50630986d6..17412a30b9 100644 --- a/openpype/tools/settings/settings/list_item_widget.py +++ b/openpype/tools/settings/settings/list_item_widget.py @@ -281,7 +281,6 @@ class ListWidget(InputWidget): if self.body_widget and not self.body_widget.is_expanded(): self.body_widget.toggle_content(True) - QtWidgets.QApplication.processEvents() for input_field in self.input_fields: if input_field.make_sure_is_visible(path, scroll_to): From c08bf6b4ad4b5b3d8e55ffd4f14cfbb511d5c6dc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 09:57:11 +0200 Subject: [PATCH 63/71] added few docstrings --- openpype/tools/settings/settings/base.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 915a408f4e..8235cf8642 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -32,11 +32,25 @@ class BaseWidget(QtWidgets.QWidget): self.category_widget.set_path(path) def set_focus(self, scroll_to=False): + """Set focus of a widget. + + Args: + scroll_to(bool): Also scroll to widget in category widget. + """ if scroll_to: self.scroll_to(self) self.setFocus() def make_sure_is_visible(self, path, scroll_to): + """Make a widget of entity visible by it's path. + + Args: + path(str): Path to entity. + scroll_to(bool): Should be scrolled to entity. + + Returns: + bool: Entity with path was found. + """ raise NotImplementedError( "{} not implemented `make_sure_is_visible`".format( self.__class__.__name__ From 255de274056fb689e08a56da6f12df94643487e9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Aug 2021 12:18:37 +0200 Subject: [PATCH 64/71] Fix - ftrack family was added incorrectly in some cases --- .../modules/ftrack/plugins/publish/collect_ftrack_family.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 8464a43ef7..cc2a5b7d37 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -63,8 +63,9 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): self.log.debug("Adding ftrack family for '{}'". format(instance.data.get("family"))) - if families and "ftrack" not in families: - instance.data["families"].append("ftrack") + if families: + if "ftrack" not in families: + instance.data["families"].append("ftrack") else: instance.data["families"] = ["ftrack"] else: From 4390c85e5accb38acc0a47ae7a06b87c96ffc2c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Aug 2021 14:13:12 +0200 Subject: [PATCH 65/71] Fix - make AE workfile publish to Ftrack configurable By default workfile is sent to Ftrack, could be disabled by Settings --- .../hosts/aftereffects/plugins/publish/collect_workfile.py | 2 +- openpype/settings/defaults/project_settings/ftrack.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index 2f8f9ae91b..c1c2be4855 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -47,7 +47,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): "subset": subset, "label": scene_file, "family": family, - "families": [family, "ftrack"], + "families": [family], "representations": list() }) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 9fa78ac588..692176a585 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -304,7 +304,8 @@ "aftereffects" ], "families": [ - "render" + "render", + "workfile" ], "tasks": [], "add_ftrack_family": true, From 8a62360491413b711d183e3bde3dcf2c5e2c1a35 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Aug 2021 14:16:25 +0200 Subject: [PATCH 66/71] Revert "Fix - make AE workfile publish to Ftrack configurable" This reverts commit 4390c85e --- .../hosts/aftereffects/plugins/publish/collect_workfile.py | 2 +- openpype/settings/defaults/project_settings/ftrack.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index c1c2be4855..2f8f9ae91b 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -47,7 +47,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): "subset": subset, "label": scene_file, "family": family, - "families": [family], + "families": [family, "ftrack"], "representations": list() }) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 692176a585..9fa78ac588 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -304,8 +304,7 @@ "aftereffects" ], "families": [ - "render", - "workfile" + "render" ], "tasks": [], "add_ftrack_family": true, From e04523beabe8fabb84ea60bcd67c7decf5a18578 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Aug 2021 14:19:01 +0200 Subject: [PATCH 67/71] Fix - make AE workfile publish to Ftrack configurable By default workfile is sent to Ftrack, could be disabled by Settings --- .../hosts/aftereffects/plugins/publish/collect_workfile.py | 2 +- openpype/settings/defaults/project_settings/ftrack.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index 2f8f9ae91b..c1c2be4855 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -47,7 +47,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): "subset": subset, "label": scene_file, "family": family, - "families": [family, "ftrack"], + "families": [family], "representations": list() }) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 9fa78ac588..692176a585 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -304,7 +304,8 @@ "aftereffects" ], "families": [ - "render" + "render", + "workfile" ], "tasks": [], "add_ftrack_family": true, From b7d8520bff58def12579be3813de8a1e376ed87f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 14:49:42 +0200 Subject: [PATCH 68/71] skip visibility when done --- openpype/tools/settings/settings/categories.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 6d8a5aa6b2..d1babd7fdb 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -254,7 +254,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): def _on_path_edit(self, path): for input_field in self.input_fields: - input_field.make_sure_is_visible(path, True) + if input_field.make_sure_is_visible(path, True): + break def scroll_to(self, widget): if widget: From 6f1636ec8cb947086be01b61a4a25348ab511a37 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 14:49:56 +0200 Subject: [PATCH 69/71] add checbkox widget to direct children --- openpype/tools/settings/settings/item_widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index fed924e0bf..7bf2ffa59b 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -180,6 +180,7 @@ class DictImmutableKeysWidget(BaseWidget): def add_widget_to_layout(self, widget, label=None): if self.checkbox_child and widget.entity is self.checkbox_child: self.body_widget.add_widget_before_label(widget) + self._direct_children_widgets.append(widget) return if not widget.entity: From 3537202caf37c58bde43f0a279f6d9d038f779be Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 14:50:04 +0200 Subject: [PATCH 70/71] fixed return value --- openpype/tools/settings/settings/dict_mutable_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 25908f5a60..14314b8ab3 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -849,7 +849,7 @@ class DictMutableKeysWidget(BaseWidget): def make_sure_is_visible(self, path, scroll_to): if not path: - return + return False entity_path = self.entity.path if entity_path == path: From f2d59de930ec9dd91bd46c695e371a240be7eff4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 13 Aug 2021 14:50:13 +0200 Subject: [PATCH 71/71] handle checkbox widget properly --- .../tools/settings/settings/item_widgets.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 7bf2ffa59b..d29fa6f42b 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -169,13 +169,24 @@ class DictImmutableKeysWidget(BaseWidget): if not path.startswith(entity_path): return False - if self.body_widget and not self.body_widget.is_expanded(): - self.body_widget.toggle_content(True) - + is_checkbox_child = False + changed = False for direct_child in self._direct_children_widgets: if direct_child.make_sure_is_visible(path, scroll_to): - return True - return False + changed = True + if direct_child.entity is self.checkbox_child: + is_checkbox_child = True + break + + # Change scroll to this widget + if is_checkbox_child: + self.scroll_to(self) + + elif self.body_widget and not self.body_widget.is_expanded(): + # Expand widget if is callapsible + self.body_widget.toggle_content(True) + + return changed def add_widget_to_layout(self, widget, label=None): if self.checkbox_child and widget.entity is self.checkbox_child: