TreeView and ListView missing context clicked callback

In IMGUI TreeView, there’s some methods that can be overridden to provide context click functionality for the treeView and items:

These are missing on TreeView and ListView in UIToolkit.
If we’re supposed to handle this manually in UIToolkit, how can we capture the item that was context clicked?

Looking at the source code for BaseVerticalCollectionView, the mouse down method only handles left-click:

private void ProcessPointerDown(IPointerEvent evt)
{
    if (!HasValidDataAndBindings())
        return;

    if (!evt.isPrimary)
        return;

    if (evt.button != (int)MouseButton.LeftMouse)
        return;

    if (evt.pointerType != PointerType.mouse)
    {
        m_TouchDownPosition = evt.position;
        return;
    }

    DoSelect(evt.localPosition, evt.clickCount, evt.actionKey, evt.shiftKey);
}

And getting the item from a click is not publicly accessible (hidden behind virtualizationController):

private void DoSelect(Vector2 localPosition, int clickCount, bool actionKey, bool shiftKey)
{
    var clickedIndex = virtualizationController.GetIndexFromPosition(localPosition);

I want to handle context click for an entire row, not the cells in each row.
How can I do this?

You can just register your own callback.

There is no callback I can see for when rows are created. So how can I register a callback for a row?

I would just register a click event for the whole tree-view and check which list view element has focused/is checked.

Is there an inbuilt method to find and element in a tree view from a position?

I would imagine that being quite complex, given there’s a scroll view, variable heights, multiple levels of nesting.

All I can find is an internal class VirtualizationController that does this. But again, no publicly available.

Maybe it’s possible to copy that method, but haven’t looked to deep into it. Just so frustrating that there’s all this useful functionality hidden away

You shouldn’t need to deal with any of that. Like I said just check what elements are selected in the tree view.

You could also just register callbacks to the elements you add to the tree in .makeItem.

I need to be able to context click non selected elements.

Thanks for the advice though. Will continue searching

Just for posterity, looks like in Unity 6, they have also made right click select items, which matches the IMGUI version. So your idea of using selected indices would work there. Unfortunately I’m on 2022.3 :melting_face:

Hello!

You can do this by using the BindItem method and capturing a context click event on a row. Something like this. You could even attempt listening for a ContextClickEvent

        listView.makeItem = MakeItem;
        listView.bindItem = BindItem;

    VisualElement MakeItem()
    {
        var item = new Label();
        item.style.flexGrow = 1;
        return item;
    }

    void BindItem(VisualElement element, int index)
    {
        var label = element as Label;
        label.text = $"Item {index}";

        // Register callback for right mouse button click
        label.RegisterCallback<PointerUpEvent>(evt =>
        {
            if (evt.button == (int)MouseButton.RightMouse)
            {
                evt.StopPropagation();
                HandleContextClick(index);
            }
        }, TrickleDown.TrickleDown);
    }


Hope that helps!

2 Likes

I’m using a MultiColumnTreeView so there is no bind method for a row there. Only per cell.

I managed to figure it out using a forced mouse event for right-click:

class MyTreeView : MultiColumnTreeView {

	private VisualElement itemContainer;
	private ContextualMenuManipulator contextManipulator;

	public MyTreeView() 
		: base(CreateColumns()) {

		itemContainer = this.Q<VisualElement>(className: ScrollView.contentUssClassName);

		RegisterCallback<AttachToPanelEvent>(OnAttached);
		RegisterCallback<DetachFromPanelEvent>(OnDetached);
	}

	private void OnAttached(AttachToPanelEvent evt) {

		//In Unity 6, right-click is already handled as part of the mouse press
		//in 2022.3, this is not the case. So we need to synthesize a mouse event so items are selected on right-click

#if !UNITY_6000_0_OR_NEWER
		RegisterRightClickSelection(itemContainer);
#endif
		itemContainer.AddManipulator(contextManipulator = new ContextualMenuManipulator(ContextClicked));
	}

	private void OnDetached(DetachFromPanelEvent evt) {

#if !UNITY_6000_0_OR_NEWER
		UnregisterRightClickSelection(itemContainer);
#endif
		itemContainer.RemoveManipulator(contextManipulator);
	}

#if !UNITY_6000_0_OR_NEWER
	private void RegisterRightClickSelection(VisualElement element) {

		element.RegisterCallback<PointerDownEvent>(OnRightClick);
	}

	private void UnregisterRightClickSelection(VisualElement element) {

		element.UnregisterCallback<PointerDownEvent>(OnRightClick);
	}

	private void OnRightClick(PointerDownEvent evt) {

		if (evt.button != (int)MouseButton.RightMouse || !evt.isPrimary) {
			return;
		}

		Event mouseEvent = new Event() {
			type = EventType.MouseDown,
			modifiers = evt.modifiers,
			mousePosition = evt.originalMousePosition,	
			pointerType = UnityEngine.PointerType.Mouse,
			button = (int)MouseButton.LeftMouse,
			clickCount = evt.clickCount
		};

		using (PointerDownEvent pointerEvent = PointerDownEvent.GetPooled(mouseEvent)) {

			evt.target.SendEvent(pointerEvent);
		}
	}
#endif

	private void ContextClicked(ContextualMenuPopulateEvent evt) {

		Debug.Log(selectedIndices.Count()); //yay, we can show a context menu for selected items now!

		AddMenuItems(evt.menu);
	}
}

Esentially, synthesize a right-click event as a left-click event so selection can happen