Skip to content
43 changes: 43 additions & 0 deletions info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ configuration:
Published File Types). The legacy Published File Type filter widget
cannot be used in combination with the Filter menu.

use_medm_data:
type: bool
default_value: false
description: Set to True to use MEDM (Flow Asset Management) data instead of Shotgun
data. When enabled, the loader fetches publish data from the Flow Asset
Management system. Requires tk-framework-flowam to be configured in the
environment.

# hooks
actions_hook:
type: hook
Expand Down Expand Up @@ -98,6 +106,39 @@ configuration:
default_value_tk-flame:
Shot: [load_batch, create_batch]

entity_fields_middle_panel_list:
type: dict
description: "Additional fields to display for each entity type in the middle panel
list view, beyond the default fields that are already shown. Keys are
entity types (e.g. 'PublishedFile', 'Shot', 'Asset') and values are
lists of field names to display."
default_value: {}
allows_empty: true

entity_fields_middle_panel_thumbnail:
type: dict
description: "Additional fields to display for each entity type in the middle panel
thumbnail view, beyond the default fields that are already shown. Keys
are entity types and values are lists of field names to display."
default_value: {}
allows_empty: true

entity_fields_detail_panel:
type: dict
description: "Fields to display for each entity type in detail panel view. Keys are
entity types (e.g. 'PublishedFile', 'Shot', 'Asset') and values are
lists of field names to display."
default_value: {}
allows_empty: true

flow_am_internal_fields:
type: dict
description: "Extra Flow AM fields loaded for internal logic or workflow purposes
(not for display). Available during model initialisation and accessible
throughout the app, but not shown in the UI."
default_value: {}
allows_empty: true

entities:
default_value:
- caption: Project
Expand Down Expand Up @@ -166,3 +207,5 @@ documentation_url: "https://help.autodesk.com/view/SGDEV/ENU/?guid=SG_Supervisor
frameworks:
- {"name": "tk-framework-shotgunutils", "version": "v5.x.x", "minimum_version": "v5.8.6"}
- {"name": "tk-framework-qtwidgets", "version": "v2.x.x", "minimum_version": "v2.10.6"}
# TODO: Remove the following line after SG-43459.
- {"name": "tk-framework-flowam", "version": "v1.x.x"}
67 changes: 61 additions & 6 deletions python/tk_multi_loader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,71 @@
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.

from .api import LoaderManager
from .open_publish_form import open_publish_browser
from .api import LoaderManager # noqa: F401
from .open_publish_form import open_publish_browser # noqa: F401

import sys

import sgtk
from sgtk.platform.qt import QtCore, QtGui

from .ui import resources_rc
from .ui import resources_rc # noqa: F401

help_screen = sgtk.platform.import_framework("tk-framework-qtwidgets", "help_screen")


def _clear_stay_on_top(win):
"""
Clear WindowStaysOnTopHint so the loader doesn't stay on top of other apps or dialogs.

Nuke-specific bug: Tested across Maya, Houdini, and Nuke - confirmed this only
affects Nuke. Window flags comparison via hex(int(win.windowFlags())):
- Nuke: 0x8013003 (includes Qt.WindowStaysOnTopHint)
- Maya: 0x8003003 (normal flags)

In Nuke, Qt reports StaysOnTop = False but the window remains on top because
Qt injects WS_EX_TOPMOST at the OS level without reflecting it in its own flag
API (https://qt-project.atlassian.net/browse/QTBUG-36181). On Windows we must clear it directly via SetWindowPos.

The extra flags (SWP_NOMOVE, SWP_NOSIZE, SWP_NOACTIVATE) prevent accidental
window movement or resizing during the operation.
"""
if win is None or not win.isWidgetType():
return

if sys.platform == "win32":
try:
import ctypes

# Verify WS_EX_TOPMOST is actually set at OS level before touching anything
GWL_EXSTYLE = -20
WS_EX_TOPMOST = 0x00000008
hwnd = int(win.winId())
if not hwnd:
return

exstyle = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
if not (exstyle & WS_EX_TOPMOST):
return # Already not topmost, nothing to do

