Source code for shotgun_menus.entity_field_menu
# Copyright (c) 2016 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
"""
This module implements a QMenu subclass that knows how to display all the fields
for a given Shotgun entity type.
An example of how to use it is:
class AppDialog(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
# grab a field manager to know what fields are displayable
self._field_manager = shotgun_fields.ShotgunFieldManager()
# setup a label to have the fields menu as its context menu
self.label = QtGui.QLabel("Right click me!")
self.label.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.label.customContextMenuRequested.connect(self.open_menu)
# and layout the dialog
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.label)
self.setLayout(layout)
def field_filter(self, field):
# display fields that are displayable by the shotgun field widgets
return bool(self._field_manager.supported_fields("CustomEntity02", [field]))
def open_menu(self, position):
menu = shotgun_menus.EntityFieldMenu("CustomEntity02")
# attach our filters
menu.set_field_filter(self.field_filter)
menu.set_checked_filter(self.checked_filter)
menu.set_disabled_filter(self.disabled_filter)
# show the menu and print the result
action = menu.exec_(self.label.mapToGlobal(position))
if action:
# action's data has the field that was selected
self.do_thing(action.data()["field"])
"""
import sgtk
from sgtk.platform.qt import QtGui
from .shotgun_menu import ShotgunMenu
shotgun_globals = sgtk.platform.import_framework(
"tk-framework-shotgunutils", "shotgun_globals"
)
[docs]class EntityFieldMenu(ShotgunMenu):
"""
A menu that automatically displays the fields for a given Shotgun entity.
The QActions for the menu will all have their data set to a dictionary in the form:
{"field": selected_field}
"""
_AUDIT_FIELDS = ["created_by", "created_at", "updated_by", "updated_at"]
def __init__(
self, sg_entity_type, parent=None, bg_task_manager=None, project_id=None
):
"""
Constructor
:param sg_entity_type: The entity type to build a menu for
:type sg_entity_type: String
:param parent: Parent widget
:type parent: :class:`~PySide.QtGui.QWidget`
:param bg_task_manager: The task manager the menu will use if it needs to run a task
:type bg_task_manager: :class:`~task_manager.BackgroundTaskManager`
:param int project_id: The project Entity id. If None, the current
context's project will be used, or the "site"
cache location will be returned if the current
context does not have an associated project.
"""
super(EntityFieldMenu, self).__init__(parent)
self._bundle = sgtk.platform.current_bundle()
self._sg_entity_type = sg_entity_type
# default state
self._field_filter = None
self._checked_filter = None
self._disabled_filter = None
self._entity_type_filter = None
self._project_id = project_id or self._get_current_project_id()
# prefix for fields if this menu represents an entity bubbled through another field
self._bubble_base = None
self._owns_task_manager = False
self._task_manager = bg_task_manager
if self._task_manager is None:
self._owns_task_manager = True
task_manager = sgtk.platform.import_framework(
"tk-framework-shotgunutils", "task_manager"
)
self._task_manager = task_manager.BackgroundTaskManager(
parent=self, max_threads=1, start_processing=True
)
# populate the menu the first time it is shown
self._initialized = False
self.aboutToShow.connect(self._on_about_to_show)
[docs] def set_field_filter(self, field_filter):
"""
Set the callback used to filter which fields are shown by the menu.
:param field_filter: Callback called for each entity field which returns True if the field
should be shown and False if it should not. The fields will be in "bubbled" notation,
for example "sg_sequence.Sequence.code"
:type field_filter: A method that takes a single field string as its only argument and
returns a boolean
"""
self._field_filter = field_filter
[docs] def set_checked_filter(self, checked_filter):
"""
Set the callback used to set which fields are checked. By specifying a value other than
None, all the menu items will be checkable.
:param checked_filter: Callback called for each entity field which returns True if the field
should be checked and False if it should not. The fields will be in "bubbled" notation,
for example "sg_sequence.Sequence.code"
:type checked_filter: A method that takes a single field string as its only argument and
returns a boolean
"""
self._checked_filter = checked_filter
[docs] def set_disabled_filter(self, disabled_filter):
"""
Set the callback used to filter which fields are disabled
:param disabled_filter: Callback called for each entity field which returns True if the field
should be disabled and False if it should not. The fields will be in "bubbled" notation,
for example "sg_sequence.Sequence.code"
:type disabled_filter: A method that takes a single field string as its only argument and
returns a boolean
"""
self._disabled_filter = disabled_filter
[docs] def set_entity_type_filter(self, entity_type_filter):
"""
Set the callback used to filter what entity types to display in submenus
:param entity_type_filter: Callback called for each entity type which returns True if the
given entity type should be displayed
:type entity_type_filter: A method that takes a single entity types string as its only argument
and returns a boolean
"""
self._entity_type_filter = entity_type_filter
def __del__(self):
"""
Destructor
"""
if self._owns_task_manager:
shotgun_globals.unregister_bg_task_manager(self._task_manager)
def _on_about_to_show(self):
"""
Lazy load the menu. This is because it is possible to have cycles when traversing
through the possible bubbled fields, so it is impossible to build the entire nested menu.
"""
if not self._initialized:
# need to wait until there is a schema available before populating the menu
shotgun_globals.run_on_schema_loaded(
self._populate, project_id=self._project_id
)
self._initialized = True
def _populate(self):
"""
Build the menu
"""
field_infos = []
bubble_fields = {}
# gather needed field info
for field in shotgun_globals.get_entity_fields(
self._sg_entity_type, project_id=self._project_id
):
# convert field to bubbled form
bubbled_field = self._get_bubbled_name(field)
# apply any needed filtering
if self._field_filter and not self._field_filter(bubbled_field):
continue
# grab display names
display_name = shotgun_globals.get_field_display_name(
self._sg_entity_type, field, project_id=self._project_id
)
field_infos.append(
{"field": field, "name": display_name, "bubbled": bubbled_field}
)
# grab info to build bubbled menu
try:
# grab the entity types this field can bubble to
entity_types = shotgun_globals.get_valid_types(
self._sg_entity_type, field, project_id=self._project_id
)
# filter out entities via the registered callback
if self._entity_type_filter:
entity_types = [
t for t in entity_types if self._entity_type_filter(t)
]
# and filter out any entities that don't have any displayable fields
if self._field_filter:
def entity_filter(et):
# get the list of fields for this entity type
fields = shotgun_globals.get_entity_fields(
et, project_id=self._project_id
)
# and filter them down with the filter
if self._field_filter:
fields = [
f
for f in fields
if self._field_filter(
"%s.%s.%s" % (bubbled_field, et, f)
)
]
return bool(fields)
entity_types = [et for et in entity_types if entity_filter(et)]
if entity_types:
bubble_fields[field] = {
"name": display_name,
"valid_types": entity_types,
"valid_type_names": [
shotgun_globals.get_type_display_name(
et, project_id=self._project_id
)
for et in entity_types
],
"bubbled_bases": [
"%s.%s" % (bubbled_field, et) for et in entity_types
],
}
except Exception:
# not a field that can be bubbled
pass
# sort by display name
field_infos.sort(key=lambda item: item["name"])
# add in all fields other than audit fields
audit_fields = []
bubbled_actions = []
for field_info in field_infos:
if field_info["field"] in self._AUDIT_FIELDS:
audit_fields.append(field_info)
else:
bubbled_actions.append(
self._get_qaction(field_info["bubbled"], field_info["name"])
)
if bubbled_actions:
self.add_group(bubbled_actions)
# now the audit fields
if audit_fields:
audit_actions = []
for field_info in audit_fields:
audit_actions.append(
self._get_qaction(field_info["field"], field_info["name"])
)
if audit_actions:
self.add_group(audit_actions, title="Audit Fields")
# and finally bubble fields
if bubble_fields:
linked_menus = []
for (field, field_info) in bubble_fields.items():
# pull all the bubbled field data in an order that sorts by display name
sorted_items = sorted(
zip(
field_info["valid_type_names"],
field_info["valid_types"],
field_info["bubbled_bases"],
)
)
entity_menus = []
for (type_name, entity_type, bubble_base) in sorted_items:
# build the menu for this entity passing on our state
entity_menu = EntityFieldMenu(
entity_type, parent=self, bg_task_manager=self._task_manager
)
entity_menu.set_field_filter(self._field_filter)
entity_menu.set_disabled_filter(self._disabled_filter)
entity_menu.set_checked_filter(self._checked_filter)
entity_menu._bubble_base = bubble_base
# keep track of the menus we built and what the display name for it would be
entity_menus.append((type_name, entity_menu))
if len(entity_menus) == 1:
# if there is only one type of entity possible, add the menu directly
entity_menu = entity_menus[0][1]
entity_menu.setTitle(field_info["name"])
linked_menus.append(entity_menu)
elif len(entity_menus) > 1:
# otherwise add an intermediate menu for each possible entity type
bubble_menu = QtGui.QMenu(field_info["name"])
for (type_name, entity_menu) in entity_menus:
entity_menu.setTitle(type_name)
bubble_menu.addMenu(entity_menu)
linked_menus.append(bubble_menu)
self.add_group(linked_menus, title="Linked Fields")
def _get_bubbled_name(self, field_name, bubble_base=None):
"""
Translate the given field name into a bubbled name. This will prepend the bubble string
that translates the given field name into a string that can be used to reach the field
from the entity associated with the root menu.
:param field_name: The non-bubbled Shotgun field name
:type field_name: String
"""
if bubble_base is None:
bubble_base = self._bubble_base
if bubble_base:
return "%s.%s" % (bubble_base, field_name)
return field_name
def _get_qaction(self, field, display_name):
"""
Add an action for the given field to the menu. The data for the action will contain
a dictionary where the selected field is set for the "field" key.
:param field: The field to add, in bubbled notation (eg 'entity.Shot.code')
:type field: String
:param display_name: The text to display for the action
:type display_name: String
"""
action = QtGui.QAction(display_name, self)
action.setData({"field": field})
if self._checked_filter:
action.setCheckable(True)
action.setChecked(self._checked_filter(field))
else:
action.setCheckable(False)
if self._disabled_filter:
action.setDisabled(self._disabled_filter(field))
return action
def _get_current_project_id(self):
"""
Return the id of the current project.
:returns: The project id associated with the current context, or ``None``
if operating in a site-level context.
:rtype: ``int`` or ``None``
"""
if self._bundle.tank.pipeline_configuration.is_site_configuration():
# site configuration (no project id). Return None which is
# consistent with core.
project_id = None
else:
project_id = self._bundle.tank.pipeline_configuration.get_project_id()
return project_id