diff --git a/hooks/tree_node_clicked.py b/hooks/tree_node_clicked.py new file mode 100644 index 00000000..22b7a20e --- /dev/null +++ b/hooks/tree_node_clicked.py @@ -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()) diff --git a/info.yml b/info.yml index 87f8d7e5..409eaba9 100644 --- a/info.yml +++ b/info.yml @@ -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: diff --git a/python/tk_multi_publish2/publish_tree_widget/publish_tree_widget.py b/python/tk_multi_publish2/publish_tree_widget/publish_tree_widget.py index a8910628..bdaf0503 100644 --- a/python/tk_multi_publish2/publish_tree_widget/publish_tree_widget.py +++ b/python/tk_multi_publish2/publish_tree_widget/publish_tree_widget.py @@ -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) @@ -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): diff --git a/tests/test_item_clicked_hook.py b/tests/test_item_clicked_hook.py new file mode 100644 index 00000000..acae7d23 --- /dev/null +++ b/tests/test_item_clicked_hook.py @@ -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 + )