# SetWindowPos with HWND_NOTOPMOST clears WS_EX_TOPMOST at the Win32 level
HWND_NOTOPMOST = -2
SWP_NOMOVE = 0x0002
SWP_NOSIZE = 0x0001
SWP_NOACTIVATE = 0x0010
ctypes.windll.user32.SetWindowPos(
hwnd,
ctypes.c_void_p(HWND_NOTOPMOST),
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
)
except Exception:
pass

Check notice on line 73 in python/tk_multi_loader/__init__.py

View check run for this annotation

ShotGrid Chorus / security/bandit

B110: try_except_pass

Try, Except, Pass detected. secure coding id: PYTH-GEAP-50.


def show_dialog(app):
"""
Show the main loader dialog
Expand All @@ -44,9 +98,6 @@
ui_title = app.get_setting("title_name")
w = app.engine.show_dialog(ui_title, app, AppDialog, action_manager)

# Keep pointer to dialog so as to be able to hide/show it in actions
engine_name = app.engine.instance_name

# attach splash screen to the main window to help GC
w.__splash_screen = splash

Expand All @@ -57,3 +108,7 @@
if w.is_first_launch():
welcome_widget = w._welcome_msg()
welcome_widget.exec_()

# Called on all DCCs as a precaution in case other applications are affected.
# Safe for unaffected DCCs - the function checks WS_EX_TOPMOST at OS level first.
QtCore.QTimer.singleShot(0, lambda: _clear_stay_on_top(w.window()))
37 changes: 30 additions & 7 deletions python/tk_multi_loader/api/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,22 @@ def get_actions_for_publish(self, sg_data, ui_area):
# this publish does not have a type
publish_type = "undefined"
else:
publish_type = publish_type_dict["name"]
publish_type = (
publish_type_dict["code"]
if "code" in publish_type_dict
else publish_type_dict["name"]
)

# check if we have logic configured to handle this publish type.
mappings = self._bundle.get_setting("action_mappings")
if not mappings:
return []
# returns a structure on the form
# { "Maya Scene": ["reference", "import"] }
actions = mappings.get(publish_type, [])
actions.extend(mappings.get("All", []))
# IMPORTANT: Make a copy to avoid modifying the cached config dict
actions = list(mappings.get(publish_type, []))
if len(actions) == 0:
actions.extend(list(mappings.get("All", [])))
if len(actions) == 0:
return []

Expand Down Expand Up @@ -203,6 +209,22 @@ def get_actions_for_publishes(self, sg_data_list, ui_area):
)
intersection_actions[action_name] = actions_list

# Filter out actions not allowed for multi-select.
# When multiple publishes are selected, only show actions that are allowed for
# multi-select. If any action dict for a given name has 'multi_select' set to False,
# that action is excluded from the available actions.
if len(sg_data_list) > 1:
filtered_actions = {}
for action_name, action_list in intersection_actions.items():
allow_multiselect = True
for action_dict in action_list:
if not action_dict["action"].get("multi_select", True):
allow_multiselect = False
break
if allow_multiselect:
filtered_actions[action_name] = action_list
intersection_actions = filtered_actions

return intersection_actions

def execute_action(self, sg_data, action):
Expand Down Expand Up @@ -255,7 +277,7 @@ def get_actions_for_entity(self, sg_data):
:param sg_data: Shotgun data dictionary representing the entity we want to get actions for.
:return: List of dictionaries, each with keys name, params, caption and description
"""
entity_type = sg_data.get("type", None)
entity_type = sg_data.get("type", None) if sg_data else None

# check if we have logic configured to handle this publish type.
mappings = self._bundle.get_setting("entity_mappings")
Expand All @@ -264,7 +286,7 @@ def get_actions_for_entity(self, sg_data):

# returns a structure on the form
# { "Shot": ["reference", "import"] }
actions = mappings.get(entity_type, [])
actions = list(mappings.get(entity_type, []))

if len(actions) == 0:
return []
Expand Down Expand Up @@ -300,8 +322,9 @@ def has_actions(self, publish_type):

