Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions hooks/tree_node_clicked.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Hook to call whenever a publish tree node is clicked (Qt tree item).

`.tk_multi_publish2` related wordings and phrases used:

- "tree node", or "node" for short:

- An instance of a subclass of
`.tk_multi_publish2.publish_tree_widget.tree_node_base.TreeNodeBase`

- Which itself subclasses `.QTreeWidgetItem`

- "widget" of the node

- Behaves like a delegate, but not actually using the Qt delegate system
from model/view architecture

- An instance of a subclass of
`.tk_multi_publish2.publish_tree_widget.custom_widget_base.CustomWidgetBase`

- Which itself subclasses `.QFrame`

- Publish "api" associated with the node, if any:

- `.tk_multi_publish2.api.item.PublishItem` for a `.TreeNodeItem`
- `.tk_multi_publish2.api.task.PublishTask` for a `.TreeNodeTask`
- otherwise `None`, i.e. for `.TreeNodeContext` and `.TreeNodeSummary`

"""

from __future__ import annotations

from typing import TYPE_CHECKING, TypeVar

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

HookBaseClass = sgtk.get_hook_baseclass()

TreeNode = TypeVar("TreeNode", bound=QtGui.QTreeWidgetItem)
CustomTreeWidget = TypeVar("CustomTreeWidget", bound=QtGui.QFrame)
PublishItem = TypeVar("PublishItem", bound="tk_multi_publish2.api.PublishItem")
PublishTask = TypeVar("PublishTask", bound="tk_multi_publish2.api.PublishTask")
if TYPE_CHECKING and (publish2_app := sgtk.platform.current_bundle()):
tk_multi_publish2 = publish2_app.import_module("tk_multi_publish2")


class TreeNodeClicked(HookBaseClass):
"""Hook called when a publish tree node is clicked (QTreeWidgetItem)."""

def single(
self,
node: TreeNode,
widget: CustomTreeWidget,
api: PublishItem | PublishTask | None,
buttons: QtCore.Qt.MouseButtons,
modifiers: QtCore.Qt.KeyboardModifiers,
) -> None:
"""Single click callback on a `.TreeNodeBase` (a `.QtGui.QTreeWidgetItem`).

By default, nothing additional is implemented and it's just Qt's built-in
behavior.
"""

def double(
self,
node: TreeNode,
widget: CustomTreeWidget,
api: PublishItem | PublishTask | None,
buttons: QtCore.Qt.MouseButtons,
modifiers: QtCore.Qt.KeyboardModifiers,
) -> None:
"""Double click callback on a `.TreeNodeBase` (a `.QTreeWidgetItem`).

Default implementation ensures expansion state is correctly set whenever
left/main mouse button is clicked (behavior from v2.10.8)
"""
if buttons == QtCore.Qt.LeftButton:
# Ensure expansion states are correctly updated
node.setExpanded(node.isExpanded())
8 changes: 8 additions & 0 deletions info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ configuration:
identification, publish display name, image sequence paths, etc."
default_value: "{self}/path_info.py"

tree_node_clicked:
type: hook
default_value: "{self}/tree_node_clicked.py"
description:
"Actions to take for clicking on a tree node (an instance of subclass
of TreeNodeBase). The methods 'single' and 'double' are called
respectively for single and double clicks."

thumbnail_generator:
type: hook
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ def __init__(self, parent):
self.addTopLevelItem(self._summary_node)
self._summary_node.setHidden(True)

# forward double clicks on items to the items themselves
self.itemDoubleClicked.connect(lambda i, c: i.double_clicked(c))
# forward clicks on items to the items themselves
self.itemDoubleClicked.connect(self._click_slot_factory("double"))
self.itemClicked.connect(self._click_slot_factory("single"))

# Capture the native expand toggles and update the button state.
self.itemExpanded.connect(self.on_item_expand_state_change)
Expand Down Expand Up @@ -559,6 +560,29 @@ def mouseMoveEvent(self, event):
# bubble up all events that aren't drag select related
super().mouseMoveEvent(event)

def _click_slot_factory(self, method_name):
"""Create a slot to call the given hook method on click.

i.e. for both single and double clicks::

self.itemClicked.connect(self._click_slot_factory("single"))
self.itemDoubleClicked.connect(self._click_slot_factory("double"))

"""

@QtCore.Slot(QtGui.QTreeWidgetItem, int)
def _on_click_slot(tree_node: QtGui.QTreeWidgetItem, column: int) -> None:
kwargs = {
"node": tree_node,
"widget": self.itemWidget(tree_node, column),
"api": tree_node.get_publish_instance(),
"buttons": QtGui.QApplication.mouseButtons(),
"modifiers": QtGui.QApplication.keyboardModifiers(),
}
self._bundle.execute_hook_method("tree_node_clicked", method_name, **kwargs)

return _on_click_slot


def _init_item_r(parent_item):

Expand Down
45 changes: 45 additions & 0 deletions tests/test_item_clicked_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (c) 2026 Autodesk.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the ShotGrid 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 ShotGrid Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Autodesk.
from unittest.mock import patch

from publish_api_test_base import PublishApiTestBase
from tank_test.tank_test_base import setUpModule # noqa


class TestItemClickedHook(PublishApiTestBase):
def test_emit_item_clicked(self):
tree = self.manager.tree
local_plugin = self.manager._load_publish_plugins(self.manager.context)[0]
tree.root_item.create_item("item", "Item", "Item").add_task(local_plugin)

tree_widget = self.PublishTreeWidget(None)
tree_widget.set_publish_manager(self.manager)
tree_widget.build_tree()

from sgtk.platform.qt import QtCore

column = 0
tree_item = tree_widget.topLevelItem(1).child(0)
expected_kwargs = {
"node": tree_item,
"widget": tree_widget.itemWidget(tree_item, column),
"api": tree_item.get_publish_instance(),
"buttons": QtCore.Qt.NoButton,
"modifiers": QtCore.Qt.NoModifier,
}
with patch.object(tree_widget._bundle, "execute_hook_method") as mocked_execute:
tree_widget.itemClicked.emit(tree_item, column)
mocked_execute.assert_called_with(
"tree_node_clicked", "single", **expected_kwargs
)
tree_widget.itemDoubleClicked.emit(tree_item, column)
mocked_execute.assert_called_with(
"tree_node_clicked", "double", **expected_kwargs
)