Skip to content

Commit

Permalink
Merge pull request #3 from Garume/v0.3.0
Browse files Browse the repository at this point in the history
V0.3.0
  • Loading branch information
Garume authored May 1, 2024
2 parents 48527e1 + 7363ac2 commit 1f5ed26
Show file tree
Hide file tree
Showing 21 changed files with 213 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"name": "ContextCircleMenu.Editor",
"rootNamespace": "",
"references": [],
"references": [
"GUID:976b98c4db6ffd04d9cdc43fa24bfbe8"
],
"includePlatforms": [
"Editor"
],
Expand Down
30 changes: 28 additions & 2 deletions Assets/ContextCircleMenu/Editor/ContextCircleMenuLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

namespace ContextCircleMenu.Editor
{
/// <summary>
/// Initializes and manages the Context Circle Menu within the Unity Scene View.
/// </summary>
[InitializeOnLoad]
public static class ContextCircleMenuLoader
{
Expand Down Expand Up @@ -58,22 +61,45 @@ private static void Initialize()
foreach (var method in attributes)
{
var attribute = method.GetCustomAttribute<ContextCircleMenuAttribute>(false);
builder.AddMenu(attribute.Path, attribute, method);
builder.AddMenu(attribute, method);
}
});
else
_contextCircleMenu.CreateMenu(_onBuild);

_activeSceneView.rootVisualElement.Add(_contextCircleMenu);
}



/// <summary>
/// Event that allows customization of the Context Circle Menu construction.
/// </summary>
/// <remarks>
/// This event provides a mechanism to extend or modify the content of the Context Circle Menu
/// dynamically at runtime. It's invoked during the initialization phase of the menu in the Scene View.
/// Subscribers can add custom menu items or modify existing ones by manipulating the CircleMenuBuilder
/// instance provided in the event arguments.
/// Example Usage:
/// ContextCircleMenuLoader.OnBuild += builder =>
/// {
/// // Adds a nested menu item under "Custom" with an action to log "custom/test" to the console
/// builder.AddMenu("Custom/Debug Test", new GUIContent(), () => Debug.Log("custom/test"));
/// // Adds another top-level menu item with an action to log "test"
/// builder.AddMenu("Debug Test", new GUIContent(), () => Debug.Log("test"));
/// };
/// This example demonstrates how developers can add items that perform actions like logging to the console,
/// but it could also be used to trigger any method reflecting more complex functionality.
/// </remarks>
public static event Action<CircleMenuBuilder> OnBuild
{
add => _onBuild += value;
remove => _onBuild -= value;
}


/// <summary>
/// Shortcut that toggles the visibility of the Context Circle Menu based on keyboard input.
/// </summary>
[ClutchShortcut("Context Circle Menu/Show Menu", typeof(SceneView), KeyCode.A)]
public static void ToggleMenuClutch(ShortcutArguments args)
{
Expand Down
23 changes: 21 additions & 2 deletions Assets/ContextCircleMenu/Editor/Core/CircleMenuBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,40 @@ internal CircleMenu Build(IMenuControllable menu)
return root;
}

public void AddMenu(string attributePath, ContextCircleMenuAttribute attribute, MethodInfo method)
/// <summary>
/// Adds a menu from attribute.
/// </summary>
/// <param name="attribute"></param>
/// <param name="method"></param>
public void AddMenu(ContextCircleMenuAttribute attribute, MethodInfo method)
{
AddMenu(new AttributeCircleMenuFactory(attributePath, attribute, method));
AddMenu(new AttributeCircleMenuFactory(attribute, method));
}

/// <summary>
/// Adds a menu manually.
/// </summary>
/// <param name="path"></param>
/// <param name="content"></param>
/// <param name="action"></param>
public void AddMenu(string path, GUIContent content, Action action)
{
AddMenu(new CircleMenuFactory(path, content, action));
}

/// <summary>
/// Adds a factory to the list of menu item factories.
/// </summary>
/// <param name="factory">The factory responsible for creating the menu item.</param>
public void AddMenu(ICircleMenuFactory factory)
{
_factories.Add(factory);
}

/// <summary>
/// Sets a custom factory for creating folder-like menu items, allowing for further customization of menu folders.
/// </summary>
/// <param name="factory">The factory to use for creating folder menu items.</param>
public void ConfigureFolder(IFolderCircleMenuFactory factory)
{
_folderFactory = factory;
Expand Down
14 changes: 9 additions & 5 deletions Assets/ContextCircleMenu/Editor/Core/CircleMenuFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,31 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;

namespace ContextCircleMenu.Editor
{
public class AttributeCircleMenuFactory : ICircleMenuFactory
{
private readonly ContextCircleMenuAttribute _attribute;
private readonly GUIContent _content;
private readonly MethodInfo _method;

public AttributeCircleMenuFactory(string path, ContextCircleMenuAttribute attribute, MethodInfo method)
public AttributeCircleMenuFactory(ContextCircleMenuAttribute attribute, MethodInfo method)
{
PathSegments = path.Split("/");
_attribute = attribute;
PathSegments = attribute.Path.Split("/");
_method = method;

_content = !string.IsNullOrEmpty(attribute.IconPath)
? EditorGUIUtility.IconContent(attribute.IconPath)
: default;
}

public IEnumerable<string> PathSegments { get; }

public CircleMenu Create()
{
return new LeafCircleMenu(PathSegments.Last(), _attribute.Icon, () => _method.Invoke(null, null));
return new LeafCircleMenu(PathSegments.Last(), _content, () => _method.Invoke(null, null));
}
}

Expand Down
32 changes: 23 additions & 9 deletions Assets/ContextCircleMenu/Editor/Core/CircularMenus/CircleMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

namespace ContextCircleMenu.Editor
{
/// <summary>
/// Represents a menu item in the circle menu.
/// </summary>
public abstract class CircleMenu
{
private readonly int _radius;
Expand All @@ -14,6 +17,9 @@ public abstract class CircleMenu
protected internal readonly CircleMenu Parent;
protected internal readonly string Path;
protected internal readonly bool ShouldCloseMenuAfterSelection;

private VisualElement[] _buttonElements;
private VisualElement[] _utilityElements;
protected internal Action OnSelected;

public CircleMenu(string path, GUIContent icon, Action onSelected, CircleMenu parent, int radius = 100,
Expand All @@ -29,28 +35,36 @@ public CircleMenu(string path, GUIContent icon, Action onSelected, CircleMenu pa

internal ReadOnlySpan<VisualElement> CreateElements()
{
var buttons = CreateButtons();
var utilityElements = CreateUtilityElements();
_buttonElements ??= CreateButtons();
_utilityElements ??= CreateUtilityElements();

for (var i = 0; i < buttons.Length; i++)
for (var i = 0; i < _buttonElements.Length; i++)
{
var button = buttons[i];
var button = _buttonElements[i];
button.transform.position = Vector3.zero;
var to = Vector2.zero + GetPositionForIndex(i, buttons.Length);
var to = Vector2.zero + GetPositionForIndex(i, _buttonElements.Length);
button.experimental.animation.Position(to, 100);
}

var pool = ArrayPool<VisualElement>.Shared;
var buffer = pool.Rent(buttons.Length + utilityElements.Length);
buttons.CopyTo(buffer, 0);
utilityElements.CopyTo(buffer, buttons.Length);
var combinedSpan = new Span<VisualElement>(buffer, 0, buttons.Length + utilityElements.Length);
var buffer = pool.Rent(_buttonElements.Length + _utilityElements.Length);
_buttonElements.CopyTo(buffer, 0);
_utilityElements.CopyTo(buffer, _buttonElements.Length);
var combinedSpan = new Span<VisualElement>(buffer, 0, _buttonElements.Length + _utilityElements.Length);
pool.Return(buffer);
return combinedSpan;
}

/// <summary>
/// Creates the buttons for the menu.
/// </summary>
/// <returns></returns>
protected abstract VisualElement[] CreateButtons();

/// <summary>
/// Creates the utility elements for the menu.
/// </summary>
/// <returns></returns>
protected virtual VisualElement[] CreateUtilityElements()
{
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

namespace ContextCircleMenu.Editor
{
/// <inheritdoc />
public class FolderCircleMenu : CircleMenu
{
private readonly Action _onBack;
Expand All @@ -18,7 +19,7 @@ public FolderCircleMenu(string path, Action<CircleMenu> onOpen, Action onBack, C
_onBack = onBack;
}


/// <inheritdoc />
protected override VisualElement[] CreateButtons()
{
var buttons = new VisualElement[Children.Count + 1];
Expand All @@ -38,6 +39,7 @@ protected override VisualElement[] CreateButtons()
return buttons;
}

/// <inheritdoc />
protected override VisualElement[] CreateUtilityElements()
{
var label = new Label(Path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

namespace ContextCircleMenu.Editor
{
/// <inheritdoc />
public class LeafCircleMenu : CircleMenu
{
public LeafCircleMenu(string path, GUIContent icon, Action onSelected, CircleMenu parent = null,
int radius = 100) : base(path, icon, onSelected, parent, radius)
{
}

/// <inheritdoc />
protected override VisualElement[] CreateButtons()
{
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

namespace ContextCircleMenu.Editor
{
/// <inheritdoc />
public class RootCircleMenu : CircleMenu
{
public RootCircleMenu() : base("root", default, null, null)
{
}

/// <inheritdoc />
protected override VisualElement[] CreateButtons()
{
var buttons = new VisualElement[Children.Count];
Expand All @@ -26,6 +28,7 @@ protected override VisualElement[] CreateButtons()
return buttons;
}

/// <inheritdoc />
protected override VisualElement[] CreateUtilityElements()
{
return new VisualElement[] { new Label("") };
Expand Down
54 changes: 53 additions & 1 deletion Assets/ContextCircleMenu/Editor/Core/ContextCircleMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

namespace ContextCircleMenu.Editor
{
/// <summary>
/// Represents a circular context menu that can be dynamically constructed and controlled within the Unity Editor.
/// </summary>
public class ContextCircleMenu : VisualElement, IMenuControllable
{
private static readonly Color AnnulusColor = new(0.02f, 0.02f, 0.02f, 0.8f);
Expand All @@ -20,6 +23,12 @@ public class ContextCircleMenu : VisualElement, IMenuControllable

private CircleMenu _selectedMenu;

/// <summary>
/// Initializes a new instance of the ContextCircleMenu with specified dimensions and target.
/// </summary>
/// <param name="width">Width of the menu.</param>
/// <param name="height">Height of the menu.</param>
/// <param name="target">The UI element in which the menu will appear.</param>
public ContextCircleMenu(float width, float height, VisualElement target)
{
_width = width;
Expand All @@ -46,12 +55,17 @@ public ContextCircleMenu(float width, float height, VisualElement target)
RegisterCallback<DetachFromPanelEvent>(OnDetach);
}

/// <summary>
/// Determines whether mouse events are blocked by the menu.
/// </summary>
public bool BlockMouseEvents { get; set; }


public bool IsVisible => style.display == DisplayStyle.Flex;


/// <summary>
/// Displays the menu and repositions it based on the current mouse position.
/// </summary>
public void Show()
{
if (IsVisible) return;
Expand All @@ -62,13 +76,20 @@ public void Show()
Rebuild();
}

/// <summary>
/// Hides the menu.
/// </summary>
public void Hide()
{
if (!IsVisible) return;

style.display = DisplayStyle.None;
}

/// <summary>
/// Opens a specified menu within the context circle menu.
/// </summary>
/// <param name="menu">The menu to display.</param>
public void Open(CircleMenu menu)
{
if (!IsVisible) return;
Expand All @@ -77,6 +98,9 @@ public void Open(CircleMenu menu)
Rebuild();
}

/// <summary>
/// Returns to the parent menu if available.
/// </summary>
public void Back()
{
if (_selectedMenu.Parent != null) Open(_selectedMenu.Parent);
Expand Down Expand Up @@ -119,6 +143,9 @@ private void OnClick(ClickEvent evt)
Hide();
}

/// <summary>
/// Force selection of the active button in the menu.
/// </summary>
public bool TryForceSelect()
{
var button = Children().OfType<CircularButton>().FirstOrDefault(b => b.IsEntered);
Expand Down Expand Up @@ -165,6 +192,31 @@ private void OnGenerateVisualContent(MeshGenerationContext context)
}


/// <summary>
/// Configures and builds the menu based on a provided configuration action.
/// </summary>
/// <param name="configureMenu">
/// An action delegate that configures the menu. This delegate is responsible for defining
/// the structure and behavior of the menu items within the context menu.
/// </param>
/// <remarks>
/// This method is used to dynamically construct the menu content at runtime. It allows for flexible modification
/// and extension of the menu items by external code. The method receives an action delegate that takes a
/// <see cref="CircleMenuBuilder" /> as a parameter, which is used to add, configure, and organize menu items.
/// The <see cref="CircleMenuBuilder" /> provides methods such as <code>AddMenu</code> which are used to define each
/// item's
/// label, icon, and the action it triggers when selected. This design allows for the encapsulation of the menu's
/// construction logic, making the <see cref="ContextCircleMenu" /> class highly modular and customizable.
/// Example usage:
/// <code>
/// contextCircleMenu.CreateMenu(builder => {
/// builder.AddMenu("File/Open", new GUIContent(), () => { Debug.Log("Open clicked"); });
/// builder.AddMenu("File/Save", new GUIContent(), () => { Debug.Log("Save clicked"); });
/// });
/// </code>
/// This example configures the menu to have "Open" and "Save" options under a "File" submenu, with each menu item
/// triggering a logging action when clicked.
/// </remarks>
public void CreateMenu(Action<CircleMenuBuilder> configureMenu)
{
var builder = new CircleMenuBuilder();
Expand Down
Loading

0 comments on commit 1f5ed26

Please sign in to comment.