# returns a structure on the form
# { "Maya Scene": ["reference", "import"] }
my_mappings = mappings.get(publish_type, [])
my_mappings.extend(mappings.get("All", []))
# IMPORTANT: Make a copy to avoid modifying the cached config dict
my_mappings = list(mappings.get(publish_type, []))
my_mappings.extend(list(mappings.get("All", [])))

return len(my_mappings) > 0

Expand Down
102 changes: 82 additions & 20 deletions python/tk_multi_loader/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,88 @@

"""

# fields to pull down for published files
PUBLISHED_FILES_FIELDS = [
"name",
"version_number",
"image",
"entity",
"path",
"description",
"sg_status_list",
"task",
"task.Task.sg_status_list",
"task.Task.due_date",
"project",
"task.Task.content",
"created_by",
"created_at",
"version", # note: not supported on TankPublishedFile so always None
"version.Version.sg_status_list",
"created_by.HumanUser.image",
]
# Fields to query during model initialization for different entity types for detail panel usage
ENTITY_TYPE_DETAIL_PANEL_FIELDS = {
"PublishedFile": [
"name",
"description",
"entity",
"project",
"version_number",
"version",
"version.Version.sg_status_list",
"sg_status_list",
"task",
"task.Task.content",
"task.Task.sg_status_list",
"task.Task.due_date",
"path",
"created_by",
"created_at",
"image",
"created_by.HumanUser.image",
],
"Task": [
"content",
"sg_status_list",
"assigned_to",
"due_date",
"entity",
"project",
"step",
],
"Asset": [
"name",
"description",
"sg_status_list",
"project",
"created_by",
"shots",
],
"Shot": [
"name",
"description",
"sg_status_list",
"project",
"created_by",
"assets",
"sg_cut_in",
"sg_cut_out",
],
}

# Fields to query for different entity types to display in the middle panel
ENTITY_TYPE_MIDDLE_PANEL_FIELDS = {
"PublishedFile": [
"name",
"description",
"entity",
"project",
"version_number",
"version",
"version.Version.sg_status_list",
"sg_status_list",
"task",
"task.Task.content",
"task.Task.sg_status_list",
"task.Task.due_date",
"path",
"created_by",
"created_at",
"image",
"created_by.HumanUser.image",
],
"Asset": ["name", "description", "sg_status_list", "created_by"],
"Shot": [
"name",
"description",
"sg_status_list",
"project",
"sg_sequence",
"sg_cut_in",
"sg_cut_out",
],
}

# left hand side tree view search only kicks in
# after a certain number have been typed in.
Expand Down
8 changes: 4 additions & 4 deletions python/tk_multi_loader/delegate_publish_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def calculate_size():

:returns: Size of the widget
"""
return QtCore.QSize(200, 90)
return QtCore.QSize(200, 50)


class SgPublishHistoryDelegate(shotgun_view.EditSelectedWidgetDelegate):
Expand Down Expand Up @@ -217,7 +217,7 @@ def _on_before_paint(self, widget, model_index, style_options):
# v004 (2014-02-21 12:34)

header_str = ""
header_str += "<b style='color:#2C93E2'>Version %03d</b>" % (
header_str += "<b style='color:#F5F5F5'>Version %03d</b>" % (
sg_item.get("version_number") or 0
)

Expand All @@ -226,12 +226,12 @@ def _on_before_paint(self, widget, model_index, style_options):
date_str = datetime.datetime.fromtimestamp(created_unixtime).strftime(
"%Y-%m-%d %H:%M"
)
header_str += "&nbsp;&nbsp;<small>(%s)</small>" % date_str
header_str += "&nbsp;-&nbsp;<b>%s</b>" % date_str
except:
pass

# set the little description bit next to the artist icon
desc_str = sg_item.get("description") or "No Description Given"
desc_str = sg_item.get("description") or "N/A"
# created_by is set to None if the user has been deleted.
if sg_item.get("created_by") and sg_item["created_by"].get("name"):
author_str = sg_item["created_by"].get("name")
Expand Down
Loading
Loading