Optional Context Menu Items in Unity

You can use the MenuItem attribute to create new menu items – including context menu items – for users in the Unity editor.

There is however one major shortcoming in the system: it does not give you any means to allow your users to pick and choose which items they would like to have in their menus. Every menu item which you create with the attribute will be present for all your users, all the time.

The Case For Making Menu Items Optional

Users can have multiple third party packages added to their project, which all might add some of their own items to the menus.

This can often lead to the menus being cluttered with several items that the users don’t really even care to use.

Even worse, there can also be overlap between the menu items offered by different packages, so you end up having completely redundant items in your menu, or you could lose access to some menu items due to conflicting names.

Is There Really No Way To Remove Them?

Unity recently introduced the Shortcuts Manager, which can be used to remap or disable the shortcuts of menu items. This is great step forward, but doesn’t help us with actually removing the menu items altogether, just their shortcuts.

Then there are the validation methods for menu items, which can be used to “disable” menu items based on the current context. Unfortunately this only means that they are greyed out in the menus, so this also doesn’t help us unclutter our menus or fix menu items conflicts.

Let’s Hack Together A Solution!

I came up with a pattern that can be used to get around this limitation, and I thought I’d share it here in case others struggling with the same problem might find it useful.

The gist of the solution is to wrap the menu item methods inside preprocessor directives, which are only defined when the user has opted to have the menu item in question be shown for them.

Below is an example of the pattern in use. Save the code inside a file called “OptionalMenuItems.cs” and place it inside a folder named “Editor” within your project, if you want to try it out.

// defines for all menu items that can be be disabled
#region MenuPreferences
#define MenuItem1Enabled
#define MenuItem2Enabled
#endregion

using System;
using System.IO;
using JetBrains.Annotations;
using UnityEditor;
using UnityEngine;

/// <summary>
/// A class that contains menu items which can be enabled and disabled based on user preferences.
/// </summary>
[InitializeOnLoad]
public static class OptionalMenuItems
{
	/// <summary>
	/// This is called during startup and after each assembly reload due to the usage of the InitializeOnLoad attribute.
	/// </summary>
	static OptionalMenuItems()
	{
		ApplyMenuItemVisibilityPreferences();
	}
		
	/// <summary> Comments out or restores defines inside MenuPreferences region based on user preferences. </summary>
	public static void ApplyMenuItemVisibilityPreferences()
	{
		var scriptFile = FindScriptFile();
		var scriptText = scriptFile.text;
		int preferencesStart = scriptText.IndexOf("#region MenuPreferences", StringComparison.Ordinal) + 23;
		if(preferencesStart == -1)
		{
			throw new InvalidDataException("#region MenuPreferences missing from OptionalMenuItems.cs");
		}
		int preferencesEnd = scriptText.IndexOf("#endregion", preferencesStart, StringComparison.Ordinal);
		if(preferencesEnd == -1)
		{
			throw new InvalidDataException("#endregion missing from OptionalMenuItems.cs");
		}
		
		string beforePreferences = scriptText.Substring(0, preferencesStart);
		string afterPreferences = scriptText.Substring(preferencesEnd);
		string menuPreferences = scriptText.Substring(preferencesStart , preferencesEnd - preferencesStart);

		bool scriptChanged = false;

		SetMenuItemEnabled(ref menuPreferences, "MenuItem1", EditorPrefs.GetBool("MenuItems/MenuItem1", true), ref scriptChanged);
		SetMenuItemEnabled(ref menuPreferences, "MenuItem2", EditorPrefs.GetBool("MenuItems/MenuItem2", true), ref scriptChanged);

		if(scriptChanged)
		{
			string localPath = AssetDatabase.GetAssetPath(scriptFile);
			string fullPath = Path.Combine(Application.dataPath, localPath.Substring(7));
			File.WriteAllText(fullPath, beforePreferences + menuPreferences + afterPreferences);
			EditorUtility.SetDirty(scriptFile);
			AssetDatabase.Refresh();
		}
	}

	[NotNull]
	private static MonoScript FindScriptFile()
	{
		var assetGuids = AssetDatabase.FindAssets("OptionalMenuItems t:MonoScript");
		var guid = assetGuids[0];
		var path = AssetDatabase.GUIDToAssetPath(guid);
		return AssetDatabase.LoadAssetAtPath<MonoScript>(path);
	}

	/// <summary> Comments out or restores define for method inside MenuPreferences region based on value of setEnabled. </summary>
	/// <param name="menuPreferences"> [in,out] The contents of MenuPreferences region, that can be altered based on value of setEnabled. </param>
	/// <param name="methodName"> Name of the method. MenuPreferences region should have a define whose name matches methodName followed by "Enabled". </param>
	/// <param name="setEnabled"> True to enable, false to disable the set. </param>
	/// <param name="changed"> [in,out] True if contents of menuPreferences were changed. </param>
	private static void SetMenuItemEnabled(ref string menuPreferences, string methodName, bool setEnabled, ref bool changed)
	{
		string define = "#define "+methodName + "Enabled";
		string defineDisabled = "//" + define;

		int isDisabled = menuPreferences.IndexOf(defineDisabled, StringComparison.Ordinal);
			
		//if menu item should be set to enabled
		if(setEnabled)
		{
			// if menu item is currently disabled
			if(isDisabled != -1)
			{
				//enable menu item by removing comment characters from in front of define
				menuPreferences = menuPreferences.Substring(0, isDisabled) + menuPreferences.Substring(isDisabled + 2);
				changed = true;
			}
		}
		// if menu item is currently enabled
		else if(isDisabled == -1)
		{
			//disable menu item by commenting define out
			int i = menuPreferences.IndexOf(define, StringComparison.Ordinal);
			if(i != -1)
			{
				menuPreferences = menuPreferences.Substring(0, i) + "//" + menuPreferences.Substring(i);
				changed = true;
			}
			else
			{
				Debug.LogError("Failed to find \""+define+"\" in OptionalMenuItems.cs");
			}
		}
	}

	#if MenuItem1Enabled
	[MenuItem("CONTEXT/Object/Menu Item 1"), UsedImplicitly]
	private static void MenuItem1()
	{
		Debug.Log("Menu Item 1 selected");
	}
	#endif

	#if MenuItem2Enabled
	[MenuItem("CONTEXT/Object/Menu Item 2"), UsedImplicitly]
	private static void MenuItem2()
	{
		Debug.Log("Menu Item 2 selected");
	}
	#endif
}

How The Code Works

The class contains two example context menu item methods defined at the end of the class: MenuItem1 and MenuItem2. The menu items mapped to these methods are displayed whenever the user opens the context menu for any Object in the inspector window. Note how both of the methods have been wrapped inside #if directives (#if MenuItem1Enabled and #if MenuItem2Enabled respectively).

When Unity is launched, the method ApplyMenuItemVisibilityPreferences inside the class will be invoked. The method will then only enable context menu items when the user has enabled them in their preferences.

EditorPrefs is used in this example to hold the user preferences, but you could replace it with whatever you use to hold your user preferences.

The code will rewrite it’s own source code in small ways, by commenting out the definitions inside the MenuPreferences region corresponding with the menu items that the user does not want. This will cause all traces of these menu items to disappear from the compiler output.

If you wanted to add new optional menu items inside the OptionalMenuItems class, you would need to do the following steps:

  1. Create your method and add the MenuItem attribute to it just like you normally would.
    • E.g. “private static void MyMethod()”.
  2. Wrap the method inside a #if directive named the same as the method followed by “Enabled”.
    • E.g. “#if MyMethodEnabled”.
  3. Add the definition for this preprocessor directive inside the MenuPreferences region at the top of the class.
    • E.g. “#define MyMethodEnabled”.
  4. Add a new SetMenuItemEnabled call inside ApplyMenuItemVisibilityPreferences to handle the newly created menu item.
    • E.g. “SetMenuItemEnabled(ref menuPreferences, “MyMethod”, EditorPrefs.GetBool(“MenuItems/MyMethod”, true), ref scriptChanged);

When Do Changes In Preferences Take Effect?

Changes in user preferences are automatically applied every time the editor is launched.

You can also call ApplyMenuItemVisibilityPreferences manually after the user has made changes to related preference items.

If It Works…

This pattern is not exactly what I would call a pretty solution, but it works. And for me in the end it is the end result, the user experience, that I care about most. And I don’t think that there are more “elegant” solutions available to this problem at this point, not until Unity adds built-in support for removing unwanted menu items.

There are different ways to define preprosessor directives in Unity besides editing script assets directly. The definitions could be injected to the Scripting Define Symbols list in Player Settings. Or they could be inserted into a mcs.rsp file. But the first solution could end up seriously cluttering up the user’s Scripting Define Symbols list, which could be quite irritating. As for the second option, I don’t feel confident that it would work as reliably across all different versions of Unity as the one I went with, as I’ve heard multiple reports of people running into issues when trying to work with them.

Could This Be More Automated?

It would be possible to make it easier to add new optional menu items with fewer steps required from the user, if code generation was used to a much greater degree.

The MenuPreferences region and its contents could be autogenerated from scratch, without the user needing to prefill it.

Preprocessor directives could be automatically added around all menu item methods inside the class.

With certain adjustments, ApplyMenuItemVisibilityPreferences could even use reflection to get all methods inside the class and automatically call SetMenuItemEnabled for all of them.

But implementing all this would also make the code much more complex, which means that it would also be more likely to break.

I also wanted to keep things simple for the purposes of this article, so that it would not become too complex to explain everything that is happening in the code.

Finally I also just don’t think that it is too much trouble to fill in those few things manually every time. You don’t usually need to add more than a few menu items into Unity anyways to justify the time investment to introduce all this additional complexity.