diff --git a/.github/workflows/update_bug_report.yml b/.github/workflows/update_bug_report.yml index 322bee4ff2..c2e5e9af42 100644 --- a/.github/workflows/update_bug_report.yml +++ b/.github/workflows/update_bug_report.yml @@ -15,7 +15,7 @@ jobs: with: ref: ${{ github.event.release.target_commitish }} - name: Update version - uses: ShaMan123/gha-populate-form-version@v2.0.2 + uses: ynput/gha-populate-form-version@main with: github_token: ${{ secrets.YNPUT_BOT_TOKEN }} registry: github diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 916fddd923..714278ba6c 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -1,4 +1,5 @@ import os +import re from maya import cmds @@ -12,6 +13,7 @@ from openpype.pipeline import ( AVALON_CONTAINER_ID, Anatomy, ) +from openpype.pipeline.load import LoadError from openpype.settings import get_project_settings from .pipeline import containerise from . import lib @@ -82,6 +84,44 @@ def get_reference_node_parents(ref): return parents +def get_custom_namespace(custom_namespace): + """Return unique namespace. + + The input namespace can contain a single group + of '#' number tokens to indicate where the namespace's + unique index should go. The amount of tokens defines + the zero padding of the number, e.g ### turns into 001. + + Warning: Note that a namespace will always be + prefixed with a _ if it starts with a digit + + Example: + >>> get_custom_namespace("myspace_##_") + # myspace_01_ + >>> get_custom_namespace("##_myspace") + # _01_myspace + >>> get_custom_namespace("myspace##") + # myspace01 + + """ + split = re.split("([#]+)", custom_namespace, 1) + + if len(split) == 3: + base, padding, suffix = split + padding = "%0{}d".format(len(padding)) + else: + base = split[0] + padding = "%02d" # default padding + suffix = "" + + return lib.unique_namespace( + base, + format=padding, + prefix="_" if not base or base[0].isdigit() else "", + suffix=suffix + ) + + class Creator(LegacyCreator): defaults = ['Main'] @@ -143,15 +183,46 @@ class ReferenceLoader(Loader): assert os.path.exists(self.fname), "%s does not exist." % self.fname asset = context['asset'] + subset = context['subset'] + settings = get_project_settings(context['project']['name']) + custom_naming = settings['maya']['load']['reference_loader'] loaded_containers = [] - count = options.get("count") or 1 - for c in range(0, count): - namespace = namespace or lib.unique_namespace( - "{}_{}_".format(asset["name"], context["subset"]["name"]), - prefix="_" if asset["name"][0].isdigit() else "", - suffix="_", + if not custom_naming['namespace']: + raise LoadError("No namespace specified in " + "Maya ReferenceLoader settings") + elif not custom_naming['group_name']: + raise LoadError("No group name specified in " + "Maya ReferenceLoader settings") + + formatting_data = { + "asset_name": asset['name'], + "asset_type": asset['type'], + "subset": subset['name'], + "family": ( + subset['data'].get('family') or + subset['data']['families'][0] ) + } + + custom_namespace = custom_naming['namespace'].format( + **formatting_data + ) + + custom_group_name = custom_naming['group_name'].format( + **formatting_data + ) + + count = options.get("count") or 1 + + for c in range(0, count): + namespace = get_custom_namespace(custom_namespace) + group_name = "{}:{}".format( + namespace, + custom_group_name + ) + + options['group_name'] = group_name # Offset loaded subset if "offset" in options: @@ -187,7 +258,7 @@ class ReferenceLoader(Loader): return loaded_containers - def process_reference(self, context, name, namespace, data): + def process_reference(self, context, name, namespace, options): """To be implemented by subclass""" raise NotImplementedError("Must be implemented by subclass") diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py index b419a730b5..2ba5fe6b64 100644 --- a/openpype/hosts/maya/plugins/load/_load_animation.py +++ b/openpype/hosts/maya/plugins/load/_load_animation.py @@ -14,7 +14,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - def process_reference(self, context, name, namespace, data): + def process_reference(self, context, name, namespace, options): import maya.cmds as cmds from openpype.hosts.maya.api.lib import unique_namespace @@ -41,7 +41,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): namespace=namespace, sharedReferenceFile=False, groupReference=True, - groupName="{}:{}".format(namespace, name), + groupName=options['group_name'], reference=True, returnNewNodes=True) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 461f4258aa..c2b321b789 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -125,14 +125,15 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): except ValueError: family = "model" - group_name = "{}:_GRP".format(namespace) # True by default to keep legacy behaviours attach_to_root = options.get("attach_to_root", True) + group_name = options["group_name"] with maintained_selection(): cmds.loadPlugin("AbcImport.mll", quiet=True) file_url = self.prepare_root_value(self.fname, context["project"]["name"]) + nodes = cmds.file(file_url, namespace=namespace, sharedReferenceFile=False, diff --git a/openpype/hosts/maya/plugins/load/load_yeti_rig.py b/openpype/hosts/maya/plugins/load/load_yeti_rig.py index 6a13d2e145..b8066871b0 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_rig.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_rig.py @@ -19,8 +19,7 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): def process_reference( self, context, name=None, namespace=None, options=None ): - - group_name = "{}:{}".format(namespace, name) + group_name = options['group_name'] with lib.maintained_selection(): file_url = self.prepare_root_value( self.fname, context["project"]["name"] diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 81913bcdd5..265a9c7822 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -354,6 +354,61 @@ def publish_plugins_discover(paths=None): return result +def _get_plugin_settings(host_name, project_settings, plugin, log): + """Get plugin settings based on host name and plugin name. + + Args: + host_name (str): Name of host. + project_settings (dict[str, Any]): Project settings. + plugin (pyliblish.Plugin): Plugin where settings are applied. + log (logging.Logger): Logger to log messages. + + Returns: + dict[str, Any]: Plugin settings {'attribute': 'value'}. + """ + + # Use project settings from host name category when available + try: + return ( + project_settings + [host_name] + ["publish"] + [plugin.__name__] + ) + except KeyError: + pass + + # Settings category determined from path + # - usually path is './/plugins/publish/' + # - category can be host name of addon name ('maya', 'deadline', ...) + filepath = os.path.normpath(inspect.getsourcefile(plugin)) + + split_path = filepath.rsplit(os.path.sep, 5) + if len(split_path) < 4: + log.warning( + 'plugin path too short to extract host {}'.format(filepath) + ) + return {} + + category_from_file = split_path[-4] + plugin_kind = split_path[-2] + + # TODO: change after all plugins are moved one level up + if category_from_file == "openpype": + category_from_file = "global" + + try: + return ( + project_settings + [category_from_file] + [plugin_kind] + [plugin.__name__] + ) + except KeyError: + pass + return {} + + def filter_pyblish_plugins(plugins): """Pyblish plugin filter which applies OpenPype settings. @@ -372,21 +427,21 @@ def filter_pyblish_plugins(plugins): # TODO: Don't use host from 'pyblish.api' but from defined host by us. # - kept becau on farm is probably used host 'shell' which propably # affect how settings are applied there - host = pyblish.api.current_host() + host_name = pyblish.api.current_host() project_name = os.environ.get("AVALON_PROJECT") - project_setting = get_project_settings(project_name) + project_settings = get_project_settings(project_name) system_settings = get_system_settings() # iterate over plugins for plugin in plugins[:]: + # Apply settings to plugins if hasattr(plugin, "apply_settings"): + # Use classmethod 'apply_settings' + # - can be used to target settings from custom settings place + # - skip default behavior when successful try: - # Use classmethod 'apply_settings' - # - can be used to target settings from custom settings place - # - skip default behavior when successful - plugin.apply_settings(project_setting, system_settings) - continue + plugin.apply_settings(project_settings, system_settings) except Exception: log.warning( @@ -395,53 +450,20 @@ def filter_pyblish_plugins(plugins): ).format(plugin.__name__), exc_info=True ) - - try: - config_data = ( - project_setting - [host] - ["publish"] - [plugin.__name__] + else: + # Automated + plugin_settins = _get_plugin_settings( + host_name, project_settings, plugin, log ) - except KeyError: - # host determined from path - file = os.path.normpath(inspect.getsourcefile(plugin)) - file = os.path.normpath(file) - - split_path = file.split(os.path.sep) - if len(split_path) < 4: - log.warning( - 'plugin path too short to extract host {}'.format(file) - ) - continue - - host_from_file = split_path[-4] - plugin_kind = split_path[-2] - - # TODO: change after all plugins are moved one level up - if host_from_file == "openpype": - host_from_file = "global" - - try: - config_data = ( - project_setting - [host_from_file] - [plugin_kind] - [plugin.__name__] - ) - except KeyError: - continue - - for option, value in config_data.items(): - if option == "enabled" and value is False: - log.info('removing plugin {}'.format(plugin.__name__)) - plugins.remove(plugin) - else: - log.info('setting {}:{} on plugin {}'.format( + for option, value in plugin_settins.items(): + log.info("setting {}:{} on plugin {}".format( option, value, plugin.__name__)) - setattr(plugin, option, value) + # Remove disabled plugins + if getattr(plugin, "enabled", True) is False: + plugins.remove(plugin) + def find_close_plugin(close_plugin_name, log): if close_plugin_name: diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 19d8667002..a8689524db 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1047,6 +1047,10 @@ 125, 255 ] + }, + "reference_loader": { + "namespace": "{asset_name}_{subset}_##", + "group_name": "_GRP" } }, "workfile_build": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json index 6b2315abc0..c1895c4824 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json @@ -91,6 +91,28 @@ "key": "yetiRig" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "reference_loader", + "label": "Reference Loader", + "children": [ + { + "type": "text", + "label": "Namespace", + "key": "namespace" + }, + { + "type": "text", + "label": "Group name", + "key": "group_name" + }, + { + "type": "label", + "label": "Here's a link to the doc where you can find explanations about customing the naming of referenced assets: https://openpype.io/docs/admin_hosts_maya#load-plugins" + } + ] } ] } diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index b62ae7ecc1..7754e4aa02 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -6,6 +6,7 @@ import collections import uuid import tempfile import shutil +import inspect from abc import ABCMeta, abstractmethod import six @@ -26,8 +27,8 @@ from openpype.pipeline import ( PublishValidationError, KnownPublishError, registered_host, - legacy_io, get_process_id, + OptionalPyblishPluginMixin, ) from openpype.pipeline.create import ( CreateContext, @@ -2307,6 +2308,37 @@ class PublisherController(BasePublisherController): def _process_main_thread_item(self, item): item() + def _is_publish_plugin_active(self, plugin): + """Decide if publish plugin is active. + + This is hack because 'active' is mis-used in mixin + 'OptionalPyblishPluginMixin' where 'active' is used for default value + of optional plugins. Because of that is 'active' state of plugin + which inherit from 'OptionalPyblishPluginMixin' ignored. That affects + headless publishing inside host, potentially remote publishing. + + We have to change that to match pyblish base, but we can do that + only when all hosts use Publisher because the change requires + change of settings schemas. + + Args: + plugin (pyblish.Plugin): Plugin which should be checked if is + active. + + Returns: + bool: Is plugin active. + """ + + if plugin.active: + return True + + if not plugin.optional: + return False + + if OptionalPyblishPluginMixin in inspect.getmro(plugin): + return True + return False + def _publish_iterator(self): """Main logic center of publishing. @@ -2315,11 +2347,9 @@ class PublisherController(BasePublisherController): states of currently processed publish plugin and instance. Also change state of processed orders like validation order has passed etc. - Also stops publishing if should stop on validation. - - QUESTION: - Does validate button still make sense? + Also stops publishing, if should stop on validation. """ + for idx, plugin in enumerate(self._publish_plugins): self._publish_progress = idx @@ -2344,6 +2374,11 @@ class PublisherController(BasePublisherController): # Add plugin to publish report self._publish_report.add_plugin_iter(plugin, self._publish_context) + # WARNING This is hack fix for optional plugins + if not self._is_publish_plugin_active(plugin): + self._publish_report.set_plugin_skipped() + continue + # Trigger callback that new plugin is going to be processed plugin_label = plugin.__name__ if hasattr(plugin, "label") and plugin.label: @@ -2450,7 +2485,11 @@ def collect_families_from_instances(instances, only_active=False): instances(list): List of publish instances from which are families collected. only_active(bool): Return families only for active instances. + + Returns: + list[str]: Families available on instances. """ + all_families = set() for instance in instances: if only_active: diff --git a/openpype/tools/publisher/publish_report_viewer/model.py b/openpype/tools/publisher/publish_report_viewer/model.py index 37da4ab3f2..ff10e091b8 100644 --- a/openpype/tools/publisher/publish_report_viewer/model.py +++ b/openpype/tools/publisher/publish_report_viewer/model.py @@ -162,7 +162,8 @@ class PluginsModel(QtGui.QStandardItemModel): items = [] for plugin_item in plugin_items: - item = QtGui.QStandardItem(plugin_item.label) + label = plugin_item.label or plugin_item.name + item = QtGui.QStandardItem(label) item.setData(False, ITEM_IS_GROUP_ROLE) item.setData(plugin_item.label, ITEM_LABEL_ROLE) item.setData(plugin_item.id, ITEM_ID_ROLE) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index fd04cb0a23..1beb168709 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -4,7 +4,6 @@ from qtpy import QtWidgets, QtCore, QtGui import qtawesome from openpype.client import get_projects -from openpype.pipeline import AvalonMongoDB from openpype.style import get_objected_colors from openpype.tools.utils.widgets import ImageButton from openpype.tools.utils.lib import paint_image_with_color @@ -97,6 +96,7 @@ class CompleterView(QtWidgets.QListView): # Open the widget unactivated self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) + self.setAttribute(QtCore.Qt.WA_NoMouseReplay) delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(delegate) @@ -241,6 +241,18 @@ class SettingsLineEdit(PlaceholderLineEdit): if self._completer is not None: self._completer.set_text_filter(text) + def _completer_should_be_visible(self): + return ( + self.isVisible() + and (self.hasFocus() or self._completer.hasFocus()) + ) + + def _show_completer(self): + if self._completer_should_be_visible(): + self._focus_timer.start() + self._completer.show() + self._update_completer() + def _update_completer(self): if self._completer is None or not self._completer.isVisible(): return @@ -249,7 +261,7 @@ class SettingsLineEdit(PlaceholderLineEdit): self._completer.move(new_point) def _on_focus_timer(self): - if not self.hasFocus() and not self._completer.hasFocus(): + if not self._completer_should_be_visible(): self._completer.hide() self._focus_timer.stop() @@ -258,9 +270,7 @@ class SettingsLineEdit(PlaceholderLineEdit): self.focused_in.emit() if self._completer is not None: - self._focus_timer.start() - self._completer.show() - self._update_completer() + self._show_completer() def paintEvent(self, event): super(SettingsLineEdit, self).paintEvent(event) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 5c8c48b2e3..0dd86d6e7c 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -106,6 +106,37 @@ or Deadlines **Draft Tile Assembler**. 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. +## Load Plugins + +### Reference Loader + +#### Namespace and Group Name +Here you can create your own custom naming for the reference loader. + +The custom naming is split into two parts: namespace and group name. If you don't set the namespace or the group name, an error will occur. +Here's the different variables you can use: + +
+
+ +| Token | Description | +|---|---| +|`{asset_name}` | Asset name | +|`{asset_type}` | Asset type | +|`{subset}` | Subset name | +|`{family}` | Subset family | + +
+
+ +The namespace field can contain a single group of '#' number tokens to indicate where the namespace's unique index should go. The amount of tokens defines the zero padding of the number, e.g ### turns into 001. + +Warning: Note that a namespace will always be prefixed with a _ if it starts with a digit. + +Example: + +![Namespace and Group Name](assets/maya-admin_custom_namespace.png) + ### Extract GPU Cache ![Maya GPU Cache](assets/maya-admin_gpu_cache.png) @@ -170,6 +201,7 @@ These options are set on the camera shape when publishing the review. They corre ![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings_camera_options.png) + ## 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_custom_namespace.png b/website/docs/assets/maya-admin_custom_namespace.png new file mode 100644 index 0000000000..80707ea727 Binary files /dev/null and b/website/docs/assets/maya-admin_custom_namespace.png differ