From 90e3317a867890ea4539cc54f3cefa7dfb611839 Mon Sep 17 00:00:00 2001 From: Joseph Yu Date: Thu, 5 Feb 2026 14:11:41 +0000 Subject: [PATCH 1/3] Redirected click to use hook --- hooks/tree_node_clicked.py | 72 +++++++++++++++++++ info.yml | 8 +++ .../publish_tree_widget.py | 26 ++++++- 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 hooks/tree_node_clicked.py diff --git a/hooks/tree_node_clicked.py b/hooks/tree_node_clicked.py new file mode 100644 index 00000000..df5fb927 --- /dev/null +++ b/hooks/tree_node_clicked.py @@ -0,0 +1,72 @@ +"""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 typing import TypeVar, Union +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) +API = Union[TypeVar("PublishItem"), TypeVar("PublishTask"), None] + + +class TreeNodeClicked(HookBaseClass): + @staticmethod + def flipped_state(check_state: QtCore.Qt.CheckState) -> QtCore.Qt.CheckState: + """Flips a unchecked state to checked and any other state to unchecked.""" + return ( + QtCore.Qt.Checked + if check_state == QtCore.Qt.Unchecked + else QtCore.Qt.Unchecked + ) + + def single( + self, + node: TreeNode, + widget: CustomTreeWidget, + api: API, + buttons: QtCore.Qt.MouseButtons, + modifiers: QtCore.Qt.KeyboardModifiers, + ): # type: (...) -> None + """Single click callback on a `.TreeNodeBase` (a `.QtGui.QTreeWidgetItem`).""" + + def double( + self, + node: TreeNode, + widget: CustomTreeWidget, + api: API, + buttons: QtCore.Qt.MouseButtons, + modifiers: QtCore.Qt.KeyboardModifiers, + ): # type: (...) -> None + """Double click callback on a `.TreeNodeBase` (a `.QtGui.QTreeWidgetItem`).""" + 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..7f0555d5 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 @@ -63,7 +63,8 @@ def __init__(self, parent): self._summary_node.setHidden(True) # forward double clicks on items to the items themselves - self.itemDoubleClicked.connect(lambda i, c: i.double_clicked(c)) + 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): From cbea86435862016ed1db682cf0c4f3966fbcc5ae Mon Sep 17 00:00:00 2001 From: Joseph Yu Date: Mon, 9 Feb 2026 13:49:06 +0000 Subject: [PATCH 2/3] Tidied added test --- hooks/tree_node_clicked.py | 8 ------ tests/test_item_clicked_hook.py | 45 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 tests/test_item_clicked_hook.py diff --git a/hooks/tree_node_clicked.py b/hooks/tree_node_clicked.py index df5fb927..e130b96a 100644 --- a/hooks/tree_node_clicked.py +++ b/hooks/tree_node_clicked.py @@ -39,14 +39,6 @@ class TreeNodeClicked(HookBaseClass): - @staticmethod - def flipped_state(check_state: QtCore.Qt.CheckState) -> QtCore.Qt.CheckState: - """Flips a unchecked state to checked and any other state to unchecked.""" - return ( - QtCore.Qt.Checked - if check_state == QtCore.Qt.Unchecked - else QtCore.Qt.Unchecked - ) def single( self, 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 + ) From 1bdf9240c7930f0b894eabf228da6c9e72d6ba87 Mon Sep 17 00:00:00 2001 From: Joseph Yu Date: Thu, 9 Apr 2026 10:52:24 +0100 Subject: [PATCH 3/3] updated docstrings comment typing --- hooks/tree_node_clicked.py | 31 ++++++++++++++----- .../publish_tree_widget.py | 2 +- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/hooks/tree_node_clicked.py b/hooks/tree_node_clicked.py index e130b96a..22b7a20e 100644 --- a/hooks/tree_node_clicked.py +++ b/hooks/tree_node_clicked.py @@ -27,7 +27,10 @@ """ -from typing import TypeVar, Union +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + import sgtk from sgtk.platform.qt import QtCore, QtGui @@ -35,30 +38,42 @@ TreeNode = TypeVar("TreeNode", bound=QtGui.QTreeWidgetItem) CustomTreeWidget = TypeVar("CustomTreeWidget", bound=QtGui.QFrame) -API = Union[TypeVar("PublishItem"), TypeVar("PublishTask"), None] +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: API, + api: PublishItem | PublishTask | None, buttons: QtCore.Qt.MouseButtons, modifiers: QtCore.Qt.KeyboardModifiers, - ): # type: (...) -> None - """Single click callback on a `.TreeNodeBase` (a `.QtGui.QTreeWidgetItem`).""" + ) -> 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: API, + api: PublishItem | PublishTask | None, buttons: QtCore.Qt.MouseButtons, modifiers: QtCore.Qt.KeyboardModifiers, - ): # type: (...) -> None - """Double click callback on a `.TreeNodeBase` (a `.QtGui.QTreeWidgetItem`).""" + ) -> 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/python/tk_multi_publish2/publish_tree_widget/publish_tree_widget.py b/python/tk_multi_publish2/publish_tree_widget/publish_tree_widget.py index 7f0555d5..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,7 +62,7 @@ def __init__(self, parent): self.addTopLevelItem(self._summary_node) self._summary_node.setHidden(True) - # forward double clicks on items to the items themselves + # forward clicks on items to the items themselves self.itemDoubleClicked.connect(self._click_slot_factory("double")) self.itemClicked.connect(self._click_slot_factory("single"))