Skip to content
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Fixed

- [UUM-133861] Fixed "Look rotation viewing vector is zero" log being spammed when holding shift while using a create tool such as Create Sprite.
<<<<<<< bugfix/sample-menuaction-disabled
- [UUM-133530] Fixed the `Set Double Sided` custom action in the Editor Sample, which was previously remaining disabled.
- [UUM-133530] Ensured that the context menu respects the value of `MenuAction.enabled`.
=======
- [UUM-133531] Fixed component icons in Light theme.
>>>>>>> master
- [UUM-133526] Material Editor: fixed a warning (`GUI Error: Invalid GUILayout state in MaterialEditor view.`) that was thrown when deleting an extra material slot.
- Fixed warnings related to obsolete API calls with Unity 6.4 and onwards.

Expand Down
2 changes: 1 addition & 1 deletion Editor/EditorCore/ProBuilderToolsContexts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public override void PopulateMenu(DropdownMenu menu)
menu.AppendAction(title, _ => EditorAction.Start(new MenuActionSettings(action, HasPreview(action))), GetStatus(action));
}
else
menu.AppendAction(GetMenuTitle(action, title), _ => action.PerformAction());
menu.AppendAction(GetMenuTitle(action, title), _ => action.PerformAction(), GetStatus(action));
}
}

Expand Down
4 changes: 3 additions & 1 deletion Samples~/Editor/CustomAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ public class MakeFacesDoubleSided : MenuAction
/// <returns></returns>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low
The XML documentation tag is empty and is generally used for methods rather than properties. For properties, the

or tags are standard. Consider removing the empty tag to keep the documentation clean.

🤖 Helpful? 👍/👎

public override bool enabled
{
get { return MeshSelection.selectedFaceCount > 0; }
get { return base.enabled && MeshSelection.selectedFaceCount > 0; }
}

protected override bool hasFileMenuEntry => false;

/// <summary>
/// This action is applicable in Face selection modes.
/// </summary>
Expand Down
184 changes: 184 additions & 0 deletions Tests/Editor/Actions/ContextualMenuTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
using NUnit.Framework;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEditor.ProBuilder;
using UnityEngine;
using UnityEngine.ProBuilder;
using UnityEngine.UIElements;

/// <summary>
/// Test the construction of the contextual menu in the Scene View.
/// It doesn't cover the cases where a MenuAction has a file menu entry.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand this comment. Aren't you doing a file menu entry test below?
MenuActionWithoutMenuItem_hasFileMenuEntry

/// </summary>
public class ContextualMenuTests
{
[ProBuilderMenuAction]
public class ConfigurableMenuAction : MenuAction
{
internal const string actionName = "Action Without File Menu Entry";
internal static bool userHasFileMenuEntry { get; set; }
internal static SelectMode userSelectMode { get; set; }
internal static bool userEnabled { get; set; }

public override ToolbarGroup group
{
get { return ToolbarGroup.Geometry; }
}

public override Texture2D icon => null;

public override string iconPath => string.Empty;

public override TooltipContent tooltip => new TooltipContent(
actionName,
@"This action should not have a file menu entry."
);

public ConfigurableMenuAction()
{
}

protected override ActionResult PerformActionImplementation()
{
return ActionResult.Success;
}

public override SelectMode validSelectModes => userSelectMode;
public override bool enabled => userEnabled;
protected internal override bool hasFileMenuEntry => userHasFileMenuEntry;
}

ProBuilderMesh m_PBMesh;

[SetUp]
public void Setup()
{
m_PBMesh = ShapeFactory.Instantiate(typeof(UnityEngine.ProBuilder.Shapes.Plane));
}

[TearDown]
public void TearDown()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't this need [Teardown] ?

{
if (m_PBMesh)
Object.DestroyImmediate(m_PBMesh.gameObject);
}

[Test]
[TestCase(true, ExpectedResult = false)]
[TestCase(false, ExpectedResult = true)]
public bool MenuActionWithoutMenuItem_hasFileMenuEntry(bool hasFileMenuEntry)
{
MeshSelection.SetSelection(m_PBMesh.gameObject);
ActiveEditorTracker.sharedTracker.ForceRebuild();

ToolManager.SetActiveContext<PositionToolContext>();
Tools.current = Tool.Move;
ProBuilderEditor.selectMode = SelectMode.Face;
ActiveEditorTracker.sharedTracker.ForceRebuild();

ConfigurableMenuAction.userHasFileMenuEntry = hasFileMenuEntry;
ConfigurableMenuAction.userSelectMode = SelectMode.Any;
ConfigurableMenuAction.userEnabled = true;

DropdownMenu menu = new DropdownMenu();
PositionToolContext ctx = Resources.FindObjectsOfTypeAll<PositionToolContext>()?[0];
Assume.That(ctx, Is.Not.Null);

ctx.PopulateMenu(menu);
menu.PrepareForDisplay(null);
DropdownMenuAction foundInMenu = null;
foreach (var t in menu.MenuItems())
{
if (t is not DropdownMenuAction menuAction)
continue;

if (menuAction.name == ConfigurableMenuAction.actionName)
{
foundInMenu = menuAction;
break;
}
}

Assert.That(foundInMenu, Is.Not.Null, "MenuAction should be present in the Contextual Menu regardless of hasFileMenuEntry value.");
return (foundInMenu.status == DropdownMenuAction.Status.Normal);
}

[Test]
[TestCase(SelectMode.Edge, ExpectedResult = false)]
[TestCase(SelectMode.Face, ExpectedResult = true)]
[TestCase(SelectMode.Vertex, ExpectedResult = false)]
public bool MenuAction_SelectModeSetToFace_EnabledOnlyForFaceSelection(SelectMode mode)
{
MeshSelection.SetSelection(m_PBMesh.gameObject);
ActiveEditorTracker.sharedTracker.ForceRebuild();

ToolManager.SetActiveContext<PositionToolContext>();
Tools.current = Tool.Move;
ProBuilderEditor.selectMode = mode;
ActiveEditorTracker.sharedTracker.ForceRebuild();

ConfigurableMenuAction.userHasFileMenuEntry = false;
ConfigurableMenuAction.userSelectMode = SelectMode.Face;
ConfigurableMenuAction.userEnabled = true;

DropdownMenu menu = new DropdownMenu();
PositionToolContext ctx = Resources.FindObjectsOfTypeAll<PositionToolContext>()?[0];
Assume.That(ctx, Is.Not.Null);

ctx.PopulateMenu(menu);
menu.PrepareForDisplay(null);
DropdownMenuAction foundInMenu = null;
foreach (var t in menu.MenuItems())
{
if (t is not DropdownMenuAction menuAction)
continue;

if (menuAction.name == ConfigurableMenuAction.actionName)
{
foundInMenu = menuAction;
break;
}
}

// MenuAction is expected to be present in the menu only when the mode matches.
return (foundInMenu != null);
}

[Test]
[TestCase(true, ExpectedResult = true)]
[TestCase(false, ExpectedResult = false)]
public bool MenuAction_enabledPropertyIsFollowedByMenu(bool enabled)
{
MeshSelection.SetSelection(m_PBMesh.gameObject);
ActiveEditorTracker.sharedTracker.ForceRebuild();

ToolManager.SetActiveContext<PositionToolContext>();
Tools.current = Tool.Move;
ProBuilderEditor.selectMode = SelectMode.Face;
ActiveEditorTracker.sharedTracker.ForceRebuild();

ConfigurableMenuAction.userHasFileMenuEntry = false;
ConfigurableMenuAction.userSelectMode = SelectMode.Face;
ConfigurableMenuAction.userEnabled = enabled;

DropdownMenu menu = new DropdownMenu();
PositionToolContext ctx = Resources.FindObjectsOfTypeAll<PositionToolContext>()?[0];
Assume.That(ctx, Is.Not.Null);

ctx.PopulateMenu(menu);
menu.PrepareForDisplay(null);
DropdownMenuAction foundInMenu = null;
foreach (var t in menu.MenuItems())
{
if (t is not DropdownMenuAction menuAction)
continue;

if (menuAction.name == ConfigurableMenuAction.actionName)
{
foundInMenu = menuAction;
break;
}
}
return (foundInMenu.status == DropdownMenuAction.Status.Normal);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lots of duplicated code, can't we combine this in a single method?

Copy link
Contributor

@varinotmUnity varinotmUnity Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven;t tested yet, but quickly with AI, it proposed this:

private DropdownMenuAction GetMenuActionFromContext(SelectMode selectMode)
{
    MeshSelection.SetSelection(m_PBMesh.gameObject);
    ToolManager.SetActiveContext<PositionToolContext>();
    ProBuilderEditor.selectMode = selectMode;
    ActiveEditorTracker.sharedTracker.ForceRebuild();
    
    var ctx = Resources.FindObjectsOfTypeAll<PositionToolContext>()?[0];
    Assume.That(ctx, Is.Not.Null);
    
    var menu = new DropdownMenu();
    ctx.PopulateMenu(menu);
    menu.PrepareForDisplay(null);
    
    foreach (var item in menu.MenuItems())
    {
        if (item is DropdownMenuAction menuAction && 
            menuAction.name == ConfigurableMenuAction.actionName)
        {
            return menuAction;
        }
    }
    
    return null;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and your test becomes

[Test]
[TestCase(true, ExpectedResult = false)]
[TestCase(false, ExpectedResult = true)]
public bool MenuActionWithoutMenuItem_hasFileMenuEntry(bool hasFileMenuEntry)
{
    ConfigurableMenuAction.userHasFileMenuEntry = hasFileMenuEntry;
    ConfigurableMenuAction.userSelectMode = SelectMode.Any;
    ConfigurableMenuAction.userEnabled = true;

    var foundInMenu = GetMenuActionFromContext(SelectMode.Face);
    
    Assert.That(foundInMenu, Is.Not.Null, "MenuAction should be present...");
    return (foundInMenu.status == DropdownMenuAction.Status.Normal);
}

2 changes: 2 additions & 0 deletions Tests/Editor/Actions/ContextualMenuTